diff --git a/etsy_python/v3/models/Receipt.py b/etsy_python/v3/models/Receipt.py index 1a19a69..604c9fa 100644 --- a/etsy_python/v3/models/Receipt.py +++ b/etsy_python/v3/models/Receipt.py @@ -1,10 +1,32 @@ -from typing import List, Optional +from typing import List, Optional, TypedDict from etsy_python.v3.models.Request import Request +class CustomsItem(TypedDict): + # HS_code preserves Etsy's spec casing (Harmonized System code); do not snake_case. + country_of_origin: Optional[str] + declared_value: Optional[float] + HS_code: Optional[str] + + class CreateReceiptShipmentRequest(Request): - nullable: List[str] = [] + # Spec marks these fields nullable: true. Strings + array are added so callers + # can pass empty values to clear them; floats are intentionally excluded so + # legitimate zero values (free label, zero duty) serialize as 0, not null. + nullable: List[str] = [ + "mail_class", + "weight_units", + "dimension_units", + "shipping_label_currency", + "revenue_eligibility", + "ship_from_country", + "ship_to_country", + "incoterm", + "customs_data", + "duty_currency", + "ship_date", + ] mandatory: List[str] = [] def __init__( @@ -13,11 +35,45 @@ def __init__( carrier_name: Optional[str] = None, send_bcc: Optional[bool] = None, note_to_buyer: Optional[str] = None, + mail_class: Optional[str] = None, + weight: Optional[float] = None, + weight_units: Optional[str] = None, + length: Optional[float] = None, + width: Optional[float] = None, + height: Optional[float] = None, + dimension_units: Optional[str] = None, + shipping_label_cost: Optional[float] = None, + shipping_label_currency: Optional[str] = None, + revenue_eligibility: Optional[str] = None, + ship_from_country: Optional[str] = None, + ship_to_country: Optional[str] = None, + incoterm: Optional[str] = None, + customs_data: Optional[List[CustomsItem]] = None, + duty_amount: Optional[float] = None, + duty_currency: Optional[str] = None, + ship_date: Optional[str] = None, ): self.tracking_code = tracking_code self.carrier_name = carrier_name self.send_bcc = send_bcc self.note_to_buyer = note_to_buyer + self.mail_class = mail_class + self.weight = weight + self.weight_units = weight_units + self.length = length + self.width = width + self.height = height + self.dimension_units = dimension_units + self.shipping_label_cost = shipping_label_cost + self.shipping_label_currency = shipping_label_currency + self.revenue_eligibility = revenue_eligibility + self.ship_from_country = ship_from_country + self.ship_to_country = ship_to_country + self.incoterm = incoterm + self.customs_data = customs_data + self.duty_amount = duty_amount + self.duty_currency = duty_currency + self.ship_date = ship_date super().__init__( nullable=CreateReceiptShipmentRequest.nullable, diff --git a/specs/baseline.json b/specs/baseline.json index 11a08b8..b5d9280 100644 --- a/specs/baseline.json +++ b/specs/baseline.json @@ -298,11 +298,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -760,11 +760,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -983,11 +983,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } }, { @@ -1210,7 +1210,7 @@ ], "responses": { "200": { - "description": "The metatdata for a file associated with a digital listing.", + "description": "The metadata for a file associated with a digital listing.", "content": { "application/json": { "schema": { @@ -1643,11 +1643,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } }, { @@ -1813,11 +1813,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -2342,11 +2342,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -2443,11 +2443,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } }, { @@ -2716,11 +2716,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -2830,11 +2830,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -2933,11 +2933,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } }, { @@ -3056,11 +3056,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -3239,11 +3239,11 @@ { "name": "supports_multiple_personalization_questions", "in": "query", - "description": "This query parameter indicates that the caller supports up to 5 personalization questions and the following question types: 'text_input', 'dropdown', 'unlabeled_upload', 'labeled_upload'. Sending this param without updating your application can lead to inadvertantly deleting seller-entered data.", + "description": "This query parameter indicates that the caller supports up to 5 personalization questions and the following question types: 'text_input', 'dropdown', 'unlabeled_upload', 'labeled_upload'. Sending this param without updating your application can lead to inadvertently deleting seller-entered data.", "required": false, "schema": { "type": "boolean", - "description": "This query parameter indicates that the caller supports up to 5 personalization questions and the following question types: 'text_input', 'dropdown', 'unlabeled_upload', 'labeled_upload'. Sending this param without updating your application can lead to inadvertantly deleting seller-entered data.", + "description": "This query parameter indicates that the caller supports up to 5 personalization questions and the following question types: 'text_input', 'dropdown', 'unlabeled_upload', 'labeled_upload'. Sending this param without updating your application can lead to inadvertently deleting seller-entered data.", "nullable": true } } @@ -4470,11 +4470,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -5171,7 +5171,7 @@ ], "responses": { "200": { - "description": "The metatdata for a video associated with a listing.", + "description": "The metadata for a video associated with a listing.", "content": { "application/json": { "schema": { @@ -6206,7 +6206,7 @@ }, "was_paid": { "type": "boolean", - "description": "When `true`, returns receipts where the seller has recieved payment for the receipt. When `false`, returns receipts where payment has not been received.", + "description": "When `true`, returns receipts where the seller has received payment for the receipt. When `false`, returns receipts where payment has not been received.", "nullable": true } } @@ -6417,11 +6417,11 @@ { "name": "was_paid", "in": "query", - "description": "When `true`, returns receipts where the seller has recieved payment for the receipt. When `false`, returns receipts where payment has not been received.", + "description": "When `true`, returns receipts where the seller has received payment for the receipt. When `false`, returns receipts where payment has not been received.", "required": false, "schema": { "type": "boolean", - "description": "When `true`, returns receipts where the seller has recieved payment for the receipt. When `false`, returns receipts where payment has not been received.", + "description": "When `true`, returns receipts where the seller has received payment for the receipt. When `false`, returns receipts where payment has not been received.", "nullable": true } }, @@ -6601,11 +6601,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -6726,7 +6726,7 @@ ], "requestBody": { "content": { - "application/x-www-form-urlencoded": { + "application/json": { "schema": { "type": "object", "properties": { @@ -6745,6 +6745,126 @@ "note_to_buyer": { "type": "string", "description": "Message to include in notification to the buyer." + }, + "mail_class": { + "type": "string", + "description": "The service level of postal or carrier service selected for the shipment (e.g., First-Class, Priority, Ground, Express).", + "nullable": true + }, + "weight": { + "type": "number", + "description": "The total weight of the package.", + "format": "float", + "nullable": true + }, + "weight_units": { + "type": "string", + "description": "Unit of measurement used for package weight (oz, grams, etc.).", + "nullable": true + }, + "length": { + "type": "number", + "description": "Longest side of the package.", + "format": "float", + "nullable": true + }, + "width": { + "type": "number", + "description": "Second longest side of the package.", + "format": "float", + "nullable": true + }, + "height": { + "type": "number", + "description": "Third longest side of the package.", + "format": "float", + "nullable": true + }, + "dimension_units": { + "type": "string", + "description": "Unit of measurement used for package dimensions (in, cm...).", + "nullable": true + }, + "shipping_label_cost": { + "type": "number", + "description": "The purchase price the seller paid for the shipping label.", + "format": "float", + "nullable": true + }, + "shipping_label_currency": { + "type": "string", + "description": "The currency in which the shipping label was purchased.", + "nullable": true + }, + "revenue_eligibility": { + "type": "string", + "description": "A flag indicating if the shipment is tied to a revenue share agreement between Etsy and the vendor.", + "nullable": true + }, + "ship_from_country": { + "type": "string", + "description": "Where the package ships from.", + "nullable": true + }, + "ship_to_country": { + "type": "string", + "description": "Package destination.", + "nullable": true + }, + "incoterm": { + "type": "string", + "description": "The specific incoterm (e.g., DDU, DDP) designated for the shipment.", + "nullable": true + }, + "customs_data": { + "type": "array", + "description": "Contains custom data like country of origin, declared value and HS code.", + "nullable": true, + "items": { + "type": "object", + "properties": { + "country_of_origin": { + "type": "string", + "description": "The country in which the goods originate from.", + "nullable": true, + "default": null + }, + "declared_value": { + "type": "number", + "description": "The commercial value of the goods.", + "format": "float", + "nullable": true, + "default": null + }, + "HS_code": { + "type": "string", + "description": "The standardized global system (Harmonized System) for classifying traded products.", + "nullable": true, + "default": null + } + }, + "required": [ + "country_of_origin", + "declared_value", + "HS_code" + ] + } + }, + "duty_amount": { + "type": "number", + "description": "The estimated or actual amount of import duties and taxes assessed by customs for the shipment.", + "format": "float", + "nullable": true + }, + "duty_currency": { + "type": "string", + "description": "The currency in which the duty was paid.", + "nullable": true + }, + "ship_date": { + "type": "string", + "description": "The date package was shipped.", + "nullable": true } } } @@ -8601,11 +8721,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -9889,11 +10009,11 @@ { "name": "legacy", "in": "query", - "description": "This parameter needed to enable new parameters and response values related to processing profiles.", + "description": "This parameter is needed to enable new parameters and response values related to processing profiles.", "required": false, "schema": { "type": "boolean", - "description": "This parameter needed to enable new parameters and response values related to processing profiles." + "description": "This parameter is needed to enable new parameters and response values related to processing profiles." } } ], @@ -12628,7 +12748,7 @@ "BuyerTaxonomyPropertyScale": { "type": "object", "x-resource-id": "BuyerTaxonomyPropertyScale", - "description": "A scale defnining the assignable increments for the property values available to specific product properties.", + "description": "A scale defining the assignable increments for the property values available to specific product properties.", "properties": { "scale_id": { "type": "integer", @@ -14038,13 +14158,13 @@ }, "shipping_from_country_iso": { "type": "string", - "description": "The country iso the shop is shipping from.", + "description": "The country ISO the shop is shipping from.", "format": "ISO 3166-1 alpha-2", "nullable": true }, "shop_location_country_iso": { "type": "string", - "description": "The country iso where the shop is located.", + "description": "The country ISO where the shop is located.", "format": "ISO 3166-1 alpha-2", "nullable": true }, @@ -14209,7 +14329,8 @@ "inactive", "deleted", "flagged" - ] + ], + "default": "active" } } }, @@ -15058,6 +15179,11 @@ "formatted_value": { "type": "string", "description": "Value of the variation entered by the buyer." + }, + "question_id": { + "type": "integer", + "description": "[Personalization only] The ID of the original personalization question.", + "nullable": true } } }, @@ -15307,7 +15433,7 @@ "PaymentAdjustmentItem": { "type": "object", "x-resource-id": "PaymentAdjustmentItem", - "description": "A payemnt adjustment line item for a payment adjustment.", + "description": "A payment adjustment line item for a payment adjustment.", "properties": { "payment_adjustment_id": { "type": "integer", @@ -15473,7 +15599,7 @@ "nullable": true }, "posted_fees": { - "description": "The total value of the fees posted once the purchase ships. Etsy refunds a proportional amount of the fees when a seller refunds a buyer. When the seller issues a refund prior to shipping, the posted amount is less then the original.", + "description": "The total value of the fees posted once the purchase ships. Etsy refunds a proportional amount of the fees when a seller refunds a buyer. When the seller issues a refund prior to shipping, the posted amount is less than the original.", "oneOf": [ { "$ref": "#/components/schemas/Money" @@ -15771,7 +15897,7 @@ ] }, "subtotal": { - "description": "A number equal to the total_price minus coupon discounts. Does not included tax or shipping costs.", + "description": "A number equal to the total_price minus coupon discounts. Does not include tax or shipping costs.", "oneOf": [ { "$ref": "#/components/schemas/Money" @@ -15779,7 +15905,7 @@ ] }, "total_price": { - "description": "A number equal to the sum of the individual listings' (price * quantity). Does not included tax or shipping costs.", + "description": "A number equal to the sum of the individual listings' (price * quantity). Does not include tax or shipping costs.", "oneOf": [ { "$ref": "#/components/schemas/Money" @@ -16308,7 +16434,7 @@ "TaxonomyPropertyScale": { "type": "object", "x-resource-id": "TaxonomyPropertyScale", - "description": "A scale defnining the assignable increments for the property values available to specific product properties.", + "description": "A scale defining the assignable increments for the property values available to specific product properties.", "properties": { "scale_id": { "type": "integer", @@ -16556,7 +16682,7 @@ }, "country_iso": { "type": "string", - "description": "The country iso where the shop is located.", + "description": "The country ISO where the shop is located.", "format": "ISO 3166-1 alpha-2" }, "is_working": { @@ -16760,7 +16886,7 @@ "ShopSections": { "type": "object", "x-resource-id": "ShopSections", - "description": "All the sections in a sprecific Shop.", + "description": "All the sections in a specific Shop.", "properties": { "count": { "type": "integer", diff --git a/tests/test_receipt_models.py b/tests/test_receipt_models.py index bad3dd1..a16d745 100644 --- a/tests/test_receipt_models.py +++ b/tests/test_receipt_models.py @@ -1,5 +1,6 @@ from etsy_python.v3.models.Receipt import ( CreateReceiptShipmentRequest, + CustomsItem, UpdateShopReceiptRequest, ) @@ -30,6 +31,118 @@ def test_with_note_and_bcc(self): assert result["send_bcc"] is True assert result["note_to_buyer"] == "Shipping soon!" + def test_with_shipping_label_fields(self): + req = CreateReceiptShipmentRequest( + tracking_code="TRACK123", + carrier_name="USPS", + mail_class="Priority", + weight=1.5, + weight_units="lb", + length=10.0, + width=8.0, + height=4.0, + dimension_units="in", + shipping_label_cost=8.95, + shipping_label_currency="USD", + ship_from_country="US", + ship_to_country="CA", + ship_date="2026-05-07", + ) + result = req.get_dict() + assert result["mail_class"] == "Priority" + assert result["weight"] == 1.5 + assert result["weight_units"] == "lb" + assert result["length"] == 10.0 + assert result["width"] == 8.0 + assert result["height"] == 4.0 + assert result["dimension_units"] == "in" + assert result["shipping_label_cost"] == 8.95 + assert result["shipping_label_currency"] == "USD" + assert result["ship_from_country"] == "US" + assert result["ship_to_country"] == "CA" + assert result["ship_date"] == "2026-05-07" + + def test_with_customs_and_duty_fields(self): + customs = [ + { + "country_of_origin": "US", + "declared_value": 25.00, + "HS_code": "9503.00.00", + } + ] + req = CreateReceiptShipmentRequest( + tracking_code="TRACK123", + customs_data=customs, + incoterm="DDP", + duty_amount=2.50, + duty_currency="USD", + revenue_eligibility="eligible", + ) + result = req.get_dict() + assert result["customs_data"] == customs + assert result["incoterm"] == "DDP" + assert result["duty_amount"] == 2.50 + assert result["duty_currency"] == "USD" + assert result["revenue_eligibility"] == "eligible" + + def test_new_optional_fields_excluded_when_none(self): + req = CreateReceiptShipmentRequest(tracking_code="TRACK123") + result = req.get_dict() + for key in ( + "mail_class", "weight", "weight_units", "length", "width", "height", + "dimension_units", "shipping_label_cost", "shipping_label_currency", + "revenue_eligibility", "ship_from_country", "ship_to_country", + "incoterm", "customs_data", "duty_amount", "duty_currency", "ship_date", + ): + assert key not in result + + def test_zero_floats_serialize_as_zero(self): + # Float fields are intentionally excluded from the nullable list so that + # legitimate zero values (free shipping label, no duty) round-trip as 0 + # rather than null. + req = CreateReceiptShipmentRequest( + weight=0.0, + length=0.0, + width=0.0, + height=0.0, + shipping_label_cost=0.0, + duty_amount=0.0, + ) + result = req.get_dict() + assert result["weight"] == 0.0 + assert result["length"] == 0.0 + assert result["width"] == 0.0 + assert result["height"] == 0.0 + assert result["shipping_label_cost"] == 0.0 + assert result["duty_amount"] == 0.0 + + def test_nullable_strings_send_null_when_empty(self): + # Spec marks these fields nullable: true; empty string is normalized to + # an explicit JSON null on the wire so callers can clear server state. + req = CreateReceiptShipmentRequest( + mail_class="", + weight_units="", + ship_from_country="", + duty_currency="", + ) + result = req.get_dict() + assert result["mail_class"] is None + assert result["weight_units"] is None + assert result["ship_from_country"] is None + assert result["duty_currency"] is None + + def test_customs_data_typed_dict(self): + # CustomsItem is a TypedDict; passing it works exactly like a plain dict + # but gives callers IDE completion / type-checker validation. + item: CustomsItem = { + "country_of_origin": "US", + "declared_value": 42.50, + "HS_code": "9503.00.00", + } + req = CreateReceiptShipmentRequest(customs_data=[item]) + result = req.get_dict() + assert result["customs_data"] == [item] + class TestUpdateShopReceiptRequest: def test_no_mandatory_fields(self):