diff --git a/mercadopago/resources/order.py b/mercadopago/resources/order.py index de4a179..85825e0 100644 --- a/mercadopago/resources/order.py +++ b/mercadopago/resources/order.py @@ -276,6 +276,34 @@ def update_transaction( request_options=request_options, ) + def get_order_refunds(self, order_id, request_options=None): + """Retrieves all refunds associated with an order. + + Queries the GET endpoint to fetch refund information for the specified + order. Accepts optional headers (X-Idempotency-Key, X-Product-ID) via + request_options. + + Args: + order_id: String identifier of the order. + request_options: Per-call configuration overrides (can include + custom_headers with X-Idempotency-Key and X-Product-ID). + + Raises: + ValueError: If *order_id* is not a ``str``. + + Returns: + dict: Refund information response containing list of refunds. + + Reference: https://www.mercadopago.com/developers/en/reference/online-payments/checkout-api/get-order-refunds/get + """ + if not isinstance(order_id, str): + raise ValueError("Param order_id must be a string") + + return self._get( + uri=f"/v1/orders/{self._path_param(order_id)}/refund", + request_options=request_options, + ) + def refund_transaction(self, order_id, transaction_object=None, request_options=None): """Refunds an order's transactions. @@ -350,4 +378,4 @@ def delete_transaction(self, order_id, transaction_id, request_options=None): def search_auto_paging_iter(self, filters=None, request_options=None, limit=100): """Lazily yields all items matching *filters* across all pages.""" - return _paging_iter(self.search, filters, request_options, limit) + return _paging_iter(self.search, filters, request_options, limit) \ No newline at end of file diff --git a/tests/resources/test_order.py b/tests/resources/test_order.py new file mode 100644 index 0000000..02ab598 --- /dev/null +++ b/tests/resources/test_order.py @@ -0,0 +1,212 @@ +"""Unit tests for the Order resource using a mock HTTP client.""" +from dataclasses import asdict +import unittest + +from mercadopago.resources.order_automatic_payments import OrderAutomaticPayments + +from tests.base_client_test import BaseClientTest + + +class TestOrder(BaseClientTest): + """Test Module: Order""" + + def test_create(self): + fixture = self.load_fixture("order_create.json") + self.mock_post(fixture, status=201) + order_object = { + "type": "online", + "processing_mode": "automatic", + "total_amount": "1000.00", + "payer": {"email": "test_user@testuser.com"}, + } + result = self.sdk.order().create(order_object) + self.assertEqual(201, result["status"]) + resp = result["response"] + self.assertEqual("01JKXTQ2AZVE0RANCPYA6WBNPW", resp["id"]) + self.assertEqual("online", resp["type"]) + self.assertEqual("automatic", resp["processing_mode"]) + self.assertEqual("processed", resp["status"]) + self.assertEqual("1000.00", resp["total_amount"]) + self.assertIsInstance(resp["items"], list) + self.assertEqual("Point Mini", resp["items"][0]["title"]) + self.assertIn("transactions", resp) + self.assertIn("payments", resp["transactions"]) + self.assertEqual("BR", resp["country_code"]) + self.mock_http.post.assert_called_once() + + def test_get(self): + fixture = self.load_fixture("order_get.json") + self.mock_get(fixture) + result = self.sdk.order().get("01JKXTQ2AZVE0RANCPYA6WBNPW") + self.assertEqual(200, result["status"]) + resp = result["response"] + self.assertEqual("01JKXTQ2AZVE0RANCPYA6WBNPW", resp["id"]) + self.assertEqual("processed", resp["status"]) + self.assertEqual("fully_processed", resp["status_detail"]) + self.assertEqual("1000.00", resp["total_amount"]) + self.assertEqual("1000.00", resp["total_paid_amount"]) + self.assertIn("payer", resp) + self.assertEqual("test_user@testuser.com", resp["payer"]["email"]) + self.assertIsInstance(resp["items"], list) + self.assertIn("transactions", resp) + self.assertIn("payments", resp["transactions"]) + self.assertEqual("pay-001", resp["transactions"]["payments"][0]["id"]) + self.assertIn("created_date", resp) + self.assertIn("last_updated_date", resp) + self.mock_http.get.assert_called_once() + + def test_get_raises_for_non_string(self): + with self.assertRaises(ValueError): + self.sdk.order().get(123) + + def test_process(self): + fixture = self.load_fixture("order_process.json") + self.mock_post(fixture, status=200) + result = self.sdk.order().process("01JKXTQ2AZVE0RANCPYA6WBNPW") + self.assertEqual(200, result["status"]) + resp = result["response"] + self.assertEqual("01JKXTQ2AZVE0RANCPYA6WBNPW", resp["id"]) + self.assertEqual("processed", resp["status"]) + self.mock_http.post.assert_called_once() + + def test_process_raises_for_non_string(self): + with self.assertRaises(ValueError): + self.sdk.order().process(123) + + def test_cancel(self): + fixture = self.load_fixture("order_cancel.json") + self.mock_post(fixture, status=200) + result = self.sdk.order().cancel("01JKXTQ2AZVE0RANCPYA6WBNPW") + self.assertEqual(200, result["status"]) + resp = result["response"] + self.assertEqual("01JKXTQ2AZVE0RANCPYA6WBNPW", resp["id"]) + self.assertEqual("canceled", resp["status"]) + self.mock_http.post.assert_called_once() + + def test_capture(self): + fixture = self.load_fixture("order_capture.json") + self.mock_post(fixture, status=200) + result = self.sdk.order().capture("01JKXTQ2AZVE0RANCPYA6WBNPW") + self.assertEqual(200, result["status"]) + resp = result["response"] + self.assertEqual("01JKXTQ2AZVE0RANCPYA6WBNPW", resp["id"]) + self.assertEqual("processed", resp["status"]) + self.mock_http.post.assert_called_once() + + def test_refund(self): + fixture = self.load_fixture("order_refund.json") + self.mock_post(fixture, status=201) + result = self.sdk.order().refund("01JKXTQ2AZVE0RANCPYA6WBNPW") + self.assertEqual(201, result["status"]) + resp = result["response"] + self.assertEqual("approved", resp["status"]) + self.assertIn("date_created", resp) + self.mock_http.post.assert_called_once() + + def test_refund_with_object(self): + fixture = self.load_fixture("order_refund.json") + self.mock_post(fixture, status=201) + refund_object = {"transactions": [{"id": "tx-001", "amount": "25.00"}]} + result = self.sdk.order().refund("01JKXTQ2AZVE0RANCPYA6WBNPW", refund_object) + self.assertEqual(201, result["status"]) + self.mock_http.post.assert_called_once() + + def test_get_order_refunds(self): + """Test retrieval of all refunds for an order.""" + fixture = self.load_fixture("order_get_refunds.json") + self.mock_get(fixture) + result = self.sdk.order().get_order_refunds("01JKXTQ2AZVE0RANCPYA6WBNPW") + self.assertEqual(200, result["status"]) + resp = result["response"] + self.assertIsInstance(resp, list) + if resp: + self.assertIn("id", resp[0]) + self.assertIn("amount", resp[0]) + self.assertIn("status", resp[0]) + self.mock_http.get.assert_called_once() + + def test_get_order_refunds_raises_for_non_string(self): + """Test that get_order_refunds raises ValueError for non-string order_id.""" + with self.assertRaises(ValueError): + self.sdk.order().get_order_refunds(123) + + def test_get_order_refunds_with_custom_headers(self): + """Test get_order_refunds with custom headers (X-Idempotency-Key, X-Product-ID).""" + from mercadopago.config.request_options import RequestOptions + + fixture = self.load_fixture("order_get_refunds.json") + self.mock_get(fixture) + + request_options = RequestOptions(access_token="TEST_TOKEN") + request_options.custom_headers = { + "X-Idempotency-Key": "test-idempotency-key-123", + "X-Product-ID": "test-product-id" + } + + result = self.sdk.order().get_order_refunds( + "01JKXTQ2AZVE0RANCPYA6WBNPW", + request_options=request_options + ) + self.assertEqual(200, result["status"]) + self.mock_http.get.assert_called_once() + + def test_search(self): + fixture = self.load_fixture("order_search.json") + self.mock_get(fixture) + result = self.sdk.order().search() + self.assertEqual(200, result["status"]) + resp = result["response"] + self.assertIn("data", resp) + self.assertIsInstance(resp["data"], list) + self.assertEqual("01JKXTQ2AZVE0RANCPYA6WBNPW", resp["data"][0]["id"]) + self.mock_http.get.assert_called_once() + + def test_create_transaction(self): + self.mock_post({"id": "tx-001"}, status=201) + transaction_object = { + "payments": [{"amount": "1000.00", "payment_method": {"id": "visa"}}] + } + result = self.sdk.order().create_transaction("01JKXTQ2AZVE0RANCPYA6WBNPW", transaction_object) + self.assertEqual(201, result["status"]) + self.mock_http.post.assert_called_once() + + def test_update_transaction(self): + self.mock_put({"id": "tx-001"}) + result = self.sdk.order().update_transaction( + "01JKXTQ2AZVE0RANCPYA6WBNPW", "tx-001", {"payment_method": {"id": "master"}} + ) + self.assertEqual(200, result["status"]) + self.mock_http.put.assert_called_once() + + def test_delete_transaction(self): + self.mock_delete(None, status=204) + result = self.sdk.order().delete_transaction("01JKXTQ2AZVE0RANCPYA6WBNPW", "tx-001") + self.assertEqual(204, result["status"]) + self.mock_http.delete.assert_called_once() + + def test_create_raises_for_non_dict(self): + with self.assertRaises(ValueError): + self.sdk.order().create("not-a-dict") + + def test_automatic_payments_subscription_payload(self): + automatic_payments = OrderAutomaticPayments( + subscription={ + "id": "subscription-1", + "sequence": {"number": 1, "total": 12}, + "invoice": { + "id": "invoice-1", + "billing_date": "2026-08-26", + "period": {"interval": 1, "type": "month"}, + }, + } + ) + + payload = asdict(automatic_payments) + + self.assertEqual("subscription-1", payload["subscription"]["id"]) + self.assertEqual(12, payload["subscription"]["sequence"]["total"]) + self.assertEqual("month", payload["subscription"]["invoice"]["period"]["type"]) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file