diff --git a/lago_python_client/client.py b/lago_python_client/client.py index 2a1924c..8a6308f 100644 --- a/lago_python_client/client.py +++ b/lago_python_client/client.py @@ -25,6 +25,8 @@ from .invoiced_usages.clients import InvoicedUsageClient from .invoices.clients import InvoiceClient from .mrrs.clients import MrrClient +from .order_forms.clients import OrderFormClient +from .orders.clients import OrderClient from .organizations.clients import OrganizationClient from .overdue_balances.clients import OverdueBalanceClient from .payment_receipts.clients import PaymentReceiptClient @@ -182,6 +184,14 @@ def invoiced_usages(self) -> InvoicedUsageClient: def mrrs(self) -> MrrClient: return self._create_client(MrrClient, self.base_api_url, self.api_key) + @callable_cached_property + def order_forms(self) -> OrderFormClient: + return self._create_client(OrderFormClient, self.base_api_url, self.api_key) + + @callable_cached_property + def orders(self) -> OrderClient: + return self._create_client(OrderClient, self.base_api_url, self.api_key) + @callable_cached_property def organizations(self) -> OrganizationClient: return self._create_client(OrganizationClient, self.base_api_url, self.api_key) diff --git a/lago_python_client/models/__init__.py b/lago_python_client/models/__init__.py index f71d17c..b19fd87 100644 --- a/lago_python_client/models/__init__.py +++ b/lago_python_client/models/__init__.py @@ -218,6 +218,21 @@ from .minimum_commitment import ( MinimumCommitmentResponse as MinimumCommitmentResponse, ) +from .order import ( + OrderExecute as OrderExecute, +) +from .order import ( + OrderExecutionRecordResponse as OrderExecutionRecordResponse, +) +from .order import ( + OrderResponse as OrderResponse, +) +from .order_form import ( + OrderFormMarkAsSigned as OrderFormMarkAsSigned, +) +from .order_form import ( + OrderFormResponse as OrderFormResponse, +) from .organization import ( Organization as Organization, ) diff --git a/lago_python_client/models/order.py b/lago_python_client/models/order.py new file mode 100644 index 0000000..f610c6b --- /dev/null +++ b/lago_python_client/models/order.py @@ -0,0 +1,39 @@ +from typing import List, Optional + +from lago_python_client.base_model import BaseModel + +from ..base_model import BaseResponseModel +from .quote import QuoteBillingItemsResponse + + +class OrderExecute(BaseModel): + execution_mode: Optional[str] + + +class OrderExecutionRecordResponse(BaseResponseModel): + executed_at: Optional[str] + execution_mode: Optional[str] + invoice_id: Optional[str] + subscription_ids: Optional[List[str]] + terminated_subscription_ids: Optional[List[str]] + applied_coupon_ids: Optional[List[str]] + wallet_ids: Optional[List[str]] + errors: Optional[List[str]] + + +class OrderResponse(BaseResponseModel): + lago_id: str + number: str + status: str + order_type: str + execution_mode: Optional[str] + currency: Optional[str] + executed_at: Optional[str] + execution_record: Optional[OrderExecutionRecordResponse] + lago_organization_id: str + lago_customer_id: str + lago_order_form_id: str + created_at: str + updated_at: str + # Omitted from the webhook payloads, being a heavy blob. + billing_snapshot: Optional[QuoteBillingItemsResponse] diff --git a/lago_python_client/models/order_form.py b/lago_python_client/models/order_form.py new file mode 100644 index 0000000..2ae212f --- /dev/null +++ b/lago_python_client/models/order_form.py @@ -0,0 +1,28 @@ +from typing import Optional + +from lago_python_client.base_model import BaseModel + +from ..base_model import BaseResponseModel + + +class OrderFormMarkAsSigned(BaseModel): + signed_document: Optional[str] + execution_mode: Optional[str] + execute_at: Optional[str] + + +class OrderFormResponse(BaseResponseModel): + lago_id: str + number: str + status: str + void_reason: Optional[str] + expires_at: Optional[str] + signed_at: Optional[str] + voided_at: Optional[str] + signed_document_url: Optional[str] + lago_organization_id: str + lago_customer_id: str + lago_quote_id: str + lago_quote_version_id: str + created_at: str + updated_at: str diff --git a/lago_python_client/order_forms/__init__.py b/lago_python_client/order_forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lago_python_client/order_forms/clients.py b/lago_python_client/order_forms/clients.py new file mode 100644 index 0000000..aa66c4a --- /dev/null +++ b/lago_python_client/order_forms/clients.py @@ -0,0 +1,74 @@ +from typing import ClassVar, Optional, Type + +import httpx + +from ..base_client import BaseClient +from ..mixins import ( + DEFAULT_TIMEOUT, + FindAllCommandMixin, + FindCommandMixin, +) +from ..models.order_form import OrderFormMarkAsSigned, OrderFormResponse +from ..services.json import to_json +from ..services.request import ( + make_headers, + make_url, + send_post_request, +) +from ..services.response import Response, get_response_data, prepare_object_response + + +class OrderFormClient( + FindCommandMixin[OrderFormResponse], + FindAllCommandMixin[OrderFormResponse], + BaseClient, +): + API_RESOURCE: ClassVar[str] = "order_forms" + RESPONSE_MODEL: ClassVar[Type[OrderFormResponse]] = OrderFormResponse + ROOT_NAME: ClassVar[str] = "order_form" + + def mark_as_signed( + self, + resource_id: str, + input_object: Optional[OrderFormMarkAsSigned] = None, + timeout: Optional[httpx.Timeout] = DEFAULT_TIMEOUT, + ) -> OrderFormResponse: + """Record the customer's signature and create the order carrying the deal out.""" + payload = input_object.dict(exclude_none=True) if input_object else {} + + api_response: Response = send_post_request( + url=make_url( + origin=self.base_url, + path_parts=(self.API_RESOURCE, resource_id, "mark_as_signed"), + ), + content=to_json({self.ROOT_NAME: payload}) if payload else None, + headers=make_headers(api_key=self.api_key), + timeout=timeout, + rate_limit_retry_config=self.rate_limit_retry_config, + ) + + return prepare_object_response( + response_model=self.RESPONSE_MODEL, + data=get_response_data(response=api_response, key=self.ROOT_NAME), + ) + + def void( + self, + resource_id: str, + timeout: Optional[httpx.Timeout] = DEFAULT_TIMEOUT, + ) -> OrderFormResponse: + """Void a generated order form, cascading to the quote version it came from.""" + api_response: Response = send_post_request( + url=make_url( + origin=self.base_url, + path_parts=(self.API_RESOURCE, resource_id, "void"), + ), + headers=make_headers(api_key=self.api_key), + timeout=timeout, + rate_limit_retry_config=self.rate_limit_retry_config, + ) + + return prepare_object_response( + response_model=self.RESPONSE_MODEL, + data=get_response_data(response=api_response, key=self.ROOT_NAME), + ) diff --git a/lago_python_client/orders/__init__.py b/lago_python_client/orders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lago_python_client/orders/clients.py b/lago_python_client/orders/clients.py new file mode 100644 index 0000000..52d4e97 --- /dev/null +++ b/lago_python_client/orders/clients.py @@ -0,0 +1,53 @@ +from typing import ClassVar, Optional, Type + +import httpx + +from ..base_client import BaseClient +from ..mixins import ( + DEFAULT_TIMEOUT, + FindAllCommandMixin, + FindCommandMixin, +) +from ..models.order import OrderExecute, OrderResponse +from ..services.json import to_json +from ..services.request import ( + make_headers, + make_url, + send_post_request, +) +from ..services.response import Response, get_response_data, prepare_object_response + + +class OrderClient( + FindCommandMixin[OrderResponse], + FindAllCommandMixin[OrderResponse], + BaseClient, +): + API_RESOURCE: ClassVar[str] = "orders" + RESPONSE_MODEL: ClassVar[Type[OrderResponse]] = OrderResponse + ROOT_NAME: ClassVar[str] = "order" + + def execute( + self, + resource_id: str, + input_object: Optional[OrderExecute] = None, + timeout: Optional[httpx.Timeout] = DEFAULT_TIMEOUT, + ) -> OrderResponse: + """Carry out an order on demand, without waiting for its schedule.""" + payload = input_object.dict(exclude_none=True) if input_object else {} + + api_response: Response = send_post_request( + url=make_url( + origin=self.base_url, + path_parts=(self.API_RESOURCE, resource_id, "execute"), + ), + content=to_json({self.ROOT_NAME: payload}) if payload else None, + headers=make_headers(api_key=self.api_key), + timeout=timeout, + rate_limit_retry_config=self.rate_limit_retry_config, + ) + + return prepare_object_response( + response_model=self.RESPONSE_MODEL, + data=get_response_data(response=api_response, key=self.ROOT_NAME), + ) diff --git a/tests/fixtures/executed_order.json b/tests/fixtures/executed_order.json new file mode 100644 index 0000000..5fd1051 --- /dev/null +++ b/tests/fixtures/executed_order.json @@ -0,0 +1,36 @@ +{ + "order": { + "lago_id": "cc33cc33-cc33-cc33-cc33-cc33cc33cc33", + "number": "OR-2026-0001", + "status": "executed", + "order_type": "subscription_creation", + "execution_mode": "execute_in_lago", + "currency": "EUR", + "executed_at": "2026-07-01T00:00:00Z", + "execution_record": { + "executed_at": "2026-07-01T00:00:00Z", + "execution_mode": "execute_in_lago", + "invoice_id": null, + "subscription_ids": ["dd44dd44-dd44-dd44-dd44-dd44dd44dd44"], + "terminated_subscription_ids": [], + "applied_coupon_ids": ["ee55ee55-ee55-ee55-ee55-ee55ee55ee55"], + "wallet_ids": [], + "errors": [] + }, + "lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12", + "lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01", + "lago_order_form_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11", + "created_at": "2026-05-02T10:15:00Z", + "updated_at": "2026-07-01T00:00:00Z", + "billing_snapshot": { + "plans": [ + { + "id": "7a567a56-7a56-7a56-7a56-7a567a567a56", + "localId": "b5c1e2a4-4e1e-4a7f-9f0e-9c1a0c7e1f2b", + "type": "plan", + "payload": { "code": "premium_plan" } + } + ] + } + } +} diff --git a/tests/fixtures/order.json b/tests/fixtures/order.json new file mode 100644 index 0000000..97043a1 --- /dev/null +++ b/tests/fixtures/order.json @@ -0,0 +1,36 @@ +{ + "order": { + "lago_id": "cc33cc33-cc33-cc33-cc33-cc33cc33cc33", + "number": "OR-2026-0001", + "status": "created", + "order_type": "subscription_creation", + "execution_mode": "execute_in_lago", + "currency": "EUR", + "executed_at": null, + "execution_record": { + "executed_at": null, + "execution_mode": "execute_in_lago", + "invoice_id": null, + "subscription_ids": [], + "terminated_subscription_ids": [], + "applied_coupon_ids": [], + "wallet_ids": [], + "errors": [] + }, + "lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12", + "lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01", + "lago_order_form_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11", + "created_at": "2026-05-02T10:15:00Z", + "updated_at": "2026-05-02T10:15:00Z", + "billing_snapshot": { + "plans": [ + { + "id": "7a567a56-7a56-7a56-7a56-7a567a567a56", + "localId": "b5c1e2a4-4e1e-4a7f-9f0e-9c1a0c7e1f2b", + "type": "plan", + "payload": { "code": "premium_plan" } + } + ] + } + } +} diff --git a/tests/fixtures/order_form.json b/tests/fixtures/order_form.json new file mode 100644 index 0000000..85b12b4 --- /dev/null +++ b/tests/fixtures/order_form.json @@ -0,0 +1,18 @@ +{ + "order_form": { + "lago_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11", + "number": "OF-2026-0001", + "status": "generated", + "void_reason": null, + "expires_at": "2026-06-30T23:59:59Z", + "signed_at": null, + "voided_at": null, + "signed_document_url": null, + "lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12", + "lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01", + "lago_quote_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "lago_quote_version_id": "4d234d23-4d23-4d23-4d23-4d234d234d23", + "created_at": "2026-04-29T08:59:51Z", + "updated_at": "2026-04-29T08:59:51Z" + } +} diff --git a/tests/fixtures/order_form_index.json b/tests/fixtures/order_form_index.json new file mode 100644 index 0000000..77f34f9 --- /dev/null +++ b/tests/fixtures/order_form_index.json @@ -0,0 +1,43 @@ +{ + "order_forms": [ + { + "lago_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11", + "number": "OF-2026-0001", + "status": "generated", + "void_reason": null, + "expires_at": "2026-06-30T23:59:59Z", + "signed_at": null, + "voided_at": null, + "signed_document_url": null, + "lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12", + "lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01", + "lago_quote_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "lago_quote_version_id": "4d234d23-4d23-4d23-4d23-4d234d234d23", + "created_at": "2026-04-29T08:59:51Z", + "updated_at": "2026-04-29T08:59:51Z" + }, + { + "lago_id": "bb22bb22-bb22-bb22-bb22-bb22bb22bb22", + "number": "OF-2026-0002", + "status": "voided", + "void_reason": "manual", + "expires_at": null, + "signed_at": null, + "voided_at": "2026-05-01T09:00:00Z", + "signed_document_url": null, + "lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12", + "lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01", + "lago_quote_id": "6f456f45-6f45-6f45-6f45-6f456f456f45", + "lago_quote_version_id": "9c789c78-9c78-9c78-9c78-9c789c789c78", + "created_at": "2026-04-30T08:59:51Z", + "updated_at": "2026-05-01T09:00:00Z" + } + ], + "meta": { + "current_page": 1, + "next_page": null, + "prev_page": null, + "total_pages": 1, + "total_count": 2 + } +} diff --git a/tests/fixtures/order_index.json b/tests/fixtures/order_index.json new file mode 100644 index 0000000..87f8570 --- /dev/null +++ b/tests/fixtures/order_index.json @@ -0,0 +1,61 @@ +{ + "orders": [ + { + "lago_id": "cc33cc33-cc33-cc33-cc33-cc33cc33cc33", + "number": "OR-2026-0001", + "status": "created", + "order_type": "subscription_creation", + "execution_mode": "execute_in_lago", + "currency": "EUR", + "executed_at": null, + "execution_record": { + "executed_at": null, + "execution_mode": "execute_in_lago", + "invoice_id": null, + "subscription_ids": [], + "terminated_subscription_ids": [], + "applied_coupon_ids": [], + "wallet_ids": [], + "errors": [] + }, + "lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12", + "lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01", + "lago_order_form_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11", + "created_at": "2026-05-02T10:15:00Z", + "updated_at": "2026-05-02T10:15:00Z", + "billing_snapshot": {} + }, + { + "lago_id": "ff66ff66-ff66-ff66-ff66-ff66ff66ff66", + "number": "OR-2026-0002", + "status": "failed", + "order_type": "one_off", + "execution_mode": null, + "currency": "EUR", + "executed_at": null, + "execution_record": { + "executed_at": null, + "execution_mode": null, + "invoice_id": null, + "subscription_ids": [], + "terminated_subscription_ids": [], + "applied_coupon_ids": [], + "wallet_ids": [], + "errors": ["plan_not_found"] + }, + "lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12", + "lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01", + "lago_order_form_id": "bb22bb22-bb22-bb22-bb22-bb22bb22bb22", + "created_at": "2026-05-03T10:15:00Z", + "updated_at": "2026-05-03T10:15:00Z", + "billing_snapshot": {} + } + ], + "meta": { + "current_page": 1, + "next_page": null, + "prev_page": null, + "total_pages": 1, + "total_count": 2 + } +} diff --git a/tests/fixtures/signed_order_form.json b/tests/fixtures/signed_order_form.json new file mode 100644 index 0000000..6aec7d9 --- /dev/null +++ b/tests/fixtures/signed_order_form.json @@ -0,0 +1,18 @@ +{ + "order_form": { + "lago_id": "aa11aa11-aa11-aa11-aa11-aa11aa11aa11", + "number": "OF-2026-0001", + "status": "signed", + "void_reason": null, + "expires_at": "2026-06-30T23:59:59Z", + "signed_at": "2026-05-02T10:15:00Z", + "voided_at": null, + "signed_document_url": "https://api.getlago.com/rails/active_storage/blobs/redirect/eyJfcmFpbHMi/OF-2026-0001", + "lago_organization_id": "3c123c12-3c12-3c12-3c12-3c123c123c12", + "lago_customer_id": "2b012b01-2b01-2b01-2b01-2b012b012b01", + "lago_quote_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "lago_quote_version_id": "4d234d23-4d23-4d23-4d23-4d234d234d23", + "created_at": "2026-04-29T08:59:51Z", + "updated_at": "2026-05-02T10:15:00Z" + } +} diff --git a/tests/test_order_client.py b/tests/test_order_client.py new file mode 100644 index 0000000..4c755d1 --- /dev/null +++ b/tests/test_order_client.py @@ -0,0 +1,146 @@ +import os +from urllib.parse import urlencode + +import pytest +from pytest_httpx import HTTPXMock + +from lago_python_client.client import Client +from lago_python_client.exceptions import LagoApiError +from lago_python_client.models.order import OrderExecute + +ENDPOINT = "https://api.getlago.com/api/v1/orders" +ORDER_ID = "cc33cc33-cc33-cc33-cc33-cc33cc33cc33" + + +def mock_response(fixture_path): + this_dir = os.path.dirname(os.path.abspath(__file__)) + data_path = os.path.join(this_dir, fixture_path) + + with open(data_path, "rb") as response: + return response.read() + + +def test_valid_find_order_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="GET", + url=ENDPOINT + f"/{ORDER_ID}", + content=mock_response("fixtures/order.json"), + ) + response = client.orders.find(ORDER_ID) + + assert response.lago_id == ORDER_ID + assert response.number == "OR-2026-0001" + assert response.status == "created" + assert response.order_type == "subscription_creation" + assert response.execution_mode == "execute_in_lago" + assert response.executed_at is None + assert response.execution_record.errors == [] + assert response.lago_order_form_id == "aa11aa11-aa11-aa11-aa11-aa11aa11aa11" + assert response.billing_snapshot.plans[0].payload["code"] == "premium_plan" + + +def test_invalid_find_order_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="GET", + url=ENDPOINT + "/invalid", + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.orders.find("invalid") + + +def test_valid_find_all_order_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="GET", + url=ENDPOINT, + content=mock_response("fixtures/order_index.json"), + ) + response = client.orders.find_all() + + assert response["orders"][0].number == "OR-2026-0001" + assert response["orders"][1].status == "failed" + assert response["orders"][1].execution_mode is None + assert response["orders"][1].execution_record.errors == ["plan_not_found"] + assert response["meta"]["current_page"] == 1 + + +def test_valid_find_all_order_request_with_options(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + options = {"per_page": 2, "page": 1, "status[]": ["created"], "execution_mode[]": ["execute_in_lago"]} + + httpx_mock.add_response( + method="GET", + url=ENDPOINT + "?" + urlencode(options, doseq=True), + content=mock_response("fixtures/order_index.json"), + ) + response = client.orders.find_all(options) + + assert response["orders"][0].number == "OR-2026-0001" + assert response["meta"]["current_page"] == 1 + + +def test_valid_execute_order_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url=ENDPOINT + f"/{ORDER_ID}/execute", + content=mock_response("fixtures/executed_order.json"), + ) + response = client.orders.execute(ORDER_ID) + + assert response.lago_id == ORDER_ID + assert response.status == "executed" + assert response.executed_at == "2026-07-01T00:00:00Z" + assert response.execution_record.subscription_ids == ["dd44dd44-dd44-dd44-dd44-dd44dd44dd44"] + assert response.execution_record.applied_coupon_ids == ["ee55ee55-ee55-ee55-ee55-ee55ee55ee55"] + + +def test_valid_execute_order_request_with_execution_mode(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url=ENDPOINT + f"/{ORDER_ID}/execute", + match_content=b'{"order":{"execution_mode":"execute_in_lago"}}', + content=mock_response("fixtures/executed_order.json"), + ) + response = client.orders.execute(ORDER_ID, OrderExecute(execution_mode="execute_in_lago")) + + assert response.status == "executed" + + +def test_invalid_execute_order_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url=ENDPOINT + f"/{ORDER_ID}/execute", + status_code=422, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.orders.execute(ORDER_ID) + + +def test_execute_order_request_with_missing_catalog_record(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url=ENDPOINT + f"/{ORDER_ID}/execute", + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.orders.execute(ORDER_ID) diff --git a/tests/test_order_form_client.py b/tests/test_order_form_client.py new file mode 100644 index 0000000..1f2096d --- /dev/null +++ b/tests/test_order_form_client.py @@ -0,0 +1,161 @@ +import os +from urllib.parse import urlencode + +import pytest +from pytest_httpx import HTTPXMock + +from lago_python_client.client import Client +from lago_python_client.exceptions import LagoApiError +from lago_python_client.models.order_form import OrderFormMarkAsSigned + +ENDPOINT = "https://api.getlago.com/api/v1/order_forms" +ORDER_FORM_ID = "aa11aa11-aa11-aa11-aa11-aa11aa11aa11" + + +def mock_response(fixture_path): + this_dir = os.path.dirname(os.path.abspath(__file__)) + data_path = os.path.join(this_dir, fixture_path) + + with open(data_path, "rb") as response: + return response.read() + + +def test_valid_find_order_form_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="GET", + url=ENDPOINT + f"/{ORDER_FORM_ID}", + content=mock_response("fixtures/order_form.json"), + ) + response = client.order_forms.find(ORDER_FORM_ID) + + assert response.lago_id == ORDER_FORM_ID + assert response.number == "OF-2026-0001" + assert response.status == "generated" + assert response.void_reason is None + assert response.signed_document_url is None + assert response.lago_quote_version_id == "4d234d23-4d23-4d23-4d23-4d234d234d23" + + +def test_invalid_find_order_form_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="GET", + url=ENDPOINT + "/invalid", + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.order_forms.find("invalid") + + +def test_valid_find_all_order_form_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="GET", + url=ENDPOINT, + content=mock_response("fixtures/order_form_index.json"), + ) + response = client.order_forms.find_all() + + assert response["order_forms"][0].number == "OF-2026-0001" + assert response["order_forms"][1].status == "voided" + assert response["order_forms"][1].void_reason == "manual" + assert response["order_forms"][1].expires_at is None + assert response["meta"]["current_page"] == 1 + + +def test_valid_find_all_order_form_request_with_options(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + options = {"per_page": 2, "page": 1, "status[]": ["generated"], "search_term": "OF-2026"} + + httpx_mock.add_response( + method="GET", + url=ENDPOINT + "?" + urlencode(options, doseq=True), + content=mock_response("fixtures/order_form_index.json"), + ) + response = client.order_forms.find_all(options) + + assert response["order_forms"][0].number == "OF-2026-0001" + assert response["meta"]["current_page"] == 1 + + +def test_valid_mark_as_signed_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url=ENDPOINT + f"/{ORDER_FORM_ID}/mark_as_signed", + content=mock_response("fixtures/signed_order_form.json"), + ) + response = client.order_forms.mark_as_signed(ORDER_FORM_ID) + + assert response.lago_id == ORDER_FORM_ID + assert response.status == "signed" + + +def test_valid_mark_as_signed_request_with_params(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url=ENDPOINT + f"/{ORDER_FORM_ID}/mark_as_signed", + match_content=(b'{"order_form":{"execution_mode":"execute_in_lago","execute_at":"2026-07-01T00:00:00Z"}}'), + content=mock_response("fixtures/signed_order_form.json"), + ) + response = client.order_forms.mark_as_signed( + ORDER_FORM_ID, + OrderFormMarkAsSigned( + execution_mode="execute_in_lago", + execute_at="2026-07-01T00:00:00Z", + ), + ) + + assert response.status == "signed" + assert response.signed_at == "2026-05-02T10:15:00Z" + assert "OF-2026-0001" in response.signed_document_url + + +def test_invalid_mark_as_signed_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url=ENDPOINT + f"/{ORDER_FORM_ID}/mark_as_signed", + status_code=422, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.order_forms.mark_as_signed(ORDER_FORM_ID) + + +def test_valid_void_order_form_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url=ENDPOINT + f"/{ORDER_FORM_ID}/void", + content=mock_response("fixtures/order_form.json"), + ) + response = client.order_forms.void(ORDER_FORM_ID) + + assert response.lago_id == ORDER_FORM_ID + + +def test_invalid_void_order_form_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url=ENDPOINT + f"/{ORDER_FORM_ID}/void", + status_code=422, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.order_forms.void(ORDER_FORM_ID)