From 8d24296dc27b02e89b70c11cc9bd6a197bdf95d9 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 11 Feb 2026 19:29:33 +0400 Subject: [PATCH 1/2] Add customer wallet alerts --- lago_python_client/customers/wallet_client.py | 5 + .../customers/wallets/alert_client.py | 28 +++ lago_python_client/models/alert.py | 11 +- tests/fixtures/wallet_alert.json | 26 +++ tests/fixtures/wallet_alert_index.json | 34 +++ tests/test_customer_wallet_alert_client.py | 194 ++++++++++++++++++ 6 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 lago_python_client/customers/wallets/alert_client.py create mode 100644 tests/fixtures/wallet_alert.json create mode 100644 tests/fixtures/wallet_alert_index.json create mode 100644 tests/test_customer_wallet_alert_client.py diff --git a/lago_python_client/customers/wallet_client.py b/lago_python_client/customers/wallet_client.py index d38a8c90..add31a48 100644 --- a/lago_python_client/customers/wallet_client.py +++ b/lago_python_client/customers/wallet_client.py @@ -11,6 +11,7 @@ ) from ..models.wallet import WalletResponse from .wallets.metadata_client import CustomerWalletMetadataClient +from .wallets.alert_client import CustomerWalletAlertClient class CustomerWalletClient( @@ -35,3 +36,7 @@ def api_resource(self, customer_id: str) -> tuple[str]: @callable_cached_property def metadata(self) -> CustomerWalletMetadataClient: return CustomerWalletMetadataClient(self.base_url, self.api_key) + + @callable_cached_property + def alerts(self) -> CustomerWalletAlertClient: + return CustomerWalletAlertClient(self.base_url, self.api_key) diff --git a/lago_python_client/customers/wallets/alert_client.py b/lago_python_client/customers/wallets/alert_client.py new file mode 100644 index 00000000..30ef6f80 --- /dev/null +++ b/lago_python_client/customers/wallets/alert_client.py @@ -0,0 +1,28 @@ +from typing import ClassVar, Type + +from ...base_client import BaseClient +from ...models.alert import AlertResponse + +from ...mixins import ( + NestedCreateCommandMixin, + NestedUpdateCommandMixin, + NestedDestroyCommandMixin, + NestedFindCommandMixin, + NestedFindAllCommandMixin, +) + + +class CustomerWalletAlertClient( + NestedCreateCommandMixin[AlertResponse], + NestedUpdateCommandMixin[AlertResponse], + NestedDestroyCommandMixin[AlertResponse], + NestedFindCommandMixin[AlertResponse], + NestedFindAllCommandMixin[AlertResponse], + BaseClient, +): + API_RESOURCE: ClassVar[str] = "alerts" + RESPONSE_MODEL: ClassVar[Type[AlertResponse]] = AlertResponse + ROOT_NAME: ClassVar[str] = "alert" + + def api_resource(self, customer_id: str, wallet_code: str) -> tuple[str]: + return ("customers", customer_id, "wallets", wallet_code, "alerts") diff --git a/lago_python_client/models/alert.py b/lago_python_client/models/alert.py index 3add5117..eacd1845 100644 --- a/lago_python_client/models/alert.py +++ b/lago_python_client/models/alert.py @@ -15,11 +15,11 @@ class AlertThresholdList(BaseModel): class Alert(BaseModel): - alert_type: str - code: str + alert_type: Optional[str] + code: Optional[str] name: Optional[str] + thresholds: Optional[AlertThresholdList] billable_metric_code: Optional[str] - thresholds: AlertThresholdList class AlertsList(BaseModel): @@ -39,10 +39,13 @@ class AlertThresholdResponseList(BaseResponseModel): class AlertResponse(BaseResponseModel): lago_id: str lago_organization_id: str - external_subscription_id: str + external_subscription_id: Optional[str] + lago_wallet_id: Optional[str] + wallet_code: Optional[str] alert_type: str code: str name: Optional[str] + direction: Optional[str] previous_value: Optional[str] last_processed_at: Optional[str] thresholds: AlertThresholdResponseList diff --git a/tests/fixtures/wallet_alert.json b/tests/fixtures/wallet_alert.json new file mode 100644 index 00000000..ab7e76c2 --- /dev/null +++ b/tests/fixtures/wallet_alert.json @@ -0,0 +1,26 @@ +{ + "alert": { + "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "external_subscription_id": null, + "external_customer_id": "customer_id", + "lago_wallet_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "wallet_code": "wallet_code", + "code": "wallet_balance_alert", + "name": "Balance Amount Alert", + "alert_type": "wallet_balance_amount", + "direction": "increasing", + "previous_value": 1000, + "thresholds": [ + { + "code": "warn", + "value": 10000, + "recurring": false + } + ], + "billable_metric": null, + "created_at": "2025-03-20T10:00:00Z", + "last_processed_at": "2025-05-19T10:04:21Z" + } +} + diff --git a/tests/fixtures/wallet_alert_index.json b/tests/fixtures/wallet_alert_index.json new file mode 100644 index 00000000..acc1c613 --- /dev/null +++ b/tests/fixtures/wallet_alert_index.json @@ -0,0 +1,34 @@ +{ + "alerts": [ + { + "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "external_subscription_id": null, + "external_customer_id": "cus_0987654321", + "lago_wallet_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "wallet_code": "wallet_code", + "code": "wallet_balance_alert", + "name": "Balance Amount Alert", + "alert_type": "wallet_balance_amount", + "direction": "increasing", + "previous_value": 1000, + "thresholds": [ + { + "code": "warn", + "recurring": false, + "value": "99.0" + } + ], + "billable_metric": null, + "created_at": "2025-03-20T10:00:00Z", + "last_processed_at": "2025-05-19T10:04:21Z" + } + ], + "meta": { + "current_page": 1, + "next_page": 2, + "prev_page": null, + "total_pages": 4, + "total_count": 70 + } +} diff --git a/tests/test_customer_wallet_alert_client.py b/tests/test_customer_wallet_alert_client.py new file mode 100644 index 00000000..5da936a5 --- /dev/null +++ b/tests/test_customer_wallet_alert_client.py @@ -0,0 +1,194 @@ +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 ( + Alert, + AlertThreshold, + AlertThresholdList, +) + +from .utils.mixin import mock_response + + +def alert_object(): + threshold = AlertThreshold(code="warn", value=10000) + + return Alert( + alert_type="wallet_balance_amount", + code="wallet_balance_alert", + name="Balance Amount Alert", + thresholds=AlertThresholdList(__root__=[threshold]), + ) + + +def test_valid_create_customer_wallet_alert_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/customers/customer_id/wallets/wallet_code/alerts", + content=mock_response("wallet_alert"), + ) + response = client.customers.wallets.alerts.create("customer_id", "wallet_code", alert_object()) + + assert response.lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response.lago_organization_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response.external_subscription_id is None + assert response.lago_wallet_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response.wallet_code == "wallet_code" + assert response.code == "wallet_balance_alert" + assert response.name == "Balance Amount Alert" + assert response.alert_type == "wallet_balance_amount" + assert response.direction == "increasing" + assert response.previous_value == "1000" + assert response.thresholds == AlertThresholdList( + __root__=[AlertThreshold(code="warn", value=10000, recurring=False)] + ) + assert response.billable_metric is None + + +def test_invalid_create_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="POST", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts", + status_code=401, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.create("customer_id", "wallet_code", alert_object()) + + +def test_valid_update_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + code = "alert-code" + + httpx_mock.add_response( + method="PUT", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + content=mock_response("wallet_alert"), + ) + response = client.customers.wallets.alerts.update("customer_id", "wallet_code", code, alert_object()) + + assert response.lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + + +def test_invalid_update_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + code = "invalid" + + httpx_mock.add_response( + method="PUT", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + status_code=401, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.update("customer_id", "wallet_code", code, alert_object()) + + +def test_valid_find_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + code = "alert-code" + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + content=mock_response("wallet_alert"), + ) + response = client.customers.wallets.alerts.find("customer_id", "wallet_code", code) + + assert response.lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + + +def test_invalid_find_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + code = "invalid" + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.find("customer_id", "wallet_code", code) + + +def test_valid_destroy_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + code = "alert-code" + + httpx_mock.add_response( + method="DELETE", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + content=mock_response("wallet_alert"), + ) + response = client.customers.wallets.alerts.destroy("customer_id", "wallet_code", code) + + assert response.lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + + +def test_invalid_destroy_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + code = "invalid" + + httpx_mock.add_response( + method="DELETE", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.destroy("customer_id", "wallet_code", code) + + +def test_valid_find_all_customer_wallet_alerts_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/customers/customer_id/wallets/wallet_code/alerts", + content=mock_response("wallet_alert_index"), + ) + response = client.customers.wallets.alerts.find_all("customer_id", "wallet_code") + + assert response["alerts"][0].lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response["meta"]["current_page"] == 1 + + +def test_valid_find_all_customer_wallet_alerts_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/customers/customer_id/wallets/wallet_code/alerts?page=1&per_page=2", + content=mock_response("wallet_alert_index"), + ) + response = client.customers.wallets.alerts.find_all( + "customer_id", "wallet_code", options={"per_page": 2, "page": 1} + ) + + assert response["alerts"][0].lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response["meta"]["current_page"] == 1 + + +def test_invalid_find_all_wallet_alerts_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts", + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.find_all("customer_id", "wallet_code") From 45f69199c075049b74283376ce58c4a83fa953f2 Mon Sep 17 00:00:00 2001 From: Julia Egorova Date: Thu, 12 Mar 2026 19:40:20 +0400 Subject: [PATCH 2/2] feat(alerting): Add batch actions for wallet alerts (#372) * Add batch actions for wallet alerts * Fix linters --- lago_python_client/customers/wallet_client.py | 2 +- .../customers/wallets/alert_client.py | 44 +++++++++- tests/fixtures/wallet_alert_index.json | 23 +++++ tests/test_customer_wallet_alert_client.py | 85 +++++++++++++++++++ 4 files changed, 149 insertions(+), 5 deletions(-) diff --git a/lago_python_client/customers/wallet_client.py b/lago_python_client/customers/wallet_client.py index add31a48..642b06cd 100644 --- a/lago_python_client/customers/wallet_client.py +++ b/lago_python_client/customers/wallet_client.py @@ -10,8 +10,8 @@ NestedUpdateCommandMixin, ) from ..models.wallet import WalletResponse -from .wallets.metadata_client import CustomerWalletMetadataClient from .wallets.alert_client import CustomerWalletAlertClient +from .wallets.metadata_client import CustomerWalletMetadataClient class CustomerWalletClient( diff --git a/lago_python_client/customers/wallets/alert_client.py b/lago_python_client/customers/wallets/alert_client.py index 30ef6f80..19558596 100644 --- a/lago_python_client/customers/wallets/alert_client.py +++ b/lago_python_client/customers/wallets/alert_client.py @@ -1,14 +1,26 @@ from typing import ClassVar, Type from ...base_client import BaseClient -from ...models.alert import AlertResponse - from ...mixins import ( NestedCreateCommandMixin, - NestedUpdateCommandMixin, NestedDestroyCommandMixin, - NestedFindCommandMixin, NestedFindAllCommandMixin, + NestedFindCommandMixin, + NestedUpdateCommandMixin, +) +from ...models.alert import AlertResponse, AlertsList +from ...services.json import to_json +from ...services.request import ( + make_headers, + make_url, + send_delete_request, + send_post_request, +) +from ...services.response import ( + Response, + get_response_data, + prepare_index_response, + verify_response, ) @@ -26,3 +38,27 @@ class CustomerWalletAlertClient( def api_resource(self, customer_id: str, wallet_code: str) -> tuple[str]: return ("customers", customer_id, "wallets", wallet_code, "alerts") + + def create_batch(self, customer_id: str, wallet_code: str, input_object: AlertsList) -> None: + api_response: Response = send_post_request( + url=make_url(origin=self.base_url, path_parts=self.api_resource(customer_id, wallet_code)), + content=to_json(input_object.dict()), + headers=make_headers(api_key=self.api_key), + ) + + return prepare_index_response( + api_resource="alerts", + response_model=AlertResponse, + data=get_response_data(response=api_response), + ) + + def destroy_all(self, customer_id: str, wallet_code: str) -> None: + api_response: Response = send_delete_request( + url=make_url( + origin=self.base_url, + path_parts=self.api_resource(customer_id, wallet_code), + ), + headers=make_headers(api_key=self.api_key), + ) + + verify_response(api_response) diff --git a/tests/fixtures/wallet_alert_index.json b/tests/fixtures/wallet_alert_index.json index acc1c613..5e515913 100644 --- a/tests/fixtures/wallet_alert_index.json +++ b/tests/fixtures/wallet_alert_index.json @@ -22,6 +22,29 @@ "billable_metric": null, "created_at": "2025-03-20T10:00:00Z", "last_processed_at": "2025-05-19T10:04:21Z" + }, + { + "lago_id": "1a921a92-1a92-1a92-1a92-1a921a921a92", + "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "external_subscription_id": null, + "external_customer_id": "cus_0987654321", + "lago_wallet_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "wallet_code": "wallet_code", + "code": "wallet_credits_alert", + "name": "Credits Balance Alert", + "alert_type": "wallet_credits_balance", + "direction": "increasing", + "previous_value": 1000, + "thresholds": [ + { + "code": "warn", + "recurring": false, + "value": "99.0" + } + ], + "billable_metric": null, + "created_at": "2025-03-20T10:00:00Z", + "last_processed_at": "2025-05-19T10:04:21Z" } ], "meta": { diff --git a/tests/test_customer_wallet_alert_client.py b/tests/test_customer_wallet_alert_client.py index 5da936a5..ed6a0320 100644 --- a/tests/test_customer_wallet_alert_client.py +++ b/tests/test_customer_wallet_alert_client.py @@ -5,6 +5,7 @@ from lago_python_client.exceptions import LagoApiError from lago_python_client.models import ( Alert, + AlertsList, AlertThreshold, AlertThresholdList, ) @@ -176,6 +177,7 @@ def test_valid_find_all_customer_wallet_alerts_request_with_options(httpx_mock: "customer_id", "wallet_code", options={"per_page": 2, "page": 1} ) + assert len(response) == 2 assert response["alerts"][0].lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" assert response["meta"]["current_page"] == 1 @@ -192,3 +194,86 @@ def test_invalid_find_all_wallet_alerts_request(httpx_mock: HTTPXMock): with pytest.raises(LagoApiError): client.customers.wallets.alerts.find_all("customer_id", "wallet_code") + + +def test_valid_create_batch_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/customers/customer_id/wallets/wallet_code/alerts", + content=mock_response("wallet_alert_index"), + ) + + input = AlertsList( + alerts=[ + Alert( + alert_type="wallet_balance_amount", + code="wallet_balance_alert", + name="Balance Amount Alert", + thresholds=[AlertThreshold(code="warn", value="1000")], + ), + Alert( + alert_type="wallet_credits_balance", + code="wallet_credits_alert", + name="Credits Balance Alert", + thresholds=[AlertThreshold(value="2000")], + ), + ] + ) + + response = client.customers.wallets.alerts.create_batch("customer_id", "wallet_code", input) + + assert len(response["alerts"]) == 2 + + assert response["alerts"][0].lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response["alerts"][0].wallet_code == "wallet_code" + assert response["alerts"][0].code == "wallet_balance_alert" + assert response["alerts"][0].alert_type == "wallet_balance_amount" + + assert response["alerts"][1].lago_id == "1a921a92-1a92-1a92-1a92-1a921a921a92" + assert response["alerts"][1].wallet_code == "wallet_code" + assert response["alerts"][1].code == "wallet_credits_alert" + assert response["alerts"][1].alert_type == "wallet_credits_balance" + + +def test_invalid_create_batch_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="POST", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts", + status_code=401, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.create_batch("customer_id", "wallet_code", AlertsList(alerts=[])) + + +def test_valid_destroy_all_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="DELETE", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts", + status_code=200, + content=b"", + ) + + response = client.customers.wallets.alerts.destroy_all("customer_id", "wallet_code") + assert response is None + + +def test_invalid_destroy_all_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="DELETE", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts", + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.destroy_all("customer_id", "wallet_code")