From 6be620a2d83b340564bf33aace0f451e194ffc5c Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Wed, 22 Jul 2026 21:31:16 -0500 Subject: [PATCH 01/11] feat(order): complete Order request dataclasses (items, shipment, payer address/phone, transaction_security) Additive changes; dict path unchanged (backward compatible): - New dataclasses: OrderItemRequest, OrderShipmentRequest (+OrderShipmentAddress, OrderShipmentFreeMethod), OrderPayerPhone, OrderPayerAddress, OrderTransactionSecurity (nested under config.online) - OrderCreateRequest: adds missing root fields (description, marketplace, marketplace_fee, expiration_time, checkout_available_at) and completes payer with phone + address - order_request_to_dict() helper: recursive asdict() with None-filtering (DD-3) - order.create() dual-accepts dict or dataclass (converts via order_request_to_dict) - README: promotes Orders API and references AP example; legacy Payments demoted All snake_case keys per canonical reference (sdk-go + sdk-dotnet). 16 new offline tests; existing dict path unaffected. Closes: orders-sdk-typed-request-classes Co-Authored-By: Claude Sonnet 4.6 (1M context) --- README.md | 70 +++- mercadopago/resources/__init__.py | 46 +++ mercadopago/resources/order.py | 20 +- mercadopago/resources/order_create.py | 148 +++++++ mercadopago/resources/order_item.py | 38 ++ mercadopago/resources/order_payer.py | 47 +++ mercadopago/resources/order_shipment.py | 72 ++++ .../resources/order_transaction_security.py | 21 + tests/test_order_request_dataclasses.py | 362 ++++++++++++++++++ 9 files changed, 817 insertions(+), 7 deletions(-) create mode 100644 mercadopago/resources/order_create.py create mode 100644 mercadopago/resources/order_item.py create mode 100644 mercadopago/resources/order_payer.py create mode 100644 mercadopago/resources/order_shipment.py create mode 100644 mercadopago/resources/order_transaction_security.py create mode 100644 tests/test_order_request_dataclasses.py diff --git a/README.md b/README.md index 61512e1..3d3fc30 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,10 @@ First time using Mercado Pago? Create your [Mercado Pago account](https://www.me Copy your `Access Token` in the [credentials panel](https://www.mercadopago.com/developers/panel/credentials) and replace the text `YOUR_ACCESS_TOKEN` with it. -### Simple usage - +### Simple usage — Orders API + +The [Orders API](https://www.mercadopago.com/developers/en/reference/online-payments/checkout-api/create-order/post) (`/v1/orders`) is the recommended way to accept payments. `sdk.order().create()` accepts either a plain `dict` or the optional typed request dataclasses (`OrderCreateRequest` and friends). Both routes produce the exact same JSON body. + ```python import mercadopago @@ -32,6 +34,68 @@ request_options.custom_headers = { 'x-idempotency-key': '' } +order_data = { + "type": "online", + "total_amount": "100.00", + "external_reference": "ext_ref_1234", + "transactions": { + "payments": [ + { + "amount": "100.00", + "payment_method": { + "id": "master", + "type": "credit_card", + "token": "CARD_TOKEN", + "installments": 1, + }, + } + ] + }, + "payer": { + "email": "test_user_123456@testuser.com" + }, +} +result = sdk.order().create(order_data, request_options) +order = result["response"] + +print(order) +``` + +#### Typed request classes (optional) + +Instead of a `dict`, you can build the request with the typed dataclasses. `None` +fields are omitted from the JSON body automatically, matching the `dict` route. + +```python +import mercadopago +from mercadopago.resources.order_create import OrderCreateRequest, OrderPayerRequest +from mercadopago.resources.order_item import OrderItemRequest + +sdk = mercadopago.SDK("YOUR_ACCESS_TOKEN") + +order = OrderCreateRequest( + type="online", + total_amount="100.00", + external_reference="ext_ref_1234", + payer=OrderPayerRequest(email="test_user_123456@testuser.com"), + items=[OrderItemRequest(title="A book", unit_price="100.00", quantity=1)], +) + +result = sdk.order().create(order) +print(result["response"]) +``` + +For a complete recurring / Automatic Payments example (stored credential, +subscription data, integration data), see +[`examples/order/create_order_automatic_payment.py`](examples/order/create_order_automatic_payment.py). + +### Creating a payment (legacy Payments API) + +```python +import mercadopago + +sdk = mercadopago.SDK("YOUR_ACCESS_TOKEN") + payment_data = { "transaction_amount": 100, "token": "CARD_TOKEN", @@ -42,7 +106,7 @@ payment_data = { "email": 'test_user_123456@testuser.com' } } -result = sdk.payment().create(payment_data, request_options) +result = sdk.payment().create(payment_data) payment = result["response"] print(payment) diff --git a/mercadopago/resources/__init__.py b/mercadopago/resources/__init__.py index c75d60d..6d025ff 100644 --- a/mercadopago/resources/__init__.py +++ b/mercadopago/resources/__init__.py @@ -17,6 +17,7 @@ from mercadopago.resources.merchant_order import MerchantOrder from mercadopago.resources.oauth import OAuth from mercadopago.resources.order import Order +from mercadopago.resources.order_automatic_payments import OrderAutomaticPayments from mercadopago.resources.order_checkout_pro import ( OrderCheckoutProConfig, OrderCheckoutProInstallments, @@ -26,6 +27,33 @@ OrderCheckoutProTrack, OrderCheckoutProDict, ) +from mercadopago.resources.order_create import ( + OrderCreateRequest, + OrderIdentification, + OrderPayerRequest, + order_request_to_dict, +) +from mercadopago.resources.order_integration_data import ( + OrderIntegrationData, + OrderSponsor, +) +from mercadopago.resources.order_item import OrderItemRequest +from mercadopago.resources.order_payer import ( + OrderPayerAddress, + OrderPayerPhone, +) +from mercadopago.resources.order_shipment import ( + OrderShipmentAddress, + OrderShipmentFreeMethod, + OrderShipmentRequest, +) +from mercadopago.resources.order_stored_credential import OrderStoredCredential +from mercadopago.resources.order_subscription_data import ( + OrderInvoicePeriod, + OrderSubscriptionData, + OrderSubscriptionSequence, +) +from mercadopago.resources.order_transaction_security import OrderTransactionSecurity from mercadopago.resources.payment import Payment from mercadopago.resources.payment_methods import PaymentMethods from mercadopago.resources.plan import Plan @@ -50,6 +78,7 @@ 'MerchantOrder', 'OAuth', 'Order', + 'OrderAutomaticPayments', 'OrderCheckoutProConfig', 'OrderCheckoutProInstallments', 'OrderCheckoutProInterestFree', @@ -57,6 +86,22 @@ 'OrderCheckoutProPaymentMethod', 'OrderCheckoutProTrack', 'OrderCheckoutProDict', + 'OrderCreateRequest', + 'OrderIdentification', + 'OrderIntegrationData', + 'OrderInvoicePeriod', + 'OrderItemRequest', + 'OrderPayerAddress', + 'OrderPayerPhone', + 'OrderPayerRequest', + 'OrderShipmentAddress', + 'OrderShipmentFreeMethod', + 'OrderShipmentRequest', + 'OrderSponsor', + 'OrderStoredCredential', + 'OrderSubscriptionData', + 'OrderSubscriptionSequence', + 'OrderTransactionSecurity', 'Payment', 'PaymentMethods', 'Plan', @@ -67,4 +112,5 @@ 'RequestOptions', 'Subscription', 'User', + 'order_request_to_dict', ) diff --git a/mercadopago/resources/order.py b/mercadopago/resources/order.py index ecb17ec..7613420 100644 --- a/mercadopago/resources/order.py +++ b/mercadopago/resources/order.py @@ -7,7 +7,10 @@ `API reference `_ """ +from dataclasses import is_dataclass + from mercadopago.core import MPBase +from mercadopago.resources.order_create import order_request_to_dict class Order(MPBase): """Manages orders and their associated transactions. @@ -88,21 +91,30 @@ def search(self, filters=None, request_options=None): def create(self, order_object, request_options=None): """Creates a new order. + Accepts either a plain ``dict`` (the historical, dynamic route) or an + :class:`~mercadopago.resources.order_create.OrderCreateRequest` typed + dataclass. When a dataclass is passed it is converted to a ``dict`` with + ``None`` fields omitted, producing the same JSON body as the dict route + (DD-3). The dict route is unchanged and fully backward compatible. + Args: order_object: Dict describing the order (items, transactions, - payer, etc.). + payer, etc.), or an ``OrderCreateRequest`` dataclass instance. request_options: Per-call configuration overrides. Raises: - ValueError: If *order_object* is not a ``dict``. + ValueError: If *order_object* is neither a ``dict`` nor a dataclass + instance. Returns: dict: Created order including its ``id``. Reference: https://www.mercadopago.com/developers/en/reference/online-payments/checkout-api/create-order/post """ - if not isinstance(order_object, dict): - raise ValueError("Param order_object must be a Dictionary") + if is_dataclass(order_object) and not isinstance(order_object, type): + order_object = order_request_to_dict(order_object) + elif not isinstance(order_object, dict): + raise ValueError("Param order_object must be a Dictionary or an OrderCreateRequest") return self._post(uri="/v1/orders", data=order_object, request_options=request_options) diff --git a/mercadopago/resources/order_create.py b/mercadopago/resources/order_create.py new file mode 100644 index 0000000..5a8acf3 --- /dev/null +++ b/mercadopago/resources/order_create.py @@ -0,0 +1,148 @@ +"""Root request dataclasses for the MercadoPago Orders API. + +These dataclasses model the ``POST /v1/orders`` request body. They are an +optional, typed alternative to passing a plain ``dict`` to +:meth:`~mercadopago.resources.order.Order.create`. Build the request with the +dataclasses and convert it to a ``dict`` with ``dataclasses.asdict()``; ``None`` +fields are filtered out before serialization so the resulting JSON matches the +dict path exactly. + +The dict path continues to work unchanged; these dataclasses are purely additive. +""" +from dataclasses import ( + asdict, + dataclass, + field, + is_dataclass, +) +from typing import ( + List, + Optional, +) + +from mercadopago.resources.order_item import OrderItemRequest +from mercadopago.resources.order_integration_data import OrderIntegrationData +from mercadopago.resources.order_payer import ( + OrderPayerAddress, + OrderPayerPhone, +) +from mercadopago.resources.order_shipment import OrderShipmentRequest + + +def _filter_none(value): + """Recursively drop ``None`` values from dicts/lists (DD-3, omit-empty).""" + if isinstance(value, dict): + return {k: _filter_none(v) for k, v in value.items() if v is not None} + if isinstance(value, list): + return [_filter_none(v) for v in value] + return value + + +def order_request_to_dict(request): + """Convert a request dataclass into a ``dict`` with ``None`` fields omitted. + + This is the canonical way to turn any of the Orders API request dataclasses + (``OrderCreateRequest`` and its nested objects) into the ``dict`` accepted by + :meth:`~mercadopago.resources.order.Order.create`. It runs + ``dataclasses.asdict()`` and then recursively strips keys whose value is + ``None`` so the resulting JSON matches the plain-dict path exactly (DD-3). + + Args: + request: A request dataclass instance (or any dataclass instance). + + Returns: + dict: The request as a plain ``dict`` with ``None`` fields removed. + + Raises: + TypeError: If *request* is not a dataclass instance. + """ + if not is_dataclass(request) or isinstance(request, type): + raise TypeError("request must be a dataclass instance") + return _filter_none(asdict(request)) + + +@dataclass +class OrderIdentification: + """Payer identification document for an order request. + + Attributes: + type: Identification document type (e.g. ``"CPF"``). Type: str. + number: Identification document number. Type: str. + """ + + type: Optional[str] = None + number: Optional[str] = None + + +@dataclass +class OrderPayerRequest: + """Payer information for an order request. + + Attributes: + email: Payer email address. Type: str. + first_name: Payer first name. Type: str. + last_name: Payer last name. Type: str. + customer_id: Stored customer identifier. Type: str. + entity_type: Payer entity type (``"individual"`` | ``"association"``). + Type: str. + identification: Payer identification document. + phone: Payer phone number. + address: Payer address. + """ + + email: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + customer_id: Optional[str] = None + entity_type: Optional[str] = None + identification: Optional[OrderIdentification] = None + phone: Optional[OrderPayerPhone] = None + address: Optional[OrderPayerAddress] = None + + +@dataclass +class OrderCreateRequest: + """Root request body for creating an order. + + Optional typed alternative to a plain ``dict``. Convert with + ``dataclasses.asdict()``; ``None`` fields are filtered out before sending. + + Attributes: + type: Order type (e.g. ``"online"``). Type: str. + external_reference: Merchant-side reference for the order. Type: str. + total_amount: Total order amount as a decimal string. Type: str. + currency: Currency identifier (e.g. ``"BRL"``). Type: str. + capture_mode: Capture mode (e.g. ``"automatic_async"``). Type: str. + processing_mode: Processing mode (e.g. ``"automatic"``). Type: str. + description: Free-text order description. Type: str. + marketplace: Marketplace identifier. Type: str. + marketplace_fee: Marketplace fee as a decimal string. Type: str. + expiration_time: Order expiration time (ISO 8601 / duration). Type: str. + checkout_available_at: When the checkout becomes available. Type: str. + transactions: Transactions payload (payments). + payer: Payer information. + items: Line items in the order. + config: Order configuration payload. + shipment: Shipment configuration. + integration_data: Integration metadata. + additional_info: Free-form additional information (kept as-is). + """ + + type: Optional[str] = None + external_reference: Optional[str] = None + total_amount: Optional[str] = None + currency: Optional[str] = None + capture_mode: Optional[str] = None + processing_mode: Optional[str] = None + description: Optional[str] = None + marketplace: Optional[str] = None + marketplace_fee: Optional[str] = None + expiration_time: Optional[str] = None + checkout_available_at: Optional[str] = None + transactions: Optional[dict] = None + payer: Optional[OrderPayerRequest] = None + items: Optional[List[OrderItemRequest]] = field(default=None) + config: Optional[dict] = None + shipment: Optional[OrderShipmentRequest] = None + integration_data: Optional[OrderIntegrationData] = None + additional_info: Optional[dict] = None diff --git a/mercadopago/resources/order_item.py b/mercadopago/resources/order_item.py new file mode 100644 index 0000000..9d6eb14 --- /dev/null +++ b/mercadopago/resources/order_item.py @@ -0,0 +1,38 @@ +"""Dataclass for line items in order requests.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class OrderItemRequest: + """A single line item within an order request. + + Use this dataclass to build an entry of the ``items`` array when creating + an order. Convert to dict with ``dataclasses.asdict()`` (``None`` fields are + filtered out before sending, per the omit-empty behavior of the API). + + Attributes: + title: Display name of the item. Type: str. + type: Item type/category classifier. Type: str. + warranty: Whether the item includes a warranty. Type: bool. + event_date: ISO 8601 date associated with the item (e.g. event tickets). + Type: str. + unit_price: Price per unit as a decimal string (e.g. ``"100.00"``). + Type: str. + external_code: Merchant-side external identifier for the item. Type: str. + category_id: MercadoPago category identifier. Type: str. + description: Free-text description of the item. Type: str. + picture_url: URL of an image representing the item. Type: str. + quantity: Number of units. Type: int. + """ + + title: Optional[str] = None + type: Optional[str] = None + warranty: Optional[bool] = None + event_date: Optional[str] = None + unit_price: Optional[str] = None + external_code: Optional[str] = None + category_id: Optional[str] = None + description: Optional[str] = None + picture_url: Optional[str] = None + quantity: Optional[int] = None diff --git a/mercadopago/resources/order_payer.py b/mercadopago/resources/order_payer.py new file mode 100644 index 0000000..a25573b --- /dev/null +++ b/mercadopago/resources/order_payer.py @@ -0,0 +1,47 @@ +"""Dataclasses for payer contact data in order requests.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class OrderPayerPhone: + """Payer phone number for an order request. + + Use this dataclass to build the ``payer.phone`` payload. Convert to dict with + ``dataclasses.asdict()`` (``None`` fields are filtered out before sending). + + Attributes: + area_code: Phone area code. Type: str. + number: Phone number without the area code. Type: str. + """ + + area_code: Optional[str] = None + number: Optional[str] = None + + +@dataclass +class OrderPayerAddress: + """Payer address for an order request. + + Use this dataclass to build the ``payer.address`` payload. Convert to dict with + ``dataclasses.asdict()`` (``None`` fields are filtered out before sending). + + Attributes: + zip_code: Postal / ZIP code. Type: str. + street_name: Name of the street. Type: str. + street_number: Street number. Type: str. + neighborhood: Neighborhood name. Type: str. + city: City name. Type: str. + state: State or province. Type: str. + complement: Additional address details. Type: str. + country: Country name or code. Type: str. + """ + + zip_code: Optional[str] = None + street_name: Optional[str] = None + street_number: Optional[str] = None + neighborhood: Optional[str] = None + city: Optional[str] = None + state: Optional[str] = None + complement: Optional[str] = None + country: Optional[str] = None diff --git a/mercadopago/resources/order_shipment.py b/mercadopago/resources/order_shipment.py new file mode 100644 index 0000000..d50c63c --- /dev/null +++ b/mercadopago/resources/order_shipment.py @@ -0,0 +1,72 @@ +"""Dataclasses for shipment data in order requests.""" +from dataclasses import ( + dataclass, + field, +) +from typing import ( + List, + Optional, +) + + +@dataclass +class OrderShipmentAddress: + """Delivery address for an order shipment. + + Attributes: + street_name: Name of the street. Type: str. + street_number: Street number. Type: str. + zip_code: Postal / ZIP code. Type: str. + floor: Floor within the building. Type: str. + apartment: Apartment / unit identifier. Type: str. + neighborhood: Neighborhood name. Type: str. + state: State or province. Type: str. + city: City name. Type: str. + complement: Additional address details. Type: str. + """ + + street_name: Optional[str] = None + street_number: Optional[str] = None + zip_code: Optional[str] = None + floor: Optional[str] = None + apartment: Optional[str] = None + neighborhood: Optional[str] = None + state: Optional[str] = None + city: Optional[str] = None + complement: Optional[str] = None + + +@dataclass +class OrderShipmentFreeMethod: + """A free-shipping method offered for the order. + + Attributes: + id: Identifier of the free shipping method. Type: int. + """ + + id: Optional[int] = None + + +@dataclass +class OrderShipmentRequest: + """Shipment configuration for an order request. + + Use this dataclass to build the ``shipment`` payload when creating an order. + Convert to dict with ``dataclasses.asdict()`` (``None`` fields are filtered + out before sending, per the omit-empty behavior of the API). + + Attributes: + mode: Shipping mode (e.g. ``"me2"``, ``"custom"``). Type: str. + local_pickup: Whether the buyer picks up the item locally. Type: bool. + cost: Shipping cost as a decimal string. Type: str. + free_shipping: Whether shipping is free. Type: bool. + free_methods: Free shipping methods available for the order. + address: Delivery address for the shipment. + """ + + mode: Optional[str] = None + local_pickup: Optional[bool] = None + cost: Optional[str] = None + free_shipping: Optional[bool] = None + free_methods: Optional[List[OrderShipmentFreeMethod]] = field(default=None) + address: Optional[OrderShipmentAddress] = None diff --git a/mercadopago/resources/order_transaction_security.py b/mercadopago/resources/order_transaction_security.py new file mode 100644 index 0000000..906f09b --- /dev/null +++ b/mercadopago/resources/order_transaction_security.py @@ -0,0 +1,21 @@ +"""Dataclass for transaction security data in order requests.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class OrderTransactionSecurity: + """Transaction security settings for an order request. + + Nested under ``config.online.transaction_security`` (not at the request root). + Convert to dict with ``dataclasses.asdict()`` (``None`` fields are filtered + out before sending, per the omit-empty behavior of the API). + + Attributes: + validation: Validation strategy applied to the transaction (e.g. + ``"complete"``). Type: str. + liability_shift: Liability shift indicator for 3-D Secure flows. Type: str. + """ + + validation: Optional[str] = None + liability_shift: Optional[str] = None diff --git a/tests/test_order_request_dataclasses.py b/tests/test_order_request_dataclasses.py new file mode 100644 index 0000000..f651ae8 --- /dev/null +++ b/tests/test_order_request_dataclasses.py @@ -0,0 +1,362 @@ +"""Offline unit + integration tests for the typed Order request dataclasses. + +These tests do not hit the live API. They verify: + * new dataclasses produce the correct snake_case keys, + * None filtering (DD-3) removes unset fields, + * the existing dict path still works (backward compatibility), + * the typed Automatic Payments flow serializes correctly. +""" +import dataclasses +import json +import unittest + +import mercadopago +from mercadopago.http import HttpClient +from mercadopago.resources.order_automatic_payments import OrderAutomaticPayments +from mercadopago.resources.order_create import ( + OrderCreateRequest, + OrderIdentification, + OrderPayerRequest, + order_request_to_dict, +) +from mercadopago.resources.order_integration_data import ( + OrderIntegrationData, + OrderSponsor, +) +from mercadopago.resources.order_item import OrderItemRequest +from mercadopago.resources.order_payer import ( + OrderPayerAddress, + OrderPayerPhone, +) +from mercadopago.resources.order_shipment import ( + OrderShipmentAddress, + OrderShipmentFreeMethod, + OrderShipmentRequest, +) +from mercadopago.resources.order_stored_credential import OrderStoredCredential +from mercadopago.resources.order_subscription_data import ( + OrderInvoicePeriod, + OrderSubscriptionData, + OrderSubscriptionSequence, +) +from mercadopago.resources.order_transaction_security import OrderTransactionSecurity + + +class _CapturingHttpClient(HttpClient): + """HttpClient stub that captures the request body instead of sending it.""" + + def __init__(self): + self.last_url = None + self.last_data = None + + def post(self, url, headers, data=None, params=None, timeout=None, maxretries=None): # noqa: D401 + self.last_url = url + self.last_data = data + return {"status": 201, "response": {"id": "ORDER_ID", "status": "processed"}} + + +def _make_sdk(): + http = _CapturingHttpClient() + sdk = mercadopago.SDK("TEST_TOKEN", http_client=http) + return sdk, http + + +class TestOrderItemRequest(unittest.TestCase): + def test_snake_case_keys(self): + item = OrderItemRequest( + title="A book", + type="physical", + warranty=True, + event_date="2026-07-01", + unit_price="100.00", + external_code="EXT-1", + category_id="books", + description="A nice book", + picture_url="https://example.com/p.png", + quantity=2, + ) + as_dict = order_request_to_dict(item) + self.assertEqual( + set(as_dict.keys()), + { + "title", "type", "warranty", "event_date", "unit_price", + "external_code", "category_id", "description", "picture_url", + "quantity", + }, + ) + self.assertEqual(as_dict["unit_price"], "100.00") + self.assertEqual(as_dict["quantity"], 2) + + def test_none_fields_filtered(self): + item = OrderItemRequest(title="Only title", quantity=1) + as_dict = order_request_to_dict(item) + self.assertEqual(as_dict, {"title": "Only title", "quantity": 1}) + + +class TestOrderShipmentRequest(unittest.TestCase): + def test_full_shipment_snake_case(self): + shipment = OrderShipmentRequest( + mode="me2", + local_pickup=False, + cost="10.00", + free_shipping=True, + free_methods=[OrderShipmentFreeMethod(id=1), OrderShipmentFreeMethod(id=2)], + address=OrderShipmentAddress( + street_name="Main", + street_number="123", + zip_code="0000", + floor="2", + apartment="B", + neighborhood="Centro", + state="SP", + city="Sao Paulo", + complement="near park", + ), + ) + as_dict = order_request_to_dict(shipment) + self.assertEqual(as_dict["free_methods"], [{"id": 1}, {"id": 2}]) + self.assertEqual(as_dict["address"]["street_name"], "Main") + self.assertIn("free_shipping", as_dict) + self.assertIn("local_pickup", as_dict) + + def test_partial_shipment_filters_none(self): + shipment = OrderShipmentRequest(mode="custom", cost="5.00") + as_dict = order_request_to_dict(shipment) + self.assertEqual(as_dict, {"mode": "custom", "cost": "5.00"}) + + +class TestOrderPayer(unittest.TestCase): + def test_payer_phone_and_address(self): + payer = OrderPayerRequest( + email="buyer@example.com", + first_name="Jane", + last_name="Doe", + customer_id="CUST-1", + entity_type="individual", + identification=OrderIdentification(type="CPF", number="12345678909"), + phone=OrderPayerPhone(area_code="11", number="999999999"), + address=OrderPayerAddress( + zip_code="0000", + street_name="Main", + street_number="123", + neighborhood="Centro", + city="Sao Paulo", + state="SP", + complement="apt 1", + country="BR", + ), + ) + as_dict = order_request_to_dict(payer) + self.assertEqual(as_dict["phone"], {"area_code": "11", "number": "999999999"}) + self.assertEqual(as_dict["address"]["zip_code"], "0000") + self.assertEqual(as_dict["identification"], {"type": "CPF", "number": "12345678909"}) + + def test_payer_email_only_filters_none(self): + payer = OrderPayerRequest(email="buyer@example.com") + self.assertEqual(order_request_to_dict(payer), {"email": "buyer@example.com"}) + + +class TestOrderTransactionSecurity(unittest.TestCase): + def test_snake_case_keys(self): + sec = OrderTransactionSecurity(validation="complete", liability_shift="yes") + self.assertEqual( + order_request_to_dict(sec), + {"validation": "complete", "liability_shift": "yes"}, + ) + + +class TestOrderCreateRequestRootFields(unittest.TestCase): + def test_all_root_fields_present(self): + req = OrderCreateRequest( + type="online", + external_reference="ext_ref_1234", + total_amount="200.00", + currency="BRL", + capture_mode="automatic_async", + processing_mode="automatic", + description="An order", + marketplace="NONE", + marketplace_fee="1.00", + expiration_time="P3D", + checkout_available_at="2026-07-22T00:00:00.000-03:00", + ) + as_dict = order_request_to_dict(req) + for key in ( + "description", "marketplace", "marketplace_fee", + "expiration_time", "checkout_available_at", "currency", + ): + self.assertIn(key, as_dict) + self.assertEqual(as_dict["marketplace_fee"], "1.00") + + def test_none_root_fields_filtered(self): + req = OrderCreateRequest(type="online", total_amount="10.00") + as_dict = order_request_to_dict(req) + self.assertEqual(as_dict, {"type": "online", "total_amount": "10.00"}) + self.assertNotIn("marketplace", as_dict) + + def test_config_online_transaction_security_nesting(self): + # transaction_security lives under config.online (not root). + req = OrderCreateRequest( + type="online", + config={ + "online": { + "transaction_security": order_request_to_dict( + OrderTransactionSecurity(validation="complete") + ) + } + }, + ) + as_dict = order_request_to_dict(req) + self.assertEqual( + as_dict["config"]["online"]["transaction_security"], + {"validation": "complete"}, + ) + self.assertNotIn("transaction_security", as_dict) + + def test_helper_rejects_non_dataclass(self): + with self.assertRaises(TypeError): + order_request_to_dict({"type": "online"}) + + +class TestOrderCreateDualPath(unittest.TestCase): + def test_dict_path_backward_compat(self): + sdk, http = _make_sdk() + order_object = { + "type": "online", + "total_amount": "200.00", + "external_reference": "ext_ref_1234", + "payer": {"email": "buyer@example.com"}, + } + result = sdk.order().create(order_object) + self.assertEqual(result["status"], 201) + sent = json.loads(http.last_data) + self.assertEqual(sent, order_object) + + def test_dataclass_path_matches_dict_path(self): + sdk, http = _make_sdk() + typed = OrderCreateRequest( + type="online", + total_amount="200.00", + external_reference="ext_ref_1234", + payer=OrderPayerRequest(email="buyer@example.com"), + items=[OrderItemRequest(title="A book", unit_price="200.00", quantity=1)], + ) + sdk.order().create(typed) + sent_typed = json.loads(http.last_data) + + equivalent_dict = { + "type": "online", + "total_amount": "200.00", + "external_reference": "ext_ref_1234", + "payer": {"email": "buyer@example.com"}, + "items": [{"title": "A book", "unit_price": "200.00", "quantity": 1}], + } + sdk2, http2 = _make_sdk() + sdk2.order().create(equivalent_dict) + sent_dict = json.loads(http2.last_data) + + self.assertEqual(sent_typed, sent_dict) + + def test_invalid_type_raises(self): + sdk, _ = _make_sdk() + with self.assertRaises(ValueError): + sdk.order().create("not-a-dict") + + +class TestAutomaticPaymentsTypedFlow(unittest.TestCase): + def test_ap_flow_snake_case(self): + sdk, http = _make_sdk() + order_object = { + "type": "online", + "total_amount": "100.00", + "external_reference": "subscription-001-payment-2", + "payer": {"email": "customer@example.com", "customer_id": "CUSTOMER_ID"}, + "transactions": { + "payments": [ + { + "amount": "100.00", + "payment_method": { + "id": "master", + "type": "credit_card", + "token": "CARD_TOKEN", + "installments": 1, + }, + "automatic_payments": dataclasses.asdict( + OrderAutomaticPayments( + payment_profile_id="PROFILE", + schedule_date="2026-08-01T00:00:00.000-04:00", + due_date="2026-08-05T00:00:00.000-04:00", + retries=3, + ) + ), + "stored_credential": dataclasses.asdict( + OrderStoredCredential( + payment_initiator="merchant", + reason="recurring", + store_payment_method=False, + first_payment=False, + prev_transaction_ref="PREV_TX", + ) + ), + "subscription_data": dataclasses.asdict( + OrderSubscriptionData( + invoice_id="INVOICE_002", + billing_date="2026-07-01", + subscription_sequence=OrderSubscriptionSequence(number=2, total=12), + invoice_period=OrderInvoicePeriod(type="monthly", period=1), + ) + ), + } + ] + }, + "integration_data": dataclasses.asdict( + OrderIntegrationData( + integrator_id="INT-1", + platform_id="PLAT-1", + corporation_id="CORP-1", + sponsor=OrderSponsor(id="SPONSOR-1"), + ) + ), + } + result = sdk.order().create(order_object) + self.assertEqual(result["status"], 201) + sent = json.loads(http.last_data) + payment = sent["transactions"]["payments"][0] + self.assertEqual(payment["stored_credential"]["prev_transaction_ref"], "PREV_TX") + self.assertEqual(payment["automatic_payments"]["payment_profile_id"], "PROFILE") + self.assertEqual( + payment["subscription_data"]["subscription_sequence"], + {"number": 2, "total": 12}, + ) + self.assertEqual(sent["integration_data"]["sponsor"], {"id": "SPONSOR-1"}) + + def test_ap_typed_via_dataclass_root(self): + # AP nested dicts inside a typed OrderCreateRequest (transactions kept as dict). + sdk, http = _make_sdk() + typed = OrderCreateRequest( + type="online", + total_amount="100.00", + payer=OrderPayerRequest(email="customer@example.com", customer_id="CUSTOMER_ID"), + transactions={ + "payments": [ + { + "amount": "100.00", + "stored_credential": dataclasses.asdict( + OrderStoredCredential( + payment_initiator="merchant", + first_payment=True, + ) + ), + } + ] + }, + ) + sdk.order().create(typed) + sent = json.loads(http.last_data) + sc = sent["transactions"]["payments"][0]["stored_credential"] + # first_payment stays; None fields (reason, prev_transaction_ref) are dropped. + self.assertEqual(sc, {"payment_initiator": "merchant", "first_payment": True}) + + +if __name__ == "__main__": + unittest.main() From fabdd50f9b96761d6b55da752f023d3ea3a0b801 Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Mon, 27 Jul 2026 10:25:45 -0500 Subject: [PATCH 02/11] fix(order): suppress pylint too-many-instance-attributes on DTO dataclasses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Order request dataclasses (OrderCreateRequest, OrderPayerRequest, OrderPayerAddress, OrderShipmentAddress, OrderItemRequest) model the Orders API contract verbatim — their attribute count is determined by the API, not by internal design. Added inline pylint disable with explanatory comment on each affected class. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- mercadopago/resources/order_create.py | 2 ++ mercadopago/resources/order_item.py | 1 + mercadopago/resources/order_payer.py | 1 + mercadopago/resources/order_shipment.py | 1 + 4 files changed, 5 insertions(+) diff --git a/mercadopago/resources/order_create.py b/mercadopago/resources/order_create.py index 5a8acf3..7cac6c3 100644 --- a/mercadopago/resources/order_create.py +++ b/mercadopago/resources/order_create.py @@ -74,6 +74,7 @@ class OrderIdentification: number: Optional[str] = None +# pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API payer contract @dataclass class OrderPayerRequest: """Payer information for an order request. @@ -100,6 +101,7 @@ class OrderPayerRequest: address: Optional[OrderPayerAddress] = None +# pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API root request contract @dataclass class OrderCreateRequest: """Root request body for creating an order. diff --git a/mercadopago/resources/order_item.py b/mercadopago/resources/order_item.py index 9d6eb14..eb19910 100644 --- a/mercadopago/resources/order_item.py +++ b/mercadopago/resources/order_item.py @@ -3,6 +3,7 @@ from typing import Optional +# pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API items contract @dataclass class OrderItemRequest: """A single line item within an order request. diff --git a/mercadopago/resources/order_payer.py b/mercadopago/resources/order_payer.py index a25573b..3a19f45 100644 --- a/mercadopago/resources/order_payer.py +++ b/mercadopago/resources/order_payer.py @@ -19,6 +19,7 @@ class OrderPayerPhone: number: Optional[str] = None +# pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API payer.address contract @dataclass class OrderPayerAddress: """Payer address for an order request. diff --git a/mercadopago/resources/order_shipment.py b/mercadopago/resources/order_shipment.py index d50c63c..179fb46 100644 --- a/mercadopago/resources/order_shipment.py +++ b/mercadopago/resources/order_shipment.py @@ -9,6 +9,7 @@ ) +# pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API shipment.address contract @dataclass class OrderShipmentAddress: """Delivery address for an order shipment. From 656fd8017fbc6f91973a8555b1773c1daaade9d4 Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Mon, 27 Jul 2026 10:27:20 -0500 Subject: [PATCH 03/11] fix(order): wrap long line in CapturingHttpClient.post signature Line 52 exceeded the 100-char pylint limit enforced by tests/.pylintrc. Wrapped the parameter list across two lines; no logic change. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- tests/test_order_request_dataclasses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_order_request_dataclasses.py b/tests/test_order_request_dataclasses.py index f651ae8..9fd99af 100644 --- a/tests/test_order_request_dataclasses.py +++ b/tests/test_order_request_dataclasses.py @@ -49,7 +49,8 @@ def __init__(self): self.last_url = None self.last_data = None - def post(self, url, headers, data=None, params=None, timeout=None, maxretries=None): # noqa: D401 + def post(self, url, headers, # noqa: D401 + data=None, params=None, timeout=None, maxretries=None): self.last_url = url self.last_data = data return {"status": 201, "response": {"id": "ORDER_ID", "status": "processed"}} From bf5ef7286d2141599fb6d068b3a253ec1f7b91f0 Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Mon, 27 Jul 2026 15:25:51 -0500 Subject: [PATCH 04/11] feat(order): add OrderTransactionRequest/OrderPaymentRequest to complete typed AP chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces OrderPaymentMethodRequest, OrderPaymentRequest, and OrderTransactionRequest dataclasses so the full typed chain OrderCreateRequest → OrderTransactionRequest → OrderPaymentRequest → automatic_payments / stored_credential / subscription_data is available without raw dicts. OrderCreateRequest.transactions now accepts Union[OrderTransactionRequest, dict] — existing dict-based callers are unaffected. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- mercadopago/resources/__init__.py | 8 ++ mercadopago/resources/order_create.py | 9 +- mercadopago/resources/order_transaction.py | 78 +++++++++++ tests/test_order_request_dataclasses.py | 143 +++++++++++++++++++++ 4 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 mercadopago/resources/order_transaction.py diff --git a/mercadopago/resources/__init__.py b/mercadopago/resources/__init__.py index 6d025ff..4c90375 100644 --- a/mercadopago/resources/__init__.py +++ b/mercadopago/resources/__init__.py @@ -53,6 +53,11 @@ OrderSubscriptionData, OrderSubscriptionSequence, ) +from mercadopago.resources.order_transaction import ( + OrderPaymentMethodRequest, + OrderPaymentRequest, + OrderTransactionRequest, +) from mercadopago.resources.order_transaction_security import OrderTransactionSecurity from mercadopago.resources.payment import Payment from mercadopago.resources.payment_methods import PaymentMethods @@ -101,6 +106,9 @@ 'OrderStoredCredential', 'OrderSubscriptionData', 'OrderSubscriptionSequence', + 'OrderPaymentMethodRequest', + 'OrderPaymentRequest', + 'OrderTransactionRequest', 'OrderTransactionSecurity', 'Payment', 'PaymentMethods', diff --git a/mercadopago/resources/order_create.py b/mercadopago/resources/order_create.py index 7cac6c3..3b5ac02 100644 --- a/mercadopago/resources/order_create.py +++ b/mercadopago/resources/order_create.py @@ -18,6 +18,7 @@ from typing import ( List, Optional, + Union, ) from mercadopago.resources.order_item import OrderItemRequest @@ -27,6 +28,7 @@ OrderPayerPhone, ) from mercadopago.resources.order_shipment import OrderShipmentRequest +from mercadopago.resources.order_transaction import OrderTransactionRequest def _filter_none(value): @@ -121,7 +123,10 @@ class OrderCreateRequest: marketplace_fee: Marketplace fee as a decimal string. Type: str. expiration_time: Order expiration time (ISO 8601 / duration). Type: str. checkout_available_at: When the checkout becomes available. Type: str. - transactions: Transactions payload (payments). + transactions: Typed transactions payload. Accepts an + :class:`~mercadopago.resources.order_transaction.OrderTransactionRequest` + for a fully typed AP chain, or a plain ``dict`` for backward + compatibility. payer: Payer information. items: Line items in the order. config: Order configuration payload. @@ -141,7 +146,7 @@ class OrderCreateRequest: marketplace_fee: Optional[str] = None expiration_time: Optional[str] = None checkout_available_at: Optional[str] = None - transactions: Optional[dict] = None + transactions: Optional[Union[OrderTransactionRequest, dict]] = None payer: Optional[OrderPayerRequest] = None items: Optional[List[OrderItemRequest]] = field(default=None) config: Optional[dict] = None diff --git a/mercadopago/resources/order_transaction.py b/mercadopago/resources/order_transaction.py new file mode 100644 index 0000000..dc14427 --- /dev/null +++ b/mercadopago/resources/order_transaction.py @@ -0,0 +1,78 @@ +"""Dataclasses for the transactions payload in Orders API requests. + +These dataclasses model the ``transactions.payments[]`` structure of the +``POST /v1/orders`` request body. They complete the typed chain started by +:class:`~mercadopago.resources.order_create.OrderCreateRequest`, allowing +Automatic Payments fields to be built without raw dicts. + +The plain dict path continues to work unchanged; these dataclasses are +purely additive. +""" +from dataclasses import dataclass +from typing import List, Optional + +from mercadopago.resources.order_automatic_payments import OrderAutomaticPayments +from mercadopago.resources.order_stored_credential import OrderStoredCredential +from mercadopago.resources.order_subscription_data import OrderSubscriptionData + + +@dataclass +class OrderPaymentMethodRequest: + """Payment method details for a transaction within an order. + + Attributes: + id: Payment method identifier (e.g. ``"master"``). Type: str. + type: Payment method type (e.g. ``"credit_card"``). Type: str. + token: Tokenized card identifier. Type: str. + installments: Number of installments. Type: int. + statement_descriptor: Descriptor shown on the cardholder statement. + Type: str. + financial_institution: Financial institution code (e.g. PSE). Type: str. + """ + + id: Optional[str] = None + type: Optional[str] = None + token: Optional[str] = None + installments: Optional[int] = None + statement_descriptor: Optional[str] = None + financial_institution: Optional[str] = None + + +@dataclass +class OrderPaymentRequest: + """A single payment transaction within an order. + + Use this dataclass to build an entry of the ``transactions.payments`` + array. It provides a fully typed path to all Automatic Payments fields + (``automatic_payments``, ``stored_credential``, ``subscription_data``). + + Attributes: + amount: Payment amount as a decimal string. Type: str. + expiration_time: ISO 8601 duration or date-time for expiration. + Type: str. + date_of_expiration: ISO 8601 date-time after which the payment + can no longer be collected. Type: str. + payment_method: Payment method details. + automatic_payments: Automatic (recurring) payment configuration. + stored_credential: Card-on-file metadata for recurring charges. + subscription_data: Subscription billing data for this payment. + """ + + amount: Optional[str] = None + expiration_time: Optional[str] = None + date_of_expiration: Optional[str] = None + payment_method: Optional[OrderPaymentMethodRequest] = None + automatic_payments: Optional[OrderAutomaticPayments] = None + stored_credential: Optional[OrderStoredCredential] = None + subscription_data: Optional[OrderSubscriptionData] = None + + +@dataclass +class OrderTransactionRequest: + """Transactions payload for an order creation request. + + Attributes: + payments: List of payment transactions for the order. + """ + + payments: Optional[List[OrderPaymentRequest]] = None diff --git a/tests/test_order_request_dataclasses.py b/tests/test_order_request_dataclasses.py index 9fd99af..2ce831e 100644 --- a/tests/test_order_request_dataclasses.py +++ b/tests/test_order_request_dataclasses.py @@ -39,6 +39,11 @@ OrderSubscriptionData, OrderSubscriptionSequence, ) +from mercadopago.resources.order_transaction import ( + OrderPaymentMethodRequest, + OrderPaymentRequest, + OrderTransactionRequest, +) from mercadopago.resources.order_transaction_security import OrderTransactionSecurity @@ -359,5 +364,143 @@ def test_ap_typed_via_dataclass_root(self): self.assertEqual(sc, {"payment_initiator": "merchant", "first_payment": True}) +class TestFullyTypedAPChain(unittest.TestCase): + """Verify the typed chain OrderCreateRequest → OrderTransactionRequest → + OrderPaymentRequest → AP dataclasses produces the correct JSON body.""" + + def test_fully_typed_ap_chain_serializes_correctly(self): + sdk, http = _make_sdk() + typed = OrderCreateRequest( + type="online", + total_amount="100.00", + external_reference="ap-typed-chain-001", + payer=OrderPayerRequest( + email="customer@example.com", + customer_id="CUSTOMER_ID", + ), + transactions=OrderTransactionRequest( + payments=[ + OrderPaymentRequest( + amount="100.00", + payment_method=OrderPaymentMethodRequest( + id="master", + type="credit_card", + token="CARD_TOKEN", + installments=1, + ), + automatic_payments=OrderAutomaticPayments( + payment_profile_id="PROFILE_ID", + retries=3, + schedule_date="2026-08-01T00:00:00.000-04:00", + due_date="2026-08-05T00:00:00.000-04:00", + ), + stored_credential=OrderStoredCredential( + payment_initiator="merchant", + reason="recurring", + store_payment_method=False, + first_payment=False, + prev_transaction_ref="PREV_TX_ID", + ), + subscription_data=OrderSubscriptionData( + invoice_id="INV-002", + billing_date="2026-07-27", + subscription_sequence=OrderSubscriptionSequence( + number=2, total=12 + ), + invoice_period=OrderInvoicePeriod( + type="monthly", period=1 + ), + ), + ) + ] + ), + integration_data=OrderIntegrationData( + integrator_id="INTEGRATOR_ID", + sponsor=OrderSponsor(id="SPONSOR_ID"), + ), + ) + result = sdk.order().create(typed) + self.assertEqual(result["status"], 201) + + sent = json.loads(http.last_data) + payment = sent["transactions"]["payments"][0] + + # payment_method + self.assertEqual(payment["payment_method"]["id"], "master") + self.assertEqual(payment["payment_method"]["token"], "CARD_TOKEN") + + # automatic_payments + ap = payment["automatic_payments"] + self.assertEqual(ap["payment_profile_id"], "PROFILE_ID") + self.assertEqual(ap["retries"], 3) + self.assertEqual(ap["schedule_date"], "2026-08-01T00:00:00.000-04:00") + self.assertEqual(ap["due_date"], "2026-08-05T00:00:00.000-04:00") + + # stored_credential + sc = payment["stored_credential"] + self.assertEqual(sc["payment_initiator"], "merchant") + self.assertEqual(sc["reason"], "recurring") + self.assertFalse(sc["store_payment_method"]) + self.assertFalse(sc["first_payment"]) + self.assertEqual(sc["prev_transaction_ref"], "PREV_TX_ID") + + # subscription_data + sub = payment["subscription_data"] + self.assertEqual(sub["invoice_id"], "INV-002") + self.assertEqual(sub["billing_date"], "2026-07-27") + self.assertEqual(sub["subscription_sequence"], {"number": 2, "total": 12}) + self.assertEqual(sub["invoice_period"], {"type": "monthly", "period": 1}) + + # integration_data + integ = sent["integration_data"] + self.assertEqual(integ["integrator_id"], "INTEGRATOR_ID") + self.assertEqual(integ["sponsor"], {"id": "SPONSOR_ID"}) + + def test_typed_transactions_and_dict_transactions_produce_same_json(self): + """Typed OrderTransactionRequest and equivalent dict produce identical JSON.""" + sdk_typed, http_typed = _make_sdk() + sdk_dict, http_dict = _make_sdk() + + typed_request = OrderCreateRequest( + type="online", + total_amount="50.00", + transactions=OrderTransactionRequest( + payments=[ + OrderPaymentRequest( + amount="50.00", + payment_method=OrderPaymentMethodRequest( + id="visa", type="credit_card", installments=1 + ), + automatic_payments=OrderAutomaticPayments( + payment_profile_id="PROF-1", + ), + ) + ] + ), + ) + + dict_request = { + "type": "online", + "total_amount": "50.00", + "transactions": { + "payments": [{ + "amount": "50.00", + "payment_method": { + "id": "visa", "type": "credit_card", "installments": 1 + }, + "automatic_payments": {"payment_profile_id": "PROF-1"}, + }] + }, + } + + sdk_typed.order().create(typed_request) + sdk_dict.order().create(dict_request) + + self.assertEqual( + json.loads(http_typed.last_data), + json.loads(http_dict.last_data), + ) + + if __name__ == "__main__": unittest.main() From ef33b381b523379fcf6bf8e05ce9d49ba42ce37a Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Wed, 29 Jul 2026 14:10:17 -0500 Subject: [PATCH 05/11] fix(order): rename prev_transaction_ref to previous_transaction_reference in stored_credential --- examples/order/create_order_automatic_payment.py | 2 +- mercadopago/resources/order_stored_credential.py | 8 ++++---- tests/test_order_request_dataclasses.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/order/create_order_automatic_payment.py b/examples/order/create_order_automatic_payment.py index 172ccad..9a0c70e 100644 --- a/examples/order/create_order_automatic_payment.py +++ b/examples/order/create_order_automatic_payment.py @@ -116,7 +116,7 @@ reason="recurring", store_payment_method=False, first_payment=False, - prev_transaction_ref=first_transaction_id, # required + previous_transaction_reference=first_transaction_id, # required ) ), "subscription_data": dataclasses.asdict( diff --git a/mercadopago/resources/order_stored_credential.py b/mercadopago/resources/order_stored_credential.py index e3d296e..0d4a77f 100644 --- a/mercadopago/resources/order_stored_credential.py +++ b/mercadopago/resources/order_stored_credential.py @@ -19,13 +19,13 @@ class OrderStoredCredential: Type: bool. first_payment: ``True`` for the initial authorization; ``False`` for subsequent recurring charges. Type: bool. - prev_transaction_ref: Identifier of the previous transaction in the recurring - series. Required from the second charge onwards to link this payment to the - original card-network authorization. Type: str. + previous_transaction_reference: Identifier of the previous transaction in the + recurring series. Required from the second charge onwards to link this payment + to the original card-network authorization. Type: str. """ payment_initiator: Optional[str] = None reason: Optional[str] = None store_payment_method: Optional[bool] = None first_payment: Optional[bool] = None - prev_transaction_ref: Optional[str] = None + previous_transaction_reference: Optional[str] = None diff --git a/tests/test_order_request_dataclasses.py b/tests/test_order_request_dataclasses.py index 2ce831e..04b219a 100644 --- a/tests/test_order_request_dataclasses.py +++ b/tests/test_order_request_dataclasses.py @@ -301,7 +301,7 @@ def test_ap_flow_snake_case(self): reason="recurring", store_payment_method=False, first_payment=False, - prev_transaction_ref="PREV_TX", + previous_transaction_reference="PREV_TX", ) ), "subscription_data": dataclasses.asdict( @@ -328,7 +328,7 @@ def test_ap_flow_snake_case(self): self.assertEqual(result["status"], 201) sent = json.loads(http.last_data) payment = sent["transactions"]["payments"][0] - self.assertEqual(payment["stored_credential"]["prev_transaction_ref"], "PREV_TX") + self.assertEqual(payment["stored_credential"]["previous_transaction_reference"], "PREV_TX") self.assertEqual(payment["automatic_payments"]["payment_profile_id"], "PROFILE") self.assertEqual( payment["subscription_data"]["subscription_sequence"], @@ -399,7 +399,7 @@ def test_fully_typed_ap_chain_serializes_correctly(self): reason="recurring", store_payment_method=False, first_payment=False, - prev_transaction_ref="PREV_TX_ID", + previous_transaction_reference="PREV_TX_ID", ), subscription_data=OrderSubscriptionData( invoice_id="INV-002", @@ -442,7 +442,7 @@ def test_fully_typed_ap_chain_serializes_correctly(self): self.assertEqual(sc["reason"], "recurring") self.assertFalse(sc["store_payment_method"]) self.assertFalse(sc["first_payment"]) - self.assertEqual(sc["prev_transaction_ref"], "PREV_TX_ID") + self.assertEqual(sc["previous_transaction_reference"], "PREV_TX_ID") # subscription_data sub = payment["subscription_data"] From 8e31325ec771f0a3341f98bb6aa9da6b65fd5510 Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Mon, 10 Aug 2026 16:05:38 -0500 Subject: [PATCH 06/11] fix(test): align _CapturingHttpClient.post signature with HttpClient master added retry_on and backoff_factor params to HttpClient.post() (ergonomics retry feature). The test stub was missing them, causing TypeError on all order create tests after the merge. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- tests/test_order_request_dataclasses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_order_request_dataclasses.py b/tests/test_order_request_dataclasses.py index 04b219a..69f3315 100644 --- a/tests/test_order_request_dataclasses.py +++ b/tests/test_order_request_dataclasses.py @@ -55,7 +55,8 @@ def __init__(self): self.last_data = None def post(self, url, headers, # noqa: D401 - data=None, params=None, timeout=None, maxretries=None): + data=None, params=None, timeout=None, maxretries=None, + retry_on=None, backoff_factor=None): self.last_url = url self.last_data = data return {"status": 201, "response": {"id": "ORDER_ID", "status": "processed"}} From 9bb3ad4c9f44a09b5484160bc39d960e4b698279 Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Mon, 10 Aug 2026 16:09:39 -0500 Subject: [PATCH 07/11] fix(lint): suppress too-many-arguments on _CapturingHttpClient.post Matches the same disable already present on HttpClient.post in http_client.py after adding retry_on and backoff_factor params. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- tests/test_order_request_dataclasses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_order_request_dataclasses.py b/tests/test_order_request_dataclasses.py index 69f3315..c45f991 100644 --- a/tests/test_order_request_dataclasses.py +++ b/tests/test_order_request_dataclasses.py @@ -54,7 +54,7 @@ def __init__(self): self.last_url = None self.last_data = None - def post(self, url, headers, # noqa: D401 + def post(self, url, headers, # noqa: D401 # pylint: disable=too-many-arguments,too-many-positional-arguments data=None, params=None, timeout=None, maxretries=None, retry_on=None, backoff_factor=None): self.last_url = url From 18eee8231972d102b72514462b471795887b3b4a Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Mon, 10 Aug 2026 16:48:11 -0500 Subject: [PATCH 08/11] fix(readme): restore request_options in payment create example When the Order API section was added, the Payment example was split into its own code block losing the request_options that was part of the original single example in master. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d3fc30..18389dc 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,11 @@ import mercadopago sdk = mercadopago.SDK("YOUR_ACCESS_TOKEN") +request_options = mercadopago.config.RequestOptions() +request_options.custom_headers = { + 'x-idempotency-key': '' +} + payment_data = { "transaction_amount": 100, "token": "CARD_TOKEN", @@ -106,7 +111,7 @@ payment_data = { "email": 'test_user_123456@testuser.com' } } -result = sdk.payment().create(payment_data) +result = sdk.payment().create(payment_data, request_options) payment = result["response"] print(payment) From 3f7b023229274b6d3727ed20c6f65cf3760c0bcf Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Mon, 10 Aug 2026 17:02:06 -0500 Subject: [PATCH 09/11] refactor(payer): consolidate payer classes into generic payer.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace order_payer.py with payer.py and move OrderPayerRequest and OrderIdentification out of order_create.py into the same module. Remove the "Order" prefix so the classes are reusable across APIs: OrderPayerPhone → PayerPhone OrderPayerAddress → PayerAddress OrderIdentification → PayerIdentification OrderPayerRequest → PayerRequest (moved from order_create.py) OrderCreateRequest.payer now types PayerRequest. Public __init__.py exports updated accordingly. No behaviour change — 79 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- mercadopago/resources/__init__.py | 18 +++--- mercadopago/resources/order_create.py | 52 +++------------- mercadopago/resources/order_payer.py | 48 --------------- mercadopago/resources/payer.py | 82 +++++++++++++++++++++++++ tests/test_order_request_dataclasses.py | 26 ++++---- 5 files changed, 111 insertions(+), 115 deletions(-) delete mode 100644 mercadopago/resources/order_payer.py create mode 100644 mercadopago/resources/payer.py diff --git a/mercadopago/resources/__init__.py b/mercadopago/resources/__init__.py index 4c90375..809e51f 100644 --- a/mercadopago/resources/__init__.py +++ b/mercadopago/resources/__init__.py @@ -29,8 +29,6 @@ ) from mercadopago.resources.order_create import ( OrderCreateRequest, - OrderIdentification, - OrderPayerRequest, order_request_to_dict, ) from mercadopago.resources.order_integration_data import ( @@ -38,9 +36,11 @@ OrderSponsor, ) from mercadopago.resources.order_item import OrderItemRequest -from mercadopago.resources.order_payer import ( - OrderPayerAddress, - OrderPayerPhone, +from mercadopago.resources.payer import ( + PayerAddress, + PayerIdentification, + PayerPhone, + PayerRequest, ) from mercadopago.resources.order_shipment import ( OrderShipmentAddress, @@ -92,13 +92,13 @@ 'OrderCheckoutProTrack', 'OrderCheckoutProDict', 'OrderCreateRequest', - 'OrderIdentification', 'OrderIntegrationData', 'OrderInvoicePeriod', 'OrderItemRequest', - 'OrderPayerAddress', - 'OrderPayerPhone', - 'OrderPayerRequest', + 'PayerAddress', + 'PayerIdentification', + 'PayerPhone', + 'PayerRequest', 'OrderShipmentAddress', 'OrderShipmentFreeMethod', 'OrderShipmentRequest', diff --git a/mercadopago/resources/order_create.py b/mercadopago/resources/order_create.py index 3b5ac02..f5734da 100644 --- a/mercadopago/resources/order_create.py +++ b/mercadopago/resources/order_create.py @@ -23,12 +23,14 @@ from mercadopago.resources.order_item import OrderItemRequest from mercadopago.resources.order_integration_data import OrderIntegrationData -from mercadopago.resources.order_payer import ( - OrderPayerAddress, - OrderPayerPhone, -) from mercadopago.resources.order_shipment import OrderShipmentRequest from mercadopago.resources.order_transaction import OrderTransactionRequest +from mercadopago.resources.payer import ( + PayerAddress, + PayerIdentification, + PayerPhone, + PayerRequest, +) def _filter_none(value): @@ -63,46 +65,6 @@ def order_request_to_dict(request): return _filter_none(asdict(request)) -@dataclass -class OrderIdentification: - """Payer identification document for an order request. - - Attributes: - type: Identification document type (e.g. ``"CPF"``). Type: str. - number: Identification document number. Type: str. - """ - - type: Optional[str] = None - number: Optional[str] = None - - -# pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API payer contract -@dataclass -class OrderPayerRequest: - """Payer information for an order request. - - Attributes: - email: Payer email address. Type: str. - first_name: Payer first name. Type: str. - last_name: Payer last name. Type: str. - customer_id: Stored customer identifier. Type: str. - entity_type: Payer entity type (``"individual"`` | ``"association"``). - Type: str. - identification: Payer identification document. - phone: Payer phone number. - address: Payer address. - """ - - email: Optional[str] = None - first_name: Optional[str] = None - last_name: Optional[str] = None - customer_id: Optional[str] = None - entity_type: Optional[str] = None - identification: Optional[OrderIdentification] = None - phone: Optional[OrderPayerPhone] = None - address: Optional[OrderPayerAddress] = None - - # pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API root request contract @dataclass class OrderCreateRequest: @@ -147,7 +109,7 @@ class OrderCreateRequest: expiration_time: Optional[str] = None checkout_available_at: Optional[str] = None transactions: Optional[Union[OrderTransactionRequest, dict]] = None - payer: Optional[OrderPayerRequest] = None + payer: Optional[PayerRequest] = None items: Optional[List[OrderItemRequest]] = field(default=None) config: Optional[dict] = None shipment: Optional[OrderShipmentRequest] = None diff --git a/mercadopago/resources/order_payer.py b/mercadopago/resources/order_payer.py deleted file mode 100644 index 3a19f45..0000000 --- a/mercadopago/resources/order_payer.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Dataclasses for payer contact data in order requests.""" -from dataclasses import dataclass -from typing import Optional - - -@dataclass -class OrderPayerPhone: - """Payer phone number for an order request. - - Use this dataclass to build the ``payer.phone`` payload. Convert to dict with - ``dataclasses.asdict()`` (``None`` fields are filtered out before sending). - - Attributes: - area_code: Phone area code. Type: str. - number: Phone number without the area code. Type: str. - """ - - area_code: Optional[str] = None - number: Optional[str] = None - - -# pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API payer.address contract -@dataclass -class OrderPayerAddress: - """Payer address for an order request. - - Use this dataclass to build the ``payer.address`` payload. Convert to dict with - ``dataclasses.asdict()`` (``None`` fields are filtered out before sending). - - Attributes: - zip_code: Postal / ZIP code. Type: str. - street_name: Name of the street. Type: str. - street_number: Street number. Type: str. - neighborhood: Neighborhood name. Type: str. - city: City name. Type: str. - state: State or province. Type: str. - complement: Additional address details. Type: str. - country: Country name or code. Type: str. - """ - - zip_code: Optional[str] = None - street_name: Optional[str] = None - street_number: Optional[str] = None - neighborhood: Optional[str] = None - city: Optional[str] = None - state: Optional[str] = None - complement: Optional[str] = None - country: Optional[str] = None diff --git a/mercadopago/resources/payer.py b/mercadopago/resources/payer.py new file mode 100644 index 0000000..91292b5 --- /dev/null +++ b/mercadopago/resources/payer.py @@ -0,0 +1,82 @@ +"""Dataclasses for payer data in API requests.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class PayerPhone: + """Payer phone number. + + Attributes: + area_code: Phone area code. Type: str. + number: Phone number without the area code. Type: str. + """ + + area_code: Optional[str] = None + number: Optional[str] = None + + +# pylint: disable=too-many-instance-attributes # DTO: fields mirror the API payer.address contract +@dataclass +class PayerAddress: + """Payer address. + + Attributes: + zip_code: Postal / ZIP code. Type: str. + street_name: Name of the street. Type: str. + street_number: Street number. Type: str. + neighborhood: Neighborhood name. Type: str. + city: City name. Type: str. + state: State or province. Type: str. + complement: Additional address details. Type: str. + country: Country name or code. Type: str. + """ + + zip_code: Optional[str] = None + street_name: Optional[str] = None + street_number: Optional[str] = None + neighborhood: Optional[str] = None + city: Optional[str] = None + state: Optional[str] = None + complement: Optional[str] = None + country: Optional[str] = None + + +@dataclass +class PayerIdentification: + """Payer identification document. + + Attributes: + type: Identification document type (e.g. ``"CPF"``). Type: str. + number: Identification document number. Type: str. + """ + + type: Optional[str] = None + number: Optional[str] = None + + +# pylint: disable=too-many-instance-attributes # DTO: fields mirror the API payer contract +@dataclass +class PayerRequest: + """Payer information for an API request. + + Attributes: + email: Payer email address. Type: str. + first_name: Payer first name. Type: str. + last_name: Payer last name. Type: str. + customer_id: Stored customer identifier. Type: str. + entity_type: Payer entity type (``"individual"`` | ``"association"``). + Type: str. + identification: Payer identification document. + phone: Payer phone number. + address: Payer address. + """ + + email: Optional[str] = None + first_name: Optional[str] = None + last_name: Optional[str] = None + customer_id: Optional[str] = None + entity_type: Optional[str] = None + identification: Optional[PayerIdentification] = None + phone: Optional[PayerPhone] = None + address: Optional[PayerAddress] = None diff --git a/tests/test_order_request_dataclasses.py b/tests/test_order_request_dataclasses.py index c45f991..331ae67 100644 --- a/tests/test_order_request_dataclasses.py +++ b/tests/test_order_request_dataclasses.py @@ -15,8 +15,6 @@ from mercadopago.resources.order_automatic_payments import OrderAutomaticPayments from mercadopago.resources.order_create import ( OrderCreateRequest, - OrderIdentification, - OrderPayerRequest, order_request_to_dict, ) from mercadopago.resources.order_integration_data import ( @@ -24,9 +22,11 @@ OrderSponsor, ) from mercadopago.resources.order_item import OrderItemRequest -from mercadopago.resources.order_payer import ( - OrderPayerAddress, - OrderPayerPhone, +from mercadopago.resources.payer import ( + PayerAddress, + PayerIdentification, + PayerPhone, + PayerRequest, ) from mercadopago.resources.order_shipment import ( OrderShipmentAddress, @@ -134,15 +134,15 @@ def test_partial_shipment_filters_none(self): class TestOrderPayer(unittest.TestCase): def test_payer_phone_and_address(self): - payer = OrderPayerRequest( + payer = PayerRequest( email="buyer@example.com", first_name="Jane", last_name="Doe", customer_id="CUST-1", entity_type="individual", - identification=OrderIdentification(type="CPF", number="12345678909"), - phone=OrderPayerPhone(area_code="11", number="999999999"), - address=OrderPayerAddress( + identification=PayerIdentification(type="CPF", number="12345678909"), + phone=PayerPhone(area_code="11", number="999999999"), + address=PayerAddress( zip_code="0000", street_name="Main", street_number="123", @@ -159,7 +159,7 @@ def test_payer_phone_and_address(self): self.assertEqual(as_dict["identification"], {"type": "CPF", "number": "12345678909"}) def test_payer_email_only_filters_none(self): - payer = OrderPayerRequest(email="buyer@example.com") + payer = PayerRequest(email="buyer@example.com") self.assertEqual(order_request_to_dict(payer), {"email": "buyer@example.com"}) @@ -245,7 +245,7 @@ def test_dataclass_path_matches_dict_path(self): type="online", total_amount="200.00", external_reference="ext_ref_1234", - payer=OrderPayerRequest(email="buyer@example.com"), + payer=PayerRequest(email="buyer@example.com"), items=[OrderItemRequest(title="A book", unit_price="200.00", quantity=1)], ) sdk.order().create(typed) @@ -343,7 +343,7 @@ def test_ap_typed_via_dataclass_root(self): typed = OrderCreateRequest( type="online", total_amount="100.00", - payer=OrderPayerRequest(email="customer@example.com", customer_id="CUSTOMER_ID"), + payer=PayerRequest(email="customer@example.com", customer_id="CUSTOMER_ID"), transactions={ "payments": [ { @@ -375,7 +375,7 @@ def test_fully_typed_ap_chain_serializes_correctly(self): type="online", total_amount="100.00", external_reference="ap-typed-chain-001", - payer=OrderPayerRequest( + payer=PayerRequest( email="customer@example.com", customer_id="CUSTOMER_ID", ), From de9fe8e81d4c18f75a3f29f0897851cdfcebb8bb Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Mon, 10 Aug 2026 17:10:35 -0500 Subject: [PATCH 10/11] refactor(item,shipment): rename to generic classes without Order prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace order_item.py/order_shipment.py with item.py/shipment.py and remove the Order prefix from class names to make them reusable across APIs (items and shipments appear in Preferences API with similar schema): OrderItemRequest → ItemRequest (item.py) OrderShipmentRequest → ShipmentRequest (shipment.py) OrderShipmentAddress → ShipmentAddress OrderShipmentFreeMethod → ShipmentFreeMethod OrderCreateRequest.items and .shipment updated accordingly. order_transaction.py keeps the Order prefix — PaymentRequest would clash with the existing Payment resource and the structure is Orders-API-only. 79 unit tests pass. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- mercadopago/resources/__init__.py | 18 ++++++------ .../resources/{order_item.py => item.py} | 8 +++--- mercadopago/resources/order_create.py | 8 +++--- .../{order_shipment.py => shipment.py} | 22 +++++++-------- tests/test_order_request_dataclasses.py | 28 +++++++++---------- 5 files changed, 42 insertions(+), 42 deletions(-) rename mercadopago/resources/{order_item.py => item.py} (90%) rename mercadopago/resources/{order_shipment.py => shipment.py} (77%) diff --git a/mercadopago/resources/__init__.py b/mercadopago/resources/__init__.py index 809e51f..068443e 100644 --- a/mercadopago/resources/__init__.py +++ b/mercadopago/resources/__init__.py @@ -35,17 +35,17 @@ OrderIntegrationData, OrderSponsor, ) -from mercadopago.resources.order_item import OrderItemRequest +from mercadopago.resources.item import ItemRequest from mercadopago.resources.payer import ( PayerAddress, PayerIdentification, PayerPhone, PayerRequest, ) -from mercadopago.resources.order_shipment import ( - OrderShipmentAddress, - OrderShipmentFreeMethod, - OrderShipmentRequest, +from mercadopago.resources.shipment import ( + ShipmentAddress, + ShipmentFreeMethod, + ShipmentRequest, ) from mercadopago.resources.order_stored_credential import OrderStoredCredential from mercadopago.resources.order_subscription_data import ( @@ -94,14 +94,14 @@ 'OrderCreateRequest', 'OrderIntegrationData', 'OrderInvoicePeriod', - 'OrderItemRequest', + 'ItemRequest', 'PayerAddress', 'PayerIdentification', 'PayerPhone', 'PayerRequest', - 'OrderShipmentAddress', - 'OrderShipmentFreeMethod', - 'OrderShipmentRequest', + 'ShipmentAddress', + 'ShipmentFreeMethod', + 'ShipmentRequest', 'OrderSponsor', 'OrderStoredCredential', 'OrderSubscriptionData', diff --git a/mercadopago/resources/order_item.py b/mercadopago/resources/item.py similarity index 90% rename from mercadopago/resources/order_item.py rename to mercadopago/resources/item.py index eb19910..6fb8c96 100644 --- a/mercadopago/resources/order_item.py +++ b/mercadopago/resources/item.py @@ -1,12 +1,12 @@ -"""Dataclass for line items in order requests.""" +"""Dataclass for line items in API requests.""" from dataclasses import dataclass from typing import Optional -# pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API items contract +# pylint: disable=too-many-instance-attributes # DTO: fields mirror the API items contract @dataclass -class OrderItemRequest: - """A single line item within an order request. +class ItemRequest: + """A single line item within an API request. Use this dataclass to build an entry of the ``items`` array when creating an order. Convert to dict with ``dataclasses.asdict()`` (``None`` fields are diff --git a/mercadopago/resources/order_create.py b/mercadopago/resources/order_create.py index f5734da..dce2db9 100644 --- a/mercadopago/resources/order_create.py +++ b/mercadopago/resources/order_create.py @@ -21,9 +21,9 @@ Union, ) -from mercadopago.resources.order_item import OrderItemRequest +from mercadopago.resources.item import ItemRequest from mercadopago.resources.order_integration_data import OrderIntegrationData -from mercadopago.resources.order_shipment import OrderShipmentRequest +from mercadopago.resources.shipment import ShipmentRequest from mercadopago.resources.order_transaction import OrderTransactionRequest from mercadopago.resources.payer import ( PayerAddress, @@ -110,8 +110,8 @@ class OrderCreateRequest: checkout_available_at: Optional[str] = None transactions: Optional[Union[OrderTransactionRequest, dict]] = None payer: Optional[PayerRequest] = None - items: Optional[List[OrderItemRequest]] = field(default=None) + items: Optional[List[ItemRequest]] = field(default=None) config: Optional[dict] = None - shipment: Optional[OrderShipmentRequest] = None + shipment: Optional[ShipmentRequest] = None integration_data: Optional[OrderIntegrationData] = None additional_info: Optional[dict] = None diff --git a/mercadopago/resources/order_shipment.py b/mercadopago/resources/shipment.py similarity index 77% rename from mercadopago/resources/order_shipment.py rename to mercadopago/resources/shipment.py index 179fb46..723eef9 100644 --- a/mercadopago/resources/order_shipment.py +++ b/mercadopago/resources/shipment.py @@ -1,4 +1,4 @@ -"""Dataclasses for shipment data in order requests.""" +"""Dataclasses for shipment data in API requests.""" from dataclasses import ( dataclass, field, @@ -9,10 +9,10 @@ ) -# pylint: disable=too-many-instance-attributes # DTO: fields mirror the Orders API shipment.address contract +# pylint: disable=too-many-instance-attributes # DTO: fields mirror the API shipment.address contract @dataclass -class OrderShipmentAddress: - """Delivery address for an order shipment. +class ShipmentAddress: + """Delivery address for a shipment. Attributes: street_name: Name of the street. Type: str. @@ -38,8 +38,8 @@ class OrderShipmentAddress: @dataclass -class OrderShipmentFreeMethod: - """A free-shipping method offered for the order. +class ShipmentFreeMethod: + """A free-shipping method. Attributes: id: Identifier of the free shipping method. Type: int. @@ -49,8 +49,8 @@ class OrderShipmentFreeMethod: @dataclass -class OrderShipmentRequest: - """Shipment configuration for an order request. +class ShipmentRequest: + """Shipment configuration for an API request. Use this dataclass to build the ``shipment`` payload when creating an order. Convert to dict with ``dataclasses.asdict()`` (``None`` fields are filtered @@ -61,7 +61,7 @@ class OrderShipmentRequest: local_pickup: Whether the buyer picks up the item locally. Type: bool. cost: Shipping cost as a decimal string. Type: str. free_shipping: Whether shipping is free. Type: bool. - free_methods: Free shipping methods available for the order. + free_methods: Free shipping methods available. address: Delivery address for the shipment. """ @@ -69,5 +69,5 @@ class OrderShipmentRequest: local_pickup: Optional[bool] = None cost: Optional[str] = None free_shipping: Optional[bool] = None - free_methods: Optional[List[OrderShipmentFreeMethod]] = field(default=None) - address: Optional[OrderShipmentAddress] = None + free_methods: Optional[List[ShipmentFreeMethod]] = field(default=None) + address: Optional[ShipmentAddress] = None diff --git a/tests/test_order_request_dataclasses.py b/tests/test_order_request_dataclasses.py index 331ae67..8806e0c 100644 --- a/tests/test_order_request_dataclasses.py +++ b/tests/test_order_request_dataclasses.py @@ -21,17 +21,17 @@ OrderIntegrationData, OrderSponsor, ) -from mercadopago.resources.order_item import OrderItemRequest +from mercadopago.resources.item import ItemRequest from mercadopago.resources.payer import ( PayerAddress, PayerIdentification, PayerPhone, PayerRequest, ) -from mercadopago.resources.order_shipment import ( - OrderShipmentAddress, - OrderShipmentFreeMethod, - OrderShipmentRequest, +from mercadopago.resources.shipment import ( + ShipmentAddress, + ShipmentFreeMethod, + ShipmentRequest, ) from mercadopago.resources.order_stored_credential import OrderStoredCredential from mercadopago.resources.order_subscription_data import ( @@ -68,9 +68,9 @@ def _make_sdk(): return sdk, http -class TestOrderItemRequest(unittest.TestCase): +class TestItemRequest(unittest.TestCase): def test_snake_case_keys(self): - item = OrderItemRequest( + item = ItemRequest( title="A book", type="physical", warranty=True, @@ -95,20 +95,20 @@ def test_snake_case_keys(self): self.assertEqual(as_dict["quantity"], 2) def test_none_fields_filtered(self): - item = OrderItemRequest(title="Only title", quantity=1) + item = ItemRequest(title="Only title", quantity=1) as_dict = order_request_to_dict(item) self.assertEqual(as_dict, {"title": "Only title", "quantity": 1}) -class TestOrderShipmentRequest(unittest.TestCase): +class TestShipmentRequest(unittest.TestCase): def test_full_shipment_snake_case(self): - shipment = OrderShipmentRequest( + shipment = ShipmentRequest( mode="me2", local_pickup=False, cost="10.00", free_shipping=True, - free_methods=[OrderShipmentFreeMethod(id=1), OrderShipmentFreeMethod(id=2)], - address=OrderShipmentAddress( + free_methods=[ShipmentFreeMethod(id=1), ShipmentFreeMethod(id=2)], + address=ShipmentAddress( street_name="Main", street_number="123", zip_code="0000", @@ -127,7 +127,7 @@ def test_full_shipment_snake_case(self): self.assertIn("local_pickup", as_dict) def test_partial_shipment_filters_none(self): - shipment = OrderShipmentRequest(mode="custom", cost="5.00") + shipment = ShipmentRequest(mode="custom", cost="5.00") as_dict = order_request_to_dict(shipment) self.assertEqual(as_dict, {"mode": "custom", "cost": "5.00"}) @@ -246,7 +246,7 @@ def test_dataclass_path_matches_dict_path(self): total_amount="200.00", external_reference="ext_ref_1234", payer=PayerRequest(email="buyer@example.com"), - items=[OrderItemRequest(title="A book", unit_price="200.00", quantity=1)], + items=[ItemRequest(title="A book", unit_price="200.00", quantity=1)], ) sdk.order().create(typed) sent_typed = json.loads(http.last_data) From d4ff99e93041b766840eb45acd1ee6c14ec4355b Mon Sep 17 00:00:00 2001 From: Diego Gerardo Barajas Suarez Date: Mon, 10 Aug 2026 17:13:46 -0500 Subject: [PATCH 11/11] fix(lint): remove unused PayerAddress/Phone/Identification imports from order_create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After moving PayerRequest to payer.py, order_create.py only needs PayerRequest — the sub-classes are internal to payer.py. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- mercadopago/resources/order_create.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mercadopago/resources/order_create.py b/mercadopago/resources/order_create.py index dce2db9..c0122d8 100644 --- a/mercadopago/resources/order_create.py +++ b/mercadopago/resources/order_create.py @@ -25,12 +25,7 @@ from mercadopago.resources.order_integration_data import OrderIntegrationData from mercadopago.resources.shipment import ShipmentRequest from mercadopago.resources.order_transaction import OrderTransactionRequest -from mercadopago.resources.payer import ( - PayerAddress, - PayerIdentification, - PayerPhone, - PayerRequest, -) +from mercadopago.resources.payer import PayerRequest def _filter_none(value):