Skip to content

Commit 835e706

Browse files
authored
[MPT-13823] Added billing ledgers endpoints (#51)
Added billing ledgers endpoints https://softwareone.atlassian.net/browse/MPT-13823
2 parents 6e86662 + d61e434 commit 835e706

File tree

7 files changed

+312
-2
lines changed

7 files changed

+312
-2
lines changed

mpt_api_client/resources/billing/billing.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from mpt_api_client.http import AsyncHTTPClient, HTTPClient
22
from mpt_api_client.resources.billing.journals import AsyncJournalsService, JournalsService
3+
from mpt_api_client.resources.billing.ledgers import AsyncLedgersService, LedgersService
34

45

56
class Billing:
@@ -13,6 +14,11 @@ def journals(self) -> JournalsService:
1314
"""Journals service."""
1415
return JournalsService(http_client=self.http_client)
1516

17+
@property
18+
def ledgers(self) -> LedgersService:
19+
"""Ledgers service."""
20+
return LedgersService(http_client=self.http_client)
21+
1622

1723
class AsyncBilling:
1824
"""Billing MPT API Module."""
@@ -24,3 +30,8 @@ def __init__(self, *, http_client: AsyncHTTPClient):
2430
def journals(self) -> AsyncJournalsService:
2531
"""Journals service."""
2632
return AsyncJournalsService(http_client=self.http_client)
33+
34+
@property
35+
def ledgers(self) -> AsyncLedgersService:
36+
"""Ledgers service."""
37+
return AsyncLedgersService(http_client=self.http_client)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from mpt_api_client.http import AsyncService, Service
2+
from mpt_api_client.http.mixins import (
3+
AsyncCreateMixin,
4+
CreateMixin,
5+
)
6+
from mpt_api_client.models import Model
7+
8+
9+
class Ledger(Model):
10+
"""Ledger resource."""
11+
12+
13+
class LedgersServiceConfig:
14+
"""Ledgers service configuration."""
15+
16+
_endpoint = "/public/v1/billing/ledgers"
17+
_model_class = Ledger
18+
_collection_key = "data"
19+
20+
21+
class LedgersService(
22+
CreateMixin[Ledger],
23+
Service[Ledger],
24+
LedgersServiceConfig,
25+
):
26+
"""Ledgers service."""
27+
28+
29+
class AsyncLedgersService(
30+
AsyncCreateMixin[Ledger],
31+
AsyncService[Ledger],
32+
LedgersServiceConfig,
33+
):
34+
"""Async Ledgers service."""

mpt_api_client/resources/billing/mixins.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,79 @@ async def accept(self, resource_id: str, resource_data: ResourceData | None = No
9898
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
9999
resource_id, "POST", "accept", json=resource_data
100100
)
101+
102+
103+
class RecalculatableMixin[Model]:
104+
"""Recalculatable mixin adds the ability to recalculate resources."""
105+
106+
def recalculate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
107+
"""Recalculate resource.
108+
109+
Args:
110+
resource_id: Resource ID
111+
resource_data: Resource data will be updated
112+
"""
113+
return self._resource_action( # type: ignore[attr-defined, no-any-return]
114+
resource_id, "POST", "recalculate", json=resource_data
115+
)
116+
117+
def accept(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
118+
"""Accept resource.
119+
120+
Args:
121+
resource_id: Resource ID
122+
resource_data: Resource data will be updated
123+
"""
124+
return self._resource_action( # type: ignore[attr-defined, no-any-return]
125+
resource_id, "POST", "accept", json=resource_data
126+
)
127+
128+
def queue(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
129+
"""Queue resource.
130+
131+
Args:
132+
resource_id: Resource ID
133+
resource_data: Resource data will be updated
134+
"""
135+
return self._resource_action( # type: ignore[attr-defined, no-any-return]
136+
resource_id, "POST", "queue", json=resource_data
137+
)
138+
139+
140+
class AsyncRecalculatableMixin[Model]:
141+
"""Recalculatable mixin adds the ability to recalculate resources."""
142+
143+
async def recalculate(
144+
self, resource_id: str, resource_data: ResourceData | None = None
145+
) -> Model:
146+
"""Recalculate resource.
147+
148+
Args:
149+
resource_id: Resource ID
150+
resource_data: Resource data will be updated
151+
"""
152+
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
153+
resource_id, "POST", "recalculate", json=resource_data
154+
)
155+
156+
async def accept(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
157+
"""Accept resource.
158+
159+
Args:
160+
resource_id: Resource ID
161+
resource_data: Resource data will be updated
162+
"""
163+
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
164+
resource_id, "POST", "accept", json=resource_data
165+
)
166+
167+
async def queue(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
168+
"""Queue resource.
169+
170+
Args:
171+
resource_id: Resource ID
172+
resource_data: Resource data will be updated
173+
"""
174+
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
175+
resource_id, "POST", "queue", json=resource_data
176+
)

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ per-file-ignores =
4242
tests/http/test_service.py: WPS204 WPS202
4343
tests/http/test_mixins.py: WPS204 WPS202
4444
tests/resources/catalog/test_products.py: WPS202 WPS210
45-
tests/resources/catalog/test_mixins.py: WPS118 WPS202 WPS204
45+
tests/resources/*/test_mixins.py: WPS118 WPS202 WPS204
4646

4747
tests/*:
4848
# Allow magic strings.

tests/resources/billing/test_billing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from mpt_api_client.resources.billing.billing import AsyncBilling, Billing
44
from mpt_api_client.resources.billing.journals import AsyncJournalsService, JournalsService
5+
from mpt_api_client.resources.billing.ledgers import AsyncLedgersService, LedgersService
56

67

78
@pytest.fixture
@@ -18,6 +19,7 @@ def async_billing(async_http_client):
1819
("property_name", "expected_service_class"),
1920
[
2021
("journals", JournalsService),
22+
("ledgers", LedgersService),
2123
],
2224
)
2325
def test_billing_properties(billing, property_name, expected_service_class):
@@ -32,6 +34,7 @@ def test_billing_properties(billing, property_name, expected_service_class):
3234
("property_name", "expected_service_class"),
3335
[
3436
("journals", AsyncJournalsService),
37+
("ledgers", AsyncLedgersService),
3538
],
3639
)
3740
def test_async_billing_properties(async_billing, property_name, expected_service_class):
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
3+
from mpt_api_client.resources.billing.ledgers import AsyncLedgersService, LedgersService
4+
5+
6+
@pytest.fixture
7+
def ledgers_service(http_client):
8+
return LedgersService(http_client=http_client)
9+
10+
11+
@pytest.fixture
12+
def async_ledgers_service(async_http_client):
13+
return AsyncLedgersService(http_client=async_http_client)
14+
15+
16+
@pytest.mark.parametrize(
17+
"method",
18+
["get", "create"],
19+
)
20+
def test_mixins_present(ledgers_service, method):
21+
assert hasattr(ledgers_service, method)
22+
23+
24+
@pytest.mark.parametrize(
25+
"method",
26+
["get", "create"],
27+
)
28+
def test_async_mixins_present(async_ledgers_service, method):
29+
assert hasattr(async_ledgers_service, method)

tests/resources/billing/test_mixins.py

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
from mpt_api_client.http.async_service import AsyncService
66
from mpt_api_client.http.service import Service
7-
from mpt_api_client.resources.billing.mixins import AsyncRegeneratableMixin, RegeneratableMixin
7+
from mpt_api_client.resources.billing.mixins import (
8+
AsyncRecalculatableMixin,
9+
AsyncRegeneratableMixin,
10+
RecalculatableMixin,
11+
RegeneratableMixin,
12+
)
813
from tests.conftest import DummyModel
914

1015

@@ -26,6 +31,24 @@ class DummyAsyncRegeneratableService(
2631
_collection_key = "data"
2732

2833

34+
class DummyRecalculatableService(
35+
RecalculatableMixin[DummyModel],
36+
Service[DummyModel],
37+
):
38+
_endpoint = "/public/v1/dummy/recalculatable/"
39+
_model_class = DummyModel
40+
_collection_key = "data"
41+
42+
43+
class DummyAsyncRecalculatableService(
44+
AsyncRecalculatableMixin[DummyModel],
45+
AsyncService[DummyModel],
46+
):
47+
_endpoint = "/public/v1/dummy/recalculatable/"
48+
_model_class = DummyModel
49+
_collection_key = "data"
50+
51+
2952
@pytest.fixture
3053
def regeneratable_service(http_client):
3154
return DummyRegeneratableService(http_client=http_client)
@@ -36,6 +59,16 @@ def async_regeneratable_service(async_http_client):
3659
return DummyAsyncRegeneratableService(http_client=async_http_client)
3760

3861

62+
@pytest.fixture
63+
def recalculatable_service(http_client):
64+
return DummyRecalculatableService(http_client=http_client)
65+
66+
67+
@pytest.fixture
68+
def async_recalculatable_service(async_http_client):
69+
return DummyAsyncRecalculatableService(http_client=async_http_client)
70+
71+
3972
@pytest.mark.parametrize(
4073
("action", "input_status"),
4174
[
@@ -164,3 +197,127 @@ async def test_async_custom_resource_actions_no_data(
164197
assert request.content == request_expected_content
165198
assert journal.to_dict() == response_expected_data
166199
assert isinstance(journal, DummyModel)
200+
201+
202+
@pytest.mark.parametrize(
203+
("action", "input_status"),
204+
[
205+
("recalculate", {"id": "OBJ-0000-0001", "status": "update"}),
206+
("accept", {"id": "OBJ-0000-0001", "status": "update"}),
207+
("queue", {"id": "OBJ-0000-0001", "status": "update"}),
208+
],
209+
)
210+
def test_recalculate_resource_actions(recalculatable_service, action, input_status):
211+
request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}'
212+
response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"}
213+
with respx.mock:
214+
mock_route = respx.post(
215+
f"https://api.example.com/public/v1/dummy/recalculatable/OBJ-0000-0001/{action}"
216+
).mock(
217+
return_value=httpx.Response(
218+
status_code=httpx.codes.OK,
219+
headers={"content-type": "application/json"},
220+
json=response_expected_data,
221+
)
222+
)
223+
recalc_obj = getattr(recalculatable_service, action)("OBJ-0000-0001", input_status)
224+
225+
assert mock_route.call_count == 1
226+
request = mock_route.calls[0].request
227+
228+
assert request.content == request_expected_content
229+
assert recalc_obj.to_dict() == response_expected_data
230+
assert isinstance(recalc_obj, DummyModel)
231+
232+
233+
@pytest.mark.parametrize(
234+
("action", "input_status"),
235+
[("recalculate", None), ("accept", None), ("queue", None)],
236+
)
237+
def test_recalculate_resource_actions_no_data(recalculatable_service, action, input_status):
238+
request_expected_content = b""
239+
response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"}
240+
with respx.mock:
241+
mock_route = respx.post(
242+
f"https://api.example.com/public/v1/dummy/recalculatable/OBJ-0000-0001/{action}"
243+
).mock(
244+
return_value=httpx.Response(
245+
status_code=httpx.codes.OK,
246+
headers={"content-type": "application/json"},
247+
json=response_expected_data,
248+
)
249+
)
250+
recalc_obj = getattr(recalculatable_service, action)("OBJ-0000-0001", input_status)
251+
252+
assert mock_route.call_count == 1
253+
request = mock_route.calls[0].request
254+
255+
assert request.content == request_expected_content
256+
assert recalc_obj.to_dict() == response_expected_data
257+
assert isinstance(recalc_obj, DummyModel)
258+
259+
260+
@pytest.mark.parametrize(
261+
("action", "input_status"),
262+
[
263+
("recalculate", {"id": "OBJ-0000-0001", "status": "update"}),
264+
("accept", {"id": "OBJ-0000-0001", "status": "update"}),
265+
("queue", {"id": "OBJ-0000-0001", "status": "update"}),
266+
],
267+
)
268+
async def test_async_recalculate_resource_actions(
269+
async_recalculatable_service, action, input_status
270+
):
271+
request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}'
272+
response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"}
273+
with respx.mock:
274+
mock_route = respx.post(
275+
f"https://api.example.com/public/v1/dummy/recalculatable/OBJ-0000-0001/{action}"
276+
).mock(
277+
return_value=httpx.Response(
278+
status_code=httpx.codes.OK,
279+
headers={"content-type": "application/json"},
280+
json=response_expected_data,
281+
)
282+
)
283+
recalc_obj = await getattr(async_recalculatable_service, action)(
284+
"OBJ-0000-0001", input_status
285+
)
286+
287+
assert mock_route.call_count == 1
288+
request = mock_route.calls[0].request
289+
290+
assert request.content == request_expected_content
291+
assert recalc_obj.to_dict() == response_expected_data
292+
assert isinstance(recalc_obj, DummyModel)
293+
294+
295+
@pytest.mark.parametrize(
296+
("action", "input_status"),
297+
[("recalculate", None), ("accept", None), ("queue", None)],
298+
)
299+
async def test_async_recalculate_resource_actions_no_data(
300+
async_recalculatable_service, action, input_status
301+
):
302+
request_expected_content = b""
303+
response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"}
304+
with respx.mock:
305+
mock_route = respx.post(
306+
f"https://api.example.com/public/v1/dummy/recalculatable/OBJ-0000-0001/{action}"
307+
).mock(
308+
return_value=httpx.Response(
309+
status_code=httpx.codes.OK,
310+
headers={"content-type": "application/json"},
311+
json=response_expected_data,
312+
)
313+
)
314+
recalc_obj = await getattr(async_recalculatable_service, action)(
315+
"OBJ-0000-0001", input_status
316+
)
317+
318+
assert mock_route.call_count == 1
319+
request = mock_route.calls[0].request
320+
321+
assert request.content == request_expected_content
322+
assert recalc_obj.to_dict() == response_expected_data
323+
assert isinstance(recalc_obj, DummyModel)

0 commit comments

Comments
 (0)