From 8395205e82ca0b1a7334e02acf303eede9b6c5f8 Mon Sep 17 00:00:00 2001 From: "lago-claude-ai-agent[bot]" <297187938+lago-claude-ai-agent[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:37:58 +0000 Subject: [PATCH] fix(fixed-charge): Accept null invoice_display_name in responses ## Context Updating, finding or listing a fixed charge that has no invoice display name raises a pydantic validation error in the client. The error reads `none is not an allowed value` for `invoice_display_name` on `FixedChargeResponse`. For an update, the API has already applied the change, so the caller sees a failure for a request that worked. The API returns `"invoice_display_name": null` for these fixed charges, because the field is optional and its schema allows null. The client model declared the field as a required `str`, so it rejected the null. ## Description `FixedChargeResponse.invoice_display_name` is now `Optional[str]`. A response with a null value parses and the attribute is `None`. Non-null values parse as before. This matches the other optional fields on the same model, such as `code`, and the `invoice_display_name` field on the other response models. The model is shared by the subscription fixed charges client, the plan fixed charges client and `PlanResponse.fixed_charges`, so one change covers all three. Request models already accepted a missing display name and are unchanged. The loop took no decisions of its own beyond this field type. The tests are outside the model file. They cover a null display name on the subscription fixed charge update and list calls, the plan fixed charge find call and the plan find call. The change was also checked against a running Lago API v1.55.0 server. The ticket's case fails without the fix and passes with it. Fixes BIL-995 Signed-off-by: lago-claude-ai-agent[bot] <297187938+lago-claude-ai-agent[bot]@users.noreply.github.com> --- lago_python_client/models/fixed_charge.py | 2 +- tests/test_plan_client.py | 39 +++++++++++++++++++++++ tests/test_subscription_client.py | 39 +++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lago_python_client/models/fixed_charge.py b/lago_python_client/models/fixed_charge.py index 72955d7c..d1dfa372 100644 --- a/lago_python_client/models/fixed_charge.py +++ b/lago_python_client/models/fixed_charge.py @@ -52,7 +52,7 @@ class FixedChargePropertiesResponse(BaseResponseModel): class FixedChargeResponse(BaseResponseModel): lago_id: str lago_add_on_id: str - invoice_display_name: str + invoice_display_name: Optional[str] add_on_code: str created_at: str code: Optional[str] diff --git a/tests/test_plan_client.py b/tests/test_plan_client.py index 3425dad8..4de9a62c 100644 --- a/tests/test_plan_client.py +++ b/tests/test_plan_client.py @@ -1,3 +1,4 @@ +import json import os import pytest @@ -290,6 +291,26 @@ def test_valid_find_plan_request(httpx_mock: HTTPXMock): assert response.minimum_commitment.amount_cents == 1000 +def test_valid_find_plan_request_with_fixed_charge_without_invoice_display_name(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + code = "plan_code" + plan_body = json.loads(mock_response()) + fixed_charge = json.loads(mock_plan_fixed_charge_response())["fixed_charge"] + fixed_charge["invoice_display_name"] = None + plan_body["plan"]["fixed_charges"] = [fixed_charge] + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/plans/" + code, + json=plan_body, + ) + response = client.plans.find(code) + + assert response.invoice_display_name == "test plan 1" + assert response.fixed_charges.__root__[0].lago_id == "fc901a90-1a90-1a90-1a90-1a901a901a90" + assert response.fixed_charges.__root__[0].invoice_display_name is None + + def test_invalid_find_plan_request(httpx_mock: HTTPXMock): client = Client(api_key="invalid") code = "invalid" @@ -643,6 +664,24 @@ def test_valid_find_fixed_charge_request(httpx_mock: HTTPXMock): assert response.properties.amount == "500" +def test_valid_find_fixed_charge_request_without_invoice_display_name(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + plan_code = "plan_code" + fixed_charge_code = "fixed_setup" + body = json.loads(mock_plan_fixed_charge_response()) + body["fixed_charge"]["invoice_display_name"] = None + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/plans/" + plan_code + "/fixed_charges/" + fixed_charge_code, + json=body, + ) + response = client.plan_fixed_charges.find(plan_code, fixed_charge_code) + + assert response.lago_id == "fc901a90-1a90-1a90-1a90-1a901a901a90" + assert response.invoice_display_name is None + + def test_valid_create_fixed_charge_request(httpx_mock: HTTPXMock): client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") plan_code = "plan_code" diff --git a/tests/test_subscription_client.py b/tests/test_subscription_client.py index 57551746..429769a7 100644 --- a/tests/test_subscription_client.py +++ b/tests/test_subscription_client.py @@ -1,3 +1,4 @@ +import json import os import httpx @@ -460,6 +461,25 @@ def test_valid_find_all_fixed_charges_request(httpx_mock: HTTPXMock): assert response["meta"]["current_page"] == 1 +def test_valid_find_all_fixed_charges_request_without_invoice_display_name(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + external_id = "sub_external_123" + body = json.loads(mock_fixed_charges_response()) + body["fixed_charges"][0]["invoice_display_name"] = None + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/subscriptions/" + external_id + "/fixed_charges", + json=body, + ) + response = client.subscription_fixed_charges.find_all(external_id) + + assert len(response["fixed_charges"]) == 2 + assert response["fixed_charges"][0].lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response["fixed_charges"][0].invoice_display_name is None + assert response["meta"]["current_page"] == 1 + + def test_invalid_find_all_fixed_charges_request(httpx_mock: HTTPXMock): client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") external_id = "invalid_sub" @@ -741,6 +761,25 @@ def test_valid_update_fixed_charge_request(httpx_mock: HTTPXMock): assert response.lago_id == "fc901a90-1a90-1a90-1a90-1a901a901a90" +def test_valid_update_fixed_charge_request_without_invoice_display_name(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + external_id = "sub_external_123" + fixed_charge_code = "fixed_setup" + response_body = json.loads(mock_subscription_fixed_charge_response()) + response_body["fixed_charge"]["invoice_display_name"] = None + + httpx_mock.add_response( + method="PUT", + url="https://api.getlago.com/api/v1/subscriptions/" + external_id + "/fixed_charges/" + fixed_charge_code, + json=response_body, + ) + fixed_charge = FixedCharge(units=20, apply_units_immediately=True) + response = client.subscription_fixed_charges.update(external_id, fixed_charge_code, fixed_charge) + + assert response.lago_id == "fc901a90-1a90-1a90-1a90-1a901a901a90" + assert response.invoice_display_name is None + + # --- Charge Filters ---