diff --git a/lago_python_client/client.py b/lago_python_client/client.py index 8686d90..7f0b8da 100644 --- a/lago_python_client/client.py +++ b/lago_python_client/client.py @@ -15,6 +15,7 @@ from .organizations.clients import OrganizationClient from .overdue_balances.clients import OverdueBalanceClient from .payment_requests.clients import PaymentRequestClient +from .payments.clients import PaymentClient from .invoice_collections.clients import InvoiceCollectionClient from .plans.clients import PlanClient from .subscriptions.clients import SubscriptionClient @@ -118,6 +119,10 @@ def overdue_balances(self) -> OverdueBalanceClient: def payment_requests(self) -> PaymentRequestClient: return PaymentRequestClient(self.base_api_url, self.api_key) + @callable_cached_property + def payments(self) -> PaymentClient: + return PaymentClient(self.base_api_url, self.api_key) + @callable_cached_property def invoice_collections(self) -> InvoiceCollectionClient: return InvoiceCollectionClient(self.base_api_url, self.api_key) diff --git a/lago_python_client/models/__init__.py b/lago_python_client/models/__init__.py index e16e28f..868e5a1 100644 --- a/lago_python_client/models/__init__.py +++ b/lago_python_client/models/__init__.py @@ -91,4 +91,5 @@ UsageThresholdsResponse as UsageThresholdsResponse, ) from .payment_request import PaymentRequest as PaymentRequest +from .payment import Payment as Payment from .lifetime_usage import LifetimeUsageResponse as LifetimeUsageResponse diff --git a/lago_python_client/models/invoice.py b/lago_python_client/models/invoice.py index 88e4030..8c0ca81 100644 --- a/lago_python_client/models/invoice.py +++ b/lago_python_client/models/invoice.py @@ -124,6 +124,7 @@ class InvoiceResponse(BaseResponseModel): sub_total_excluding_taxes_amount_cents: int sub_total_including_taxes_amount_cents: int total_amount_cents: int + total_due_amount_cents: int prepaid_credit_amount_cents: int file_url: Optional[str] diff --git a/lago_python_client/models/payment.py b/lago_python_client/models/payment.py new file mode 100644 index 0000000..0335055 --- /dev/null +++ b/lago_python_client/models/payment.py @@ -0,0 +1,26 @@ +from typing import List, Optional + +from ..base_model import BaseModel, BaseResponseModel + + +class Payment(BaseModel): + invoice_id: str + amount_cents: int + reference: str + paid_at: Optional[str] + + +class PaymentResponse(BaseResponseModel): + lago_id: str + invoice_ids: List[str] + amount_cents: int + amount_currency: str + payment_status: str + type: str + reference: str + external_payment_id: Optional[str] + created_at: str + + +class PaymentsResponse(BaseResponseModel): + __root__: List[PaymentResponse] diff --git a/lago_python_client/payments/__init__.py b/lago_python_client/payments/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lago_python_client/payments/clients.py b/lago_python_client/payments/clients.py new file mode 100644 index 0000000..a7b0dff --- /dev/null +++ b/lago_python_client/payments/clients.py @@ -0,0 +1,16 @@ +from typing import ClassVar, Type + +from ..base_client import BaseClient +from ..mixins import CreateCommandMixin, FindAllCommandMixin, FindCommandMixin +from ..models.payment import PaymentResponse + + +class PaymentClient( + CreateCommandMixin[PaymentResponse], + FindAllCommandMixin[PaymentResponse], + FindCommandMixin[PaymentResponse], + BaseClient, +): + API_RESOURCE: ClassVar[str] = "payments" + RESPONSE_MODEL: ClassVar[Type[PaymentResponse]] = PaymentResponse + ROOT_NAME: ClassVar[str] = "payment" diff --git a/tests/fixtures/invoice.json b/tests/fixtures/invoice.json index dd253e8..fcc54e3 100644 --- a/tests/fixtures/invoice.json +++ b/tests/fixtures/invoice.json @@ -21,6 +21,7 @@ "sub_total_including_taxes_amount_cents": 120, "prepaid_credit_amount_cents": 0, "total_amount_cents": 120, + "total_due_amount_cents": 120, "metadata": [ { "key": "key", diff --git a/tests/fixtures/invoice_index.json b/tests/fixtures/invoice_index.json index 471f22d..f12cd1a 100644 --- a/tests/fixtures/invoice_index.json +++ b/tests/fixtures/invoice_index.json @@ -22,6 +22,7 @@ "sub_total_including_taxes_amount_cents": 120, "prepaid_credit_amount_cents": 0, "total_amount_cents": 120, + "total_due_amount_cents": 120, "applied_taxes": [ { "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", @@ -59,6 +60,7 @@ "sub_total_including_taxes_amount_cents": 120, "prepaid_credit_amount_cents": 0, "total_amount_cents": 120, + "total_due_amount_cents": 120, "applied_taxes": [ { "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", diff --git a/tests/fixtures/one_off_invoice.json b/tests/fixtures/one_off_invoice.json index 995cc3e..074c09d 100644 --- a/tests/fixtures/one_off_invoice.json +++ b/tests/fixtures/one_off_invoice.json @@ -21,6 +21,7 @@ "sub_total_including_taxes_amount_cents": 120, "prepaid_credit_amount_cents": 0, "total_amount_cents": 120, + "total_due_amount_cents": 120, "metadata": [ { "key": "key", diff --git a/tests/fixtures/payment.json b/tests/fixtures/payment.json new file mode 100644 index 0000000..9b006c5 --- /dev/null +++ b/tests/fixtures/payment.json @@ -0,0 +1,13 @@ +{ + "payment": { + "lago_id": "8e5d5ec2-bdc7-4c43-a944-5ababae775d4", + "invoice_ids": ["f8e194df-5d90-4382-b146-c881d2c67f28"], + "amount_cents": 100, + "amount_currency": "USD", + "payment_status": "succeeded", + "type": "manual", + "reference": "123", + "external_payment_id": null, + "created_at": "2025-02-18T18:31:13Z" + } +} diff --git a/tests/fixtures/payment_index.json b/tests/fixtures/payment_index.json new file mode 100644 index 0000000..e13679a --- /dev/null +++ b/tests/fixtures/payment_index.json @@ -0,0 +1,20 @@ +{ + "payments": [ + { + "lago_id": "8e5d5ec2-bdc7-4c43-a944-5ababae775d4", + "invoice_ids": [ + "ed267c66-8170-4d23-83e8-6d6e4fc735ef" + ], + "amount_cents": 100, + "amount_currency": "USD", + "payment_status": "succeeded", + "type": "manual", + "reference": "123", + "external_payment_id": null, + "created_at": "2025-02-18T18:31:13Z" + } + ], + "meta": { + "current_page": 1 + } +} diff --git a/tests/fixtures/payment_request_index.json b/tests/fixtures/payment_request_index.json index 6abaebd..987033d 100644 --- a/tests/fixtures/payment_request_index.json +++ b/tests/fixtures/payment_request_index.json @@ -57,7 +57,8 @@ "sub_total_excluding_taxes_amount_cents": 100, "sub_total_including_taxes_amount_cents": 120, "prepaid_credit_amount_cents": 0, - "total_amount_cents": 120 + "total_amount_cents": 120, + "total_due_amount_cents": 120 } ] } diff --git a/tests/test_payment_client.py b/tests/test_payment_client.py new file mode 100644 index 0000000..7d2c4c9 --- /dev/null +++ b/tests/test_payment_client.py @@ -0,0 +1,108 @@ +import os + +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 import Payment + + +def payment_object(): + return Payment( + amount_cents=100, + reference="ref1", + invoice_id="f8e194df-5d90-4382-b146-c881d2c67f28", + paid_at="2025-01-01T13:00:00Z" + ) + + +def mock_response(): + this_dir = os.path.dirname(os.path.abspath(__file__)) + data_path = os.path.join(this_dir, "fixtures/payment.json") + + with open(data_path, "rb") as payment_response: + return payment_response.read() + + +def mock_collection_response(): + current_dir = os.path.dirname(os.path.abspath(__file__)) + data_path = os.path.join(current_dir, "fixtures/payment_index.json") + + with open(data_path, "rb") as payments_response: + return payments_response.read() + + +def test_valid_find_all_payments_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/payments", + content=mock_collection_response(), + ) + response = client.payments.find_all() + + assert response["payments"][0].lago_id == "8e5d5ec2-bdc7-4c43-a944-5ababae775d4" + assert response["meta"]["current_page"] == 1 + + +def test_valid_find_all_payments_request_with_options(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/payments?per_page=2&page=1", + content=mock_collection_response(), + ) + response = client.payments.find_all({"per_page": 2, "page": 1}) + + assert response["payments"][0].lago_id == "8e5d5ec2-bdc7-4c43-a944-5ababae775d4" + assert response["meta"]["current_page"] == 1 + + +def test_invalid_find_all_payment_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/payments", + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.payments.find_all() + + +def test_valid_create_payment_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url="https://api.getlago.com/api/v1/payments", + content=mock_response(), + ) + response = client.payments.create(payment_object()) + + assert response is not None + assert response.lago_id == "8e5d5ec2-bdc7-4c43-a944-5ababae775d4" + assert response.amount_cents == 100 + assert response.amount_currency == "USD" + assert response.payment_status == "succeeded" + assert len(response.invoice_ids) == 1 + assert response.invoice_ids[0] == "f8e194df-5d90-4382-b146-c881d2c67f28" + + +def test_invalid_create_payment_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="POST", + url="https://api.getlago.com/api/v1/payments", + status_code=401, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.payments.create(payment_object())