diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..611c4b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +This project follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.2.0] - 2026-05-27 + +### Added + +- **OAuth**: authorization URL builder, token exchange, and token refresh + (`sdk.oauth().get_authorization_url()`, `.create()`, `.refresh()`). + Enables marketplace and platform integrations to operate on behalf of + other sellers via the OAuth 2.0 authorization code flow. + Endpoints: `POST /oauth/token`. + +- **Point**: in-person payment intent management for MercadoPago Point + devices (`sdk.point().get_devices()`, `.create()`, `.get()`, `.cancel()`). + Enables card-present transactions through physical Point card readers. + Endpoints: `GET /point/integration-api/devices`, + `POST /point/integration-api/devices/{device_id}/payment-intents`, + `GET /point/integration-api/payment-intents/{id}`, + `DELETE /point/integration-api/devices/{device_id}/payment-intents/{id}`. + Note: `change_operating_mode` (PATCH) is not included; requires HttpClient + PATCH support. + +- **Invoice**: retrieval and search of subscription billing invoices + (`sdk.invoice().get()`, `.search()`). Enables monitoring of authorized + payments generated by PreApproval billing cycles. + Endpoints: `GET /authorized_payments/{id}`, + `GET /authorized_payments/search`. diff --git a/examples/order/create_order_automatic_payment.py b/examples/order/create_order_automatic_payment.py new file mode 100644 index 0000000..172ccad --- /dev/null +++ b/examples/order/create_order_automatic_payment.py @@ -0,0 +1,137 @@ +"""Example: Automatic Payments flow with the MercadoPago Orders API. + +Demonstrates both the first payment (storing the card credential) and a +subsequent recurring charge (referencing the previous transaction). +""" +import dataclasses +import mercadopago +from mercadopago.resources.order_automatic_payments import OrderAutomaticPayments +from mercadopago.resources.order_stored_credential import OrderStoredCredential +from mercadopago.resources.order_subscription_data import ( + OrderInvoicePeriod, + OrderSubscriptionData, + OrderSubscriptionSequence, +) + +sdk = mercadopago.SDK("YOUR_ACCESS_TOKEN") + + +# ── First payment ────────────────────────────────────────────────────────── +# Use first_payment=True and no prev_transaction_ref. +# The API stores the credential for future charges. + +first_payment_order = { + "type": "online", + "total_amount": "100.00", + "external_reference": "subscription-001-payment-1", + "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="PAYMENT_PROFILE_ID", + schedule_date="2026-07-01T00:00:00.000-04:00", + due_date="2026-07-05T00:00:00.000-04:00", + retries=3, + ) + ), + "stored_credential": dataclasses.asdict( + OrderStoredCredential( + payment_initiator="merchant", + reason="recurring", + store_payment_method=True, + first_payment=True, + # prev_transaction_ref not required on first payment + ) + ), + "subscription_data": dataclasses.asdict( + OrderSubscriptionData( + invoice_id="INVOICE_001", + billing_date="2026-06-01", + subscription_sequence=OrderSubscriptionSequence(number=1, total=12), + invoice_period=OrderInvoicePeriod(type="monthly", period=1), + ) + ), + } + ] + }, +} + +result = sdk.order().create(first_payment_order) +first_order = result["response"] +print("First payment order:", first_order.get("id"), "→", first_order.get("status")) + +# Save the transaction ID for use in the next charge. +first_transaction_id = ( + first_order.get("transactions", {}) + .get("payments", [{}])[0] + .get("id", "") +) + + +# ── Subsequent recurring charge ──────────────────────────────────────────── +# Use first_payment=False and include prev_transaction_ref pointing to the +# previous charge. The API links this payment to the original authorization. + +recurring_order = { + "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="PAYMENT_PROFILE_ID", + 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=first_transaction_id, # required + ) + ), + "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), + ) + ), + } + ] + }, +} + +result = sdk.order().create(recurring_order) +recurring = result["response"] +print("Recurring charge order:", recurring.get("id"), "→", recurring.get("status")) diff --git a/mercadopago/config/config.py b/mercadopago/config/config.py index 36d74c8..1c79773 100644 --- a/mercadopago/config/config.py +++ b/mercadopago/config/config.py @@ -26,7 +26,7 @@ class Config: def __init__(self): """Builds version-dependent values (user_agent, tracking_id).""" - self.__version = "3.1.1" + self.__version = "3.2.0" self.__user_agent = "MercadoPago Python SDK v" + self.__version self.__product_id = "bc32bpftrpp001u8nhlg" self.__tracking_id = "platform:" + platform.python_version() diff --git a/mercadopago/examples/invoice/get_invoice.py b/mercadopago/examples/invoice/get_invoice.py new file mode 100644 index 0000000..244dfcb --- /dev/null +++ b/mercadopago/examples/invoice/get_invoice.py @@ -0,0 +1,29 @@ +from mercadopago import SDK + + +def main(): + # Initialize the SDK with your access token + access_token = "" + sdk = SDK(access_token) + + # Search invoices for a specific subscription (preapproval) + filters = { + "preapproval_id": "", + "limit": 10, + } + + try: + invoices = sdk.invoice().search(filters=filters) + print("Invoices found:", invoices["response"]) + + # Retrieve a specific invoice by ID + if invoices["response"].get("results"): + invoice_id = invoices["response"]["results"][0]["id"] + invoice = sdk.invoice().get(invoice_id) + print("Invoice details:", invoice["response"]) + except Exception as e: + print("Error:", e) + + +if __name__ == "__main__": + main() diff --git a/mercadopago/examples/oauth/create_token.py b/mercadopago/examples/oauth/create_token.py new file mode 100644 index 0000000..7170f9c --- /dev/null +++ b/mercadopago/examples/oauth/create_token.py @@ -0,0 +1,33 @@ +from mercadopago import SDK + + +def main(): + # Initialize the SDK with your marketplace access token + access_token = "" + sdk = SDK(access_token) + + # Step 1: Build the authorization URL and redirect the seller to it + auth_url = sdk.oauth().get_authorization_url( + app_id="", + redirect_uri="https://yourapp.com/callback", + random_id="", + ) + print("Redirect seller to:", auth_url) + + # Step 2: After the seller authorizes, exchange the received code for tokens + oauth_object = { + "client_secret": access_token, + "code": "", + "redirect_uri": "https://yourapp.com/callback", + "grant_type": "authorization_code", + } + + try: + response = sdk.oauth().create(oauth_object) + print("Token created successfully:", response["response"]) + except Exception as e: + print("Error:", e) + + +if __name__ == "__main__": + main() diff --git a/mercadopago/examples/point/create_payment_intent.py b/mercadopago/examples/point/create_payment_intent.py new file mode 100644 index 0000000..68a0623 --- /dev/null +++ b/mercadopago/examples/point/create_payment_intent.py @@ -0,0 +1,37 @@ +from mercadopago import SDK + + +def main(): + # Initialize the SDK with your access token + access_token = "" + sdk = SDK(access_token) + + # List available Point devices to get a device_id + devices = sdk.point().get_devices() + print("Available devices:", devices["response"]) + + # Create a payment intent on a specific device + device_id = "" + payment_intent_object = { + "amount": 1500, + "description": "Product purchase", + "payment": { + "installments": 1, + "type": "credit_card", + }, + } + + try: + response = sdk.point().create(device_id, payment_intent_object) + print("Payment intent created:", response["response"]) + + # Retrieve the payment intent status + payment_intent_id = response["response"]["id"] + status = sdk.point().get(payment_intent_id) + print("Payment intent status:", status["response"]) + except Exception as e: + print("Error:", e) + + +if __name__ == "__main__": + main() diff --git a/mercadopago/resources/__init__.py b/mercadopago/resources/__init__.py index 0b58843..a4e4baf 100644 --- a/mercadopago/resources/__init__.py +++ b/mercadopago/resources/__init__.py @@ -13,11 +13,14 @@ from mercadopago.resources.customer import Customer from mercadopago.resources.disbursement_refund import DisbursementRefund from mercadopago.resources.identification_type import IdentificationType +from mercadopago.resources.invoice import Invoice from mercadopago.resources.merchant_order import MerchantOrder +from mercadopago.resources.oauth import OAuth from mercadopago.resources.order import Order from mercadopago.resources.payment import Payment from mercadopago.resources.payment_methods import PaymentMethods from mercadopago.resources.plan import Plan +from mercadopago.resources.point import Point from mercadopago.resources.preapproval import PreApproval from mercadopago.resources.preference import Preference from mercadopago.resources.refund import Refund @@ -34,11 +37,14 @@ 'DisbursementRefund', 'HttpClient', 'IdentificationType', + 'Invoice', 'MerchantOrder', + 'OAuth', 'Order', 'Payment', 'PaymentMethods', 'Plan', + 'Point', 'PreApproval', 'Preference', 'Refund', diff --git a/mercadopago/resources/invoice.py b/mercadopago/resources/invoice.py new file mode 100644 index 0000000..3305f53 --- /dev/null +++ b/mercadopago/resources/invoice.py @@ -0,0 +1,62 @@ +"""Invoice resource for the MercadoPago Subscriptions API. + +Wraps ``/authorized_payments`` endpoints to retrieve and search invoices +generated by subscription (pre-approval) billing cycles. Each invoice +represents a scheduled charge attempt against the subscriber's payment +method. + +`API reference +`_ +""" +from mercadopago.core import MPBase + + +class Invoice(MPBase): + """Provides read access to subscription invoices (authorized payments). + + Each invoice corresponds to a billing cycle of a + :class:`~mercadopago.resources.preapproval.PreApproval` and tracks + the charge amount, status, retry attempts, and the resulting + payment. Use :meth:`get` and :meth:`search` to monitor billing + cycles and their outcomes. + """ + + def get(self, invoice_id, request_options=None): + """Retrieves a single invoice (authorized payment) by its ID. + + Args: + invoice_id: Unique invoice identifier assigned by + MercadoPago. + request_options: Per-call configuration overrides. + + Returns: + dict: Full invoice object including ``status``, + ``transaction_amount``, ``preapproval_id``, and + associated ``payment`` details. + + Reference: https://www.mercadopago.com/developers/en/reference/online-payments/subscriptions/get-authorized-payment/get + """ + return self._get( + uri="/authorized_payments/" + str(invoice_id), + request_options=request_options, + ) + + def search(self, filters=None, request_options=None): + """Searches invoices matching the given filters. + + Args: + filters: Query-string parameters such as ``preapproval_id``, + ``status``, ``payer_id``, ``offset``, and ``limit``. + request_options: Per-call configuration overrides. + + Returns: + dict: Paginated result with ``paging`` metadata and a + ``results`` list of matching invoices. + + Reference: https://www.mercadopago.com/developers/en/reference/online-payments/subscriptions/authorized-payment-search/get + """ + return self._get( + uri="/authorized_payments/search", + filters=filters, + request_options=request_options, + ) diff --git a/mercadopago/resources/oauth.py b/mercadopago/resources/oauth.py new file mode 100644 index 0000000..a51c22a --- /dev/null +++ b/mercadopago/resources/oauth.py @@ -0,0 +1,119 @@ +"""OAuth resource for the MercadoPago Authorization API. + +Wraps the OAuth 2.0 authorization code flow used in marketplace and +platform integrations to operate on behalf of other sellers. + +`API reference +`_ +""" +from urllib.parse import urlencode + +from mercadopago.core import MPBase + + +_AUTH_URL = "https://auth.mercadopago.com/authorization" + + +class OAuth(MPBase): + """Manages the OAuth 2.0 authorization code flow. + + Use this resource when your application needs to operate on behalf + of other MercadoPago sellers (marketplace or platform scenarios). + The flow involves redirecting the seller to the authorization URL, + receiving an authorization code, and exchanging it for access and + refresh tokens. + """ + + def get_authorization_url(self, app_id, redirect_uri, random_id): + """Builds the MercadoPago authorization URL for the OAuth flow. + + Redirect the seller to this URL to start the authorization + process. After granting permission, MercadoPago redirects back + to *redirect_uri* with a ``code`` query parameter. + + Args: + app_id: Your MercadoPago application's client ID. + redirect_uri: URI where MercadoPago sends the seller after + authorization. + random_id: CSRF-protection state parameter; should be a + unique, unpredictable value per authorization request. + + Returns: + str: Full authorization URL with query parameters. + + Reference: https://www.mercadopago.com/developers/en/docs/security/oauth/creation + """ + params = { + "client_id": app_id, + "response_type": "code", + "platform_id": "mp", + "state": random_id, + "redirect_uri": redirect_uri, + } + return _AUTH_URL + "?" + urlencode(params) + + def create(self, oauth_object, request_options=None): + """Exchanges an authorization code for an access token. + + Call this after receiving the ``code`` parameter in your + *redirect_uri* callback. The returned access token can be used + to make API requests on behalf of the authorizing seller. + + Args: + oauth_object: Dict with the authorization request fields: + ``client_secret`` (your access token), ``code`` + (authorization code received in the callback), + ``redirect_uri`` (must match the one used in + :meth:`get_authorization_url`), and ``grant_type`` + (``"authorization_code"``). + request_options: Per-call configuration overrides. + + Raises: + ValueError: If *oauth_object* is not a ``dict``. + + Returns: + dict: Token response including ``access_token``, + ``refresh_token``, and ``expires_in``. + + Reference: https://www.mercadopago.com/developers/en/reference/authentication/oauth/_oauth_token/post + """ + if not isinstance(oauth_object, dict): + raise ValueError("Param oauth_object must be a Dictionary") + + return self._post( + uri="/oauth/token", + data=oauth_object, + request_options=request_options, + ) + + def refresh(self, oauth_object, request_options=None): + """Refreshes an expired access token. + + Use this to extend the seller's session without requiring them + to re-authorize. The *refresh_token* is obtained from the + initial :meth:`create` response. + + Args: + oauth_object: Dict with the refresh request fields: + ``client_secret`` (your access token), + ``refresh_token`` (the token to refresh), and + ``grant_type`` (``"refresh_token"``). + request_options: Per-call configuration overrides. + + Raises: + ValueError: If *oauth_object* is not a ``dict``. + + Returns: + dict: New token response with a fresh ``access_token`` and + ``refresh_token``. + + Reference: https://www.mercadopago.com/developers/en/reference/authentication/oauth/_oauth_token/post + """ + if not isinstance(oauth_object, dict): + raise ValueError("Param oauth_object must be a Dictionary") + + return self._post( + uri="/oauth/token", + data=oauth_object, + request_options=request_options, + ) diff --git a/mercadopago/resources/order.py b/mercadopago/resources/order.py index 9c15479..ae992bd 100644 --- a/mercadopago/resources/order.py +++ b/mercadopago/resources/order.py @@ -14,6 +14,60 @@ class Order(MPBase): An order groups one or more payment transactions and supports a two-step (authorise then capture) flow as well as direct processing. + + Automatic Payments (AP) — supported keys for ``stored_credential`` dict: + + .. code-block:: python + + "stored_credential": { + "payment_initiator": str, # "cardholder" | "merchant" + "reason": str, # "recurring" | "installment" | "unscheduled" + "store_payment_method": bool, + "first_payment": bool, + "prev_transaction_ref": str, # ID of the previous transaction in the series. + # Required from the second charge onwards. + } + + Supported keys for ``automatic_payments`` dict: + + .. code-block:: python + + "automatic_payments": { + "payment_profile_id": str, # Stored payment profile ID. + "retries": int, # Max retry attempts on failure. + "schedule_date": str, # ISO 8601 scheduled charge date. + "due_date": str, # ISO 8601 payment due date. + } + + Supported keys for ``subscription_data`` dict: + + .. code-block:: python + + "subscription_data": { + "invoice_id": str, # ID of the invoice being paid. + "billing_date": str, # ISO 8601 billing date. + "subscription_sequence": { + "number": int, # Current payment number in the series. + "total": int, # Total payments in the subscription. + }, + "invoice_period": { + "type": str, # "monthly" | "daily" | "yearly". + "period": int, # Number of period units. + }, + } + + Supported keys for ``integration_data`` dict (root of order request): + + .. code-block:: python + + "integration_data": { + "integrator_id": str, # Certified integrator ID. + "platform_id": str, # Platform ID assigned by MercadoPago. + "corporation_id": str, # Corporation ID for multi-account setups. + "sponsor": { + "id": str, # MercadoPago user ID of the sponsor. + }, + } """ def search(self, filters=None, request_options=None): diff --git a/mercadopago/resources/order_automatic_payments.py b/mercadopago/resources/order_automatic_payments.py new file mode 100644 index 0000000..15bdbb2 --- /dev/null +++ b/mercadopago/resources/order_automatic_payments.py @@ -0,0 +1,27 @@ +"""Dataclass for automatic/recurring payment scheduling in order requests.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class OrderAutomaticPayments: + """Configuration for automatic (recurring) payment scheduling within an order. + + Use this dataclass to build the ``automatic_payments`` payload when creating + an order with Automatic Payments. Convert to dict with ``dataclasses.asdict()``. + + Attributes: + payment_profile_id: Identifier of the stored payment profile used for + automatic charges. Type: str. + retries: Maximum number of retry attempts if the automatic charge fails. + Type: int. + schedule_date: ISO 8601 date when the automatic payment is scheduled to + be charged (e.g. ``"2026-06-01T00:00:00.000-04:00"``). Type: str. + due_date: ISO 8601 date by which the payment must be completed before it + is considered overdue. Type: str. + """ + + payment_profile_id: Optional[str] = None + retries: Optional[int] = None + schedule_date: Optional[str] = None + due_date: Optional[str] = None diff --git a/mercadopago/resources/order_integration_data.py b/mercadopago/resources/order_integration_data.py new file mode 100644 index 0000000..e729807 --- /dev/null +++ b/mercadopago/resources/order_integration_data.py @@ -0,0 +1,36 @@ +"""Dataclasses for integration metadata in order requests.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class OrderSponsor: + """Sponsoring marketplace owner associated with an order. + + Attributes: + id: MercadoPago user ID of the sponsoring marketplace owner. Type: str. + """ + + id: Optional[str] = None + + +@dataclass +class OrderIntegrationData: + """Integration metadata for an order request. + + Identifies the integrator, platform, and corporation associated with the + integration. Use this dataclass to build the ``integration_data`` payload + at the root of the order create request. Convert to dict with + ``dataclasses.asdict()``. + + Attributes: + integrator_id: Identifier of the certified integrator. Type: str. + platform_id: Platform identifier assigned by MercadoPago. Type: str. + corporation_id: Corporation identifier for multi-account setups. Type: str. + sponsor: Sponsoring marketplace owner information. + """ + + integrator_id: Optional[str] = None + platform_id: Optional[str] = None + corporation_id: Optional[str] = None + sponsor: Optional[OrderSponsor] = None diff --git a/mercadopago/resources/order_stored_credential.py b/mercadopago/resources/order_stored_credential.py new file mode 100644 index 0000000..e3d296e --- /dev/null +++ b/mercadopago/resources/order_stored_credential.py @@ -0,0 +1,31 @@ +"""Dataclass for stored credential (card-on-file) data in automatic payment flows.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class OrderStoredCredential: + """Stored credential configuration for recurring payments (card-on-file). + + Use this dataclass to build the ``stored_credential`` payload when creating + an order with Automatic Payments. Convert to dict with ``dataclasses.asdict()``. + + Attributes: + payment_initiator: Who initiated the payment: ``"cardholder"`` or ``"merchant"``. + Type: str. + reason: Reason for using the stored credential. + One of ``"recurring"``, ``"installment"``, ``"unscheduled"``. Type: str. + store_payment_method: Whether to save the payment method for future charges. + 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. + """ + + 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 diff --git a/mercadopago/resources/order_subscription_data.py b/mercadopago/resources/order_subscription_data.py new file mode 100644 index 0000000..055e282 --- /dev/null +++ b/mercadopago/resources/order_subscription_data.py @@ -0,0 +1,51 @@ +"""Dataclass for subscription billing data in automatic payment order requests.""" +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class OrderSubscriptionSequence: + """Position of a payment within a subscription series. + + Attributes: + number: Current payment number in the subscription (1-based). Type: int. + total: Total number of payments expected in the plan. Type: int. + """ + + number: Optional[int] = None + total: Optional[int] = None + + +@dataclass +class OrderInvoicePeriod: + """Billing period covered by a subscription invoice. + + Attributes: + type: Period unit (e.g. ``"monthly"``, ``"daily"``, ``"yearly"``). Type: str. + period: Number of period units covered by this invoice. Type: int. + """ + + type: Optional[str] = None + period: Optional[int] = None + + +@dataclass +class OrderSubscriptionData: + """Subscription billing information for a payment within an order. + + Use this dataclass to build the ``subscription_data`` payload when creating + an order with Automatic Payments. Convert to dict with ``dataclasses.asdict()``. + + Attributes: + invoice_id: Identifier of the invoice being settled by this payment. + Type: str. + billing_date: ISO 8601 date when the subscription billing was triggered. + Type: str. + subscription_sequence: Position of this payment in the subscription series. + invoice_period: Billing period covered by this invoice. + """ + + invoice_id: Optional[str] = None + billing_date: Optional[str] = None + subscription_sequence: Optional[OrderSubscriptionSequence] = None + invoice_period: Optional[OrderInvoicePeriod] = None diff --git a/mercadopago/resources/point.py b/mercadopago/resources/point.py new file mode 100644 index 0000000..545df88 --- /dev/null +++ b/mercadopago/resources/point.py @@ -0,0 +1,121 @@ +"""Point resource for the MercadoPago Point Integration API. + +Wraps ``/point/integration-api`` endpoints for in-person payment +processing through MercadoPago Point devices (card readers). + +Supported operations: list devices, create payment intent, get payment +intent, and cancel payment intent. + +Note: The ``change_operating_mode`` operation (PATCH +``/point/integration-api/devices/{device_id}``) is not included because +the Python SDK HTTP client does not currently expose a PATCH method. + +`API reference +`_ +""" +from mercadopago.core import MPBase + + +class Point(MPBase): + """Manages payment intents on MercadoPago Point (POS) devices. + + Enables in-person payment processing by creating payment intents + that are sent to a physical Point device for the buyer to complete + the transaction by inserting or tapping their card. + """ + + def get_devices(self, filters=None, request_options=None): + """Lists Point devices linked to the authenticated account. + + Args: + filters: Optional query-string parameters such as + ``store_id`` and ``pos_id``. + request_options: Per-call configuration overrides. + + Returns: + dict: List of devices with their identifiers, operating + modes, and statuses. + + Reference: https://www.mercadopago.com/developers/en/reference/in-person-payments/point/devices/list-devices/get + """ + return self._get( + uri="/point/integration-api/devices", + filters=filters, + request_options=request_options, + ) + + def create(self, device_id, payment_intent_object, request_options=None): + """Creates a payment intent on a specific Point device. + + The payment intent is sent to the physical device, where the + buyer completes the transaction. + + Args: + device_id: Unique identifier of the target Point device. + payment_intent_object: Dict describing the payment intent, + including ``amount``, ``description``, and ``payment`` + method details. + request_options: Per-call configuration overrides. + + Raises: + ValueError: If *payment_intent_object* is not a ``dict``. + + Returns: + dict: Created payment intent including its ``id`` and + ``state``. + + Reference: https://www.mercadopago.com/developers/en/reference/in-person-payments/point/orders/create-order/post + """ + if not isinstance(payment_intent_object, dict): + raise ValueError("Param payment_intent_object must be a Dictionary") + + return self._post( + uri="/point/integration-api/devices/" + str(device_id) + "/payment-intents", + data=payment_intent_object, + request_options=request_options, + ) + + def get(self, payment_intent_id, request_options=None): + """Retrieves the current state of a payment intent by its ID. + + Use this to check whether the buyer has completed, cancelled, + or is still processing the payment on the device. + + Args: + payment_intent_id: Unique identifier of the payment intent. + request_options: Per-call configuration overrides. + + Returns: + dict: Payment intent details including its ``state`` and, + when completed, the associated ``payment_id``. + + Reference: https://www.mercadopago.com/developers/en/reference/in-person-payments/point/orders/get-order/get + """ + return self._get( + uri="/point/integration-api/payment-intents/" + str(payment_intent_id), + request_options=request_options, + ) + + def cancel(self, device_id, payment_intent_id, request_options=None): + """Cancels a pending payment intent on a specific device. + + Use this to abort a transaction before the buyer completes the + payment on the device. + + Args: + device_id: Unique identifier of the Point device holding + the intent. + payment_intent_id: Unique identifier of the payment intent + to cancel. + request_options: Per-call configuration overrides. + + Returns: + dict: Cancellation confirmation. + + Reference: https://www.mercadopago.com/developers/en/reference/in-person-payments/point/orders/cancel-order/post + """ + return self._delete( + uri="/point/integration-api/devices/" + str(device_id) + + "/payment-intents/" + str(payment_intent_id), + request_options=request_options, + ) diff --git a/mercadopago/sdk.py b/mercadopago/sdk.py index e225556..1b0b899 100644 --- a/mercadopago/sdk.py +++ b/mercadopago/sdk.py @@ -24,11 +24,14 @@ Customer, DisbursementRefund, IdentificationType, + Invoice, MerchantOrder, + OAuth, Order, Payment, PaymentMethods, Plan, + Point, PreApproval, Preference, Refund, @@ -37,7 +40,7 @@ ) -class SDK: +class SDK: # pylint: disable=too-many-public-methods """Central client providing factory methods for every API resource. Each factory method (e.g. :meth:`payment`, :meth:`order`) returns a @@ -116,6 +119,21 @@ def identification_type(self, request_options=None): return IdentificationType(request_options is not None and request_options or self.request_options, self.http_client) + def invoice(self, request_options=None): + """Creates an :class:`Invoice` resource to retrieve subscription billing invoices.""" + return Invoice(request_options is not None and request_options + or self.request_options, self.http_client) + + def oauth(self, request_options=None): + """Creates an :class:`OAuth` resource for the OAuth 2.0 authorization code flow.""" + return OAuth(request_options is not None and request_options + or self.request_options, self.http_client) + + def point(self, request_options=None): + """Creates a :class:`Point` resource for in-person payments via Point devices.""" + return Point(request_options is not None and request_options + or self.request_options, self.http_client) + def merchant_order(self, request_options=None): """Creates a :class:`MerchantOrder` resource for Checkout Pro orders.""" return MerchantOrder(request_options is not None and request_options diff --git a/pyproject.toml b/pyproject.toml index 5cba398..1fa201b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mercadopago" -version = "3.1.1" +version = "3.2.0" authors = [ {name = "Mercado Pago", email = "mp_sdk@mercadopago.com"} ] @@ -14,7 +14,7 @@ keywords = ["api", "mercadopago", "checkout", "payment in sdk integration", "lts license = {file = "LICENSE"} requires-python = ">=3.9" dependencies = [ - "requests>=2.32.4,<3" + "requests>=2.33.0,<3" ] classifiers = [ "License :: OSI Approved :: MIT License", diff --git a/tests/test_invoice.py b/tests/test_invoice.py new file mode 100644 index 0000000..7860eba --- /dev/null +++ b/tests/test_invoice.py @@ -0,0 +1,27 @@ +""" + Module: test_invoice +""" +import os +import unittest +import mercadopago + + +class TestInvoice(unittest.TestCase): + """ + Test Module: Invoice + """ + sdk = mercadopago.SDK(os.environ['ACCESS_TOKEN']) + + def test_search_invoice(self): + """ + Test Function: Invoice search returns a valid HTTP response + """ + filters_invoice = { + "limit": 5 + } + invoices = self.sdk.invoice().search(filters=filters_invoice) + self.assertIn(invoices["status"], [200, 400, 401, 404]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_oauth.py b/tests/test_oauth.py new file mode 100644 index 0000000..bc3d8af --- /dev/null +++ b/tests/test_oauth.py @@ -0,0 +1,47 @@ +""" + Module: test_oauth +""" +import os +import unittest +import mercadopago + + +class TestOAuth(unittest.TestCase): + """ + Test Module: OAuth + """ + sdk = mercadopago.SDK(os.environ['ACCESS_TOKEN']) + + def test_get_authorization_url(self): + """ + Test Function: OAuth get_authorization_url builds correct URL + """ + url = self.sdk.oauth().get_authorization_url( + app_id="TEST_APP_ID", + redirect_uri="https://example.com/callback", + random_id="csrf_state_token", + ) + self.assertIn("https://auth.mercadopago.com/authorization", url) + self.assertIn("client_id=TEST_APP_ID", url) + self.assertIn("response_type=code", url) + self.assertIn("redirect_uri=", url) + self.assertIn("state=csrf_state_token", url) + self.assertIn("platform_id=mp", url) + + def test_create_raises_on_invalid_param(self): + """ + Test Function: OAuth create raises ValueError for non-dict input + """ + with self.assertRaises(ValueError): + self.sdk.oauth().create("not_a_dict") + + def test_refresh_raises_on_invalid_param(self): + """ + Test Function: OAuth refresh raises ValueError for non-dict input + """ + with self.assertRaises(ValueError): + self.sdk.oauth().refresh("not_a_dict") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_order.py b/tests/test_order.py index 34445b9..7df6b59 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -2,7 +2,6 @@ Module: test_order """ import os -import time import unittest import random from datetime import datetime, timezone, timedelta @@ -222,8 +221,9 @@ def test_process_order(self): def test_cancel_order(self): card_token_id = self.create_master_test_card() order_id = self.create_order_canceled_or_captured(card_token_id) - time.sleep(10) - order_canceled = self.sdk.order().cancel(order_id) + order_canceled = api_call_with_retry( + lambda: self.sdk.order().cancel(order_id), expected_status=200 + ) self.assertEqual(order_canceled["status"], 200) self.assertEqual(order_canceled["response"]["status"], "canceled") diff --git a/tests/test_point.py b/tests/test_point.py new file mode 100644 index 0000000..d2e3ab0 --- /dev/null +++ b/tests/test_point.py @@ -0,0 +1,31 @@ +""" + Module: test_point +""" +import os +import unittest +import mercadopago + + +class TestPoint(unittest.TestCase): + """ + Test Module: Point + """ + sdk = mercadopago.SDK(os.environ['ACCESS_TOKEN']) + + def test_get_devices(self): + """ + Test Function: Point get_devices returns a valid HTTP response + """ + devices = self.sdk.point().get_devices() + self.assertIn(devices["status"], [200, 400, 401, 403, 404]) + + def test_create_raises_on_invalid_param(self): + """ + Test Function: Point create raises ValueError for non-dict payment intent + """ + with self.assertRaises(ValueError): + self.sdk.point().create("device_123", "not_a_dict") + + +if __name__ == "__main__": + unittest.main()