diff --git a/lago_python_client/customers/clients.py b/lago_python_client/customers/clients.py index 1b8e856..41573f0 100644 --- a/lago_python_client/customers/clients.py +++ b/lago_python_client/customers/clients.py @@ -47,6 +47,7 @@ def current_usage( filter_by_charge_id: Optional[str] = None, filter_by_charge_code: Optional[str] = None, filter_by_group: Optional[dict] = None, + filter_by_presentation: Optional[Union[str, list[str]]] = None, full_usage: Optional[bool] = None, charge_id: Optional[str] = None, charge_code: Optional[str] = None, @@ -76,6 +77,12 @@ def current_usage( if filter_by_group is not None: warnings.warn("filter_by_group is deprecated, use group instead", DeprecationWarning, stacklevel=2) query_params["filter_by_group"] = json.dumps(filter_by_group) + if filter_by_presentation is not None: + query_params["filter_by_presentation"] = ( + filter_by_presentation + if isinstance(filter_by_presentation, str) + else json.dumps(filter_by_presentation) + ) if full_usage is not None: query_params["full_usage"] = str(full_usage).lower() diff --git a/lago_python_client/models/__init__.py b/lago_python_client/models/__init__.py index fe1cca3..fbd141a 100644 --- a/lago_python_client/models/__init__.py +++ b/lago_python_client/models/__init__.py @@ -138,6 +138,12 @@ from .customer_usage import ( Metric as Metric, ) +from .customer_usage import ( + PresentationBreakdown as PresentationBreakdown, +) +from .customer_usage import ( + PresentationBreakdowns as PresentationBreakdowns, +) from .event import BatchEvent as BatchEvent from .event import Event as Event from .fee import Fee as Fee diff --git a/lago_python_client/models/customer_projected_usage.py b/lago_python_client/models/customer_projected_usage.py index 05c68fd..417b696 100644 --- a/lago_python_client/models/customer_projected_usage.py +++ b/lago_python_client/models/customer_projected_usage.py @@ -3,6 +3,7 @@ from lago_python_client.base_model import BaseModel from ..base_model import BaseResponseModel +from .customer_usage import PresentationBreakdowns class Metric(BaseModel): @@ -27,6 +28,8 @@ class ProjectedChargeFilterUsage(BaseModel): projected_amount_cents: int events_count: int pricing_unit_details: Optional[PricingUnitDetails] + presentation_breakdowns: Optional[PresentationBreakdowns] + projected_presentation_breakdowns: Optional[PresentationBreakdowns] class ChargeObject(BaseModel): @@ -44,6 +47,8 @@ class ProjectedGroupedUsage(BaseModel): grouped_by: Dict[str, Optional[str]] filters: List[ProjectedChargeFilterUsage] pricing_unit_details: Optional[PricingUnitDetails] + presentation_breakdowns: Optional[PresentationBreakdowns] + projected_presentation_breakdowns: Optional[PresentationBreakdowns] class ProjectedChargeUsage(BaseModel): @@ -58,6 +63,8 @@ class ProjectedChargeUsage(BaseModel): filters: List[ProjectedChargeFilterUsage] grouped_usage: Optional[List[ProjectedGroupedUsage]] pricing_unit_details: Optional[PricingUnitDetails] + presentation_breakdowns: Optional[PresentationBreakdowns] + projected_presentation_breakdowns: Optional[PresentationBreakdowns] class CustomerProjectedUsageResponse(BaseResponseModel): diff --git a/lago_python_client/models/customer_usage.py b/lago_python_client/models/customer_usage.py index 249b670..78bf752 100644 --- a/lago_python_client/models/customer_usage.py +++ b/lago_python_client/models/customer_usage.py @@ -18,6 +18,15 @@ class PricingUnitDetails(BaseModel): conversion_rate: float +class PresentationBreakdown(BaseModel): + presentation_by: Dict[str, str] + units: str + + +class PresentationBreakdowns(BaseModel): + __root__: List[PresentationBreakdown] + + class ChargeFilterUsage(BaseModel): invoice_display_name: Optional[str] values: Optional[Dict[str, List[str]]] @@ -26,6 +35,7 @@ class ChargeFilterUsage(BaseModel): amount_cents: int events_count: int pricing_unit_details: Optional[PricingUnitDetails] + presentation_breakdowns: Optional[PresentationBreakdowns] class ChargeObject(BaseModel): @@ -42,6 +52,7 @@ class GroupedUsage(BaseModel): grouped_by: Dict[str, Optional[str]] filters: List[ChargeFilterUsage] pricing_unit_details: Optional[PricingUnitDetails] + presentation_breakdowns: Optional[PresentationBreakdowns] class ChargeUsage(BaseModel): @@ -55,6 +66,7 @@ class ChargeUsage(BaseModel): filters: List[ChargeFilterUsage] grouped_usage: Optional[List[GroupedUsage]] pricing_unit_details: Optional[PricingUnitDetails] + presentation_breakdowns: Optional[PresentationBreakdowns] class CustomerUsageResponse(BaseResponseModel): diff --git a/lago_python_client/models/fee.py b/lago_python_client/models/fee.py index a061f8d..bf25923 100644 --- a/lago_python_client/models/fee.py +++ b/lago_python_client/models/fee.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional from ..base_model import BaseModel, BaseResponseModel +from .customer_usage import PresentationBreakdowns from .invoice_item import InvoiceItemResponse @@ -75,6 +76,7 @@ class FeeResponse(BaseResponseModel): to_date: Optional[str] amount_details: Optional[Dict[str, Any]] pricing_unit_details: Optional[PricingUnitDetails] + presentation_breakdowns: Optional[PresentationBreakdowns] billing_entity_code: Optional[str] item: Optional[InvoiceItemResponse] diff --git a/tests/fixtures/charge.json b/tests/fixtures/charge.json index 27d19f1..7c5a4ec 100644 --- a/tests/fixtures/charge.json +++ b/tests/fixtures/charge.json @@ -12,7 +12,15 @@ "min_amount_cents": 0, "prorated": false, "properties": { - "amount": "0.22" + "amount": "0.22", + "presentation_group_keys": [ + { + "value": "region", + "options": { + "display_in_invoice": true + } + } + ] }, "filters": [], "taxes": [], diff --git a/tests/fixtures/customer_past_usage.json b/tests/fixtures/customer_past_usage.json index 34554b1..3099b70 100644 --- a/tests/fixtures/customer_past_usage.json +++ b/tests/fixtures/customer_past_usage.json @@ -46,7 +46,23 @@ "amount_cents": 123, "short_name": "CR", "conversion_rate": 1.0 - } + }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ] + } + ], + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "3.0" } ] } diff --git a/tests/fixtures/customer_projected_usage.json b/tests/fixtures/customer_projected_usage.json index 235f1ba..ffc9ad9 100644 --- a/tests/fixtures/customer_projected_usage.json +++ b/tests/fixtures/customer_projected_usage.json @@ -49,7 +49,23 @@ "amount_cents": 123, "short_name": "CR", "conversion_rate": 1.0 - } + }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ], + "projected_presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "4.0" + } + ] } ], "grouped_usage": [ @@ -67,6 +83,22 @@ "short_name": "CR", "conversion_rate": 1.2 }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "operations" + }, + "units": "1.0" + } + ], + "projected_presentation_breakdowns": [ + { + "presentation_by": { + "team": "operations" + }, + "units": "2.0" + } + ], "filters": [ { "units": "3.0", @@ -79,10 +111,42 @@ "france" ] }, - "pricing_unit_details": null + "pricing_unit_details": null, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "support" + }, + "units": "1.0" + } + ], + "projected_presentation_breakdowns": [ + { + "presentation_by": { + "team": "support" + }, + "units": "2.0" + } + ] } ] } + ], + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "3.0" + } + ], + "projected_presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "6.0" + } ] } ] diff --git a/tests/fixtures/customer_usage.json b/tests/fixtures/customer_usage.json index cf07f71..a0ad50d 100644 --- a/tests/fixtures/customer_usage.json +++ b/tests/fixtures/customer_usage.json @@ -46,7 +46,15 @@ "amount_cents": 123, "short_name": "CR", "conversion_rate": 1.0 - } + }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "2.0" + } + ] } ], "grouped_usage": [ @@ -63,6 +71,14 @@ "short_name": "CR", "conversion_rate": 1.2 }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "operations" + }, + "units": "1.0" + } + ], "filters": [ { "units": "3.0", @@ -74,10 +90,26 @@ "france" ] }, - "pricing_unit_details": null + "pricing_unit_details": null, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "support" + }, + "units": "1.0" + } + ] } ] } + ], + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "3.0" + } ] } ] diff --git a/tests/fixtures/fee.json b/tests/fixtures/fee.json index 97a0105..8983ba5 100644 --- a/tests/fixtures/fee.json +++ b/tests/fixtures/fee.json @@ -39,6 +39,14 @@ "precise_unit_amount": "12.00", "conversion_rate": 1.0 }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "10.0" + } + ], "applied_taxes": [ { "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", diff --git a/tests/fixtures/fees.json b/tests/fixtures/fees.json index 0586c04..86c9896 100644 --- a/tests/fixtures/fees.json +++ b/tests/fixtures/fees.json @@ -38,6 +38,14 @@ "precise_unit_amount": "120.00", "conversion_rate": 1.0 }, + "presentation_breakdowns": [ + { + "presentation_by": { + "team": "engineering" + }, + "units": "1.0" + } + ], "applied_taxes": [ { "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", diff --git a/tests/fixtures/plan.json b/tests/fixtures/plan.json index 3b030d9..1423c64 100644 --- a/tests/fixtures/plan.json +++ b/tests/fixtures/plan.json @@ -27,7 +27,15 @@ "min_amount_cents": 0, "properties": { "amount": "0.22", - "pricing_group_keys": ["agent_name"] + "pricing_group_keys": ["agent_name"], + "presentation_group_keys": [ + { + "value": "region", + "options": { + "display_in_invoice": true + } + } + ] }, "filters": [ { diff --git a/tests/fixtures/plan_index.json b/tests/fixtures/plan_index.json index 3198f0b..e756a72 100644 --- a/tests/fixtures/plan_index.json +++ b/tests/fixtures/plan_index.json @@ -24,6 +24,16 @@ "charge_model": "standard", "pay_in_advance": true, "invoiceable": true, + "properties": { + "presentation_group_keys": [ + { + "value": "region", + "options": { + "display_in_invoice": true + } + } + ] + }, "filters": [ { "properties": { diff --git a/tests/test_customer_client.py b/tests/test_customer_client.py index 416d72e..4ce3564 100644 --- a/tests/test_customer_client.py +++ b/tests/test_customer_client.py @@ -134,6 +134,11 @@ def test_valid_current_usage(httpx_mock: HTTPXMock): assert response.charges_usage[0].units == "1.0" assert len(response.charges_usage[0].filters) == 1 assert response.charges_usage[0].filters[0].values["country"] == ["france"] + charge_usage = response.charges_usage[0] + assert charge_usage.presentation_breakdowns.__root__[0].presentation_by["team"] == "engineering" + assert charge_usage.presentation_breakdowns.__root__[0].units == "3.0" + assert charge_usage.filters[0].presentation_breakdowns.__root__[0].units == "2.0" + assert charge_usage.grouped_usage[0].presentation_breakdowns.__root__[0].units == "1.0" def test_valid_projected_usage(httpx_mock: HTTPXMock): @@ -157,6 +162,11 @@ def test_valid_projected_usage(httpx_mock: HTTPXMock): assert response.charges_usage[0].charge.charge_model == "graduated" assert response.charges_usage[0].charge.invoice_display_name == "add_on_invoice_display_name" assert response.charges_usage[0].billable_metric.lago_id == "99a6094e-199b-4101-896a-54e927ce7bd7" + charge_usage = response.charges_usage[0] + assert charge_usage.presentation_breakdowns.__root__[0].units == "3.0" + assert charge_usage.projected_presentation_breakdowns.__root__[0].units == "6.0" + assert charge_usage.filters[0].projected_presentation_breakdowns.__root__[0].units == "4.0" + assert charge_usage.grouped_usage[0].projected_presentation_breakdowns.__root__[0].units == "2.0" def test_valid_current_usage_without_taxes(httpx_mock: HTTPXMock): @@ -216,6 +226,7 @@ def test_valid_current_usage_with_new_filters(httpx_mock: HTTPXMock): "&charge_code=storage" "&billable_metric_code=storage" "&group%5Bcloud%5D=aws" + "&filter_by_presentation=%5B%22engineering%22%2C+%22operations%22%5D" "&full_usage=true" ), content=mock_response("customer_usage"), @@ -227,6 +238,7 @@ def test_valid_current_usage_with_new_filters(httpx_mock: HTTPXMock): charge_code="storage", billable_metric_code="storage", group={"cloud": "aws"}, + filter_by_presentation=["engineering", "operations"], full_usage=True, ) @@ -263,6 +275,8 @@ def test_valid_past_usage(httpx_mock: HTTPXMock): assert len(response["usage_periods"][0].charges_usage) == 1 assert response["usage_periods"][0].charges_usage[0].units == "1.0" assert len(response["usage_periods"][0].charges_usage[0].filters) == 1 + past_charge_usage = response["usage_periods"][0].charges_usage[0] + assert past_charge_usage.presentation_breakdowns.__root__[0].units == "3.0" def test_invalid_past_usage(httpx_mock: HTTPXMock): diff --git a/tests/test_fee_client.py b/tests/test_fee_client.py index 1aaf36b..fe37504 100644 --- a/tests/test_fee_client.py +++ b/tests/test_fee_client.py @@ -29,6 +29,8 @@ def test_valid_find_fee_request(httpx_mock: HTTPXMock): assert response.lago_id == identifier assert response.invoice_display_name == fee_invoice_display_name assert response.item.invoice_display_name == charge_invoice_display_name + assert response.presentation_breakdowns.__root__[0].presentation_by["team"] == "engineering" + assert response.presentation_breakdowns.__root__[0].units == "10.0" def test_fee_response_with_fixed_charge_id(): diff --git a/tests/test_plan_client.py b/tests/test_plan_client.py index 5a13d69..c8a65f4 100644 --- a/tests/test_plan_client.py +++ b/tests/test_plan_client.py @@ -25,7 +25,14 @@ def plan_object(): id=None, invoice_display_name=None, regroup_paid_fees=None, - properties=None, + properties={ + "presentation_group_keys": [ + { + "value": "region", + "options": {"display_in_invoice": True}, + } + ] + }, tax_codes=None, billable_metric_id="id", charge_model="standard", @@ -194,6 +201,8 @@ def test_valid_create_plan_request(httpx_mock: HTTPXMock): assert response.code == "plan_code" assert response.invoice_display_name == "test plan 1" assert response.charges.__root__[0].invoice_display_name == "Setup" + charge = response.charges.__root__[0] + assert charge.properties["presentation_group_keys"][0]["value"] == "region" assert response.minimum_commitment.invoice_display_name == "Minimum commitment (C1)" assert response.usage_thresholds.__root__[0].threshold_display_name == "Threshold 1" assert response.metadata == {"key1": "value1", "key2": None} @@ -276,6 +285,8 @@ def test_valid_find_plan_request(httpx_mock: HTTPXMock): assert response.invoice_display_name == "test plan 1" assert response.charges.__root__[0].charge_model == "standard" assert response.charges.__root__[0].min_amount_cents == 0 + charge = response.charges.__root__[0] + assert charge.properties["presentation_group_keys"][0]["value"] == "region" assert response.minimum_commitment.amount_cents == 1000 @@ -338,6 +349,8 @@ def test_valid_find_all_plan_request(httpx_mock: HTTPXMock): assert response["plans"][0].invoice_display_name == "test plan 1" assert response["plans"][0].minimum_commitment.invoice_display_name == "Minimum commitment (C2)" assert response["plans"][0].charges.__root__[0].lago_id == "51c1e851-5be6-4343-a0ee-39a81d8b4ee1" + plan_charge = response["plans"][0].charges.__root__[0] + assert plan_charge.properties["presentation_group_keys"][0]["value"] == "region" assert response["plans"][0].charges.__root__[0].filters.__root__[0].properties["amount"] == "0.22" assert response["plans"][0].charges.__root__[0].filters.__root__[0].invoice_display_name == "Europe" assert response["meta"]["current_page"] == 1 @@ -506,6 +519,9 @@ def test_valid_find_charge_request(httpx_mock: HTTPXMock): assert response.code == "charge_code" assert response.charge_model == "standard" assert response.invoice_display_name == "Setup" + presentation_group_key = response.properties["presentation_group_keys"][0] + assert presentation_group_key["value"] == "region" + assert presentation_group_key["options"]["display_in_invoice"] is True def test_valid_create_charge_request(httpx_mock: HTTPXMock): @@ -522,12 +538,21 @@ def test_valid_create_charge_request(httpx_mock: HTTPXMock): charge_model="standard", pay_in_advance=True, invoiceable=True, - properties={"amount": "0.22"}, + properties={ + "amount": "0.22", + "presentation_group_keys": [ + { + "value": "region", + "options": {"display_in_invoice": True}, + } + ], + }, ) response = client.plan_charges.create(plan_code, charge) assert response.lago_id == "51c1e851-5be6-4343-a0ee-39a81d8b4ee1" assert response.charge_model == "standard" + assert charge.dict()["properties"]["presentation_group_keys"][0]["value"] == "region" def test_valid_update_charge_request(httpx_mock: HTTPXMock): diff --git a/tests/test_subscription_client.py b/tests/test_subscription_client.py index fbb5d71..2512e42 100644 --- a/tests/test_subscription_client.py +++ b/tests/test_subscription_client.py @@ -639,6 +639,9 @@ def test_valid_find_charge_request(httpx_mock: HTTPXMock): assert response.code == "charge_code" assert response.charge_model == "standard" assert response.invoice_display_name == "Setup" + presentation_group_key = response.properties["presentation_group_keys"][0] + assert presentation_group_key["value"] == "region" + assert presentation_group_key["options"]["display_in_invoice"] is True def test_valid_update_charge_request(httpx_mock: HTTPXMock):