Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions mpt_api_client/resources/billing/billing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from mpt_api_client.http import AsyncHTTPClient, HTTPClient
from mpt_api_client.resources.billing.journals import AsyncJournalsService, JournalsService
from mpt_api_client.resources.billing.ledgers import AsyncLedgersService, LedgersService


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

@property
def ledgers(self) -> LedgersService:
"""Ledgers service."""
return LedgersService(http_client=self.http_client)


class AsyncBilling:
"""Billing MPT API Module."""
Expand All @@ -24,3 +30,8 @@ def __init__(self, *, http_client: AsyncHTTPClient):
def journals(self) -> AsyncJournalsService:
"""Journals service."""
return AsyncJournalsService(http_client=self.http_client)

@property
def ledgers(self) -> AsyncLedgersService:
"""Ledgers service."""
return AsyncLedgersService(http_client=self.http_client)
34 changes: 34 additions & 0 deletions mpt_api_client/resources/billing/ledgers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from mpt_api_client.http import AsyncService, Service
from mpt_api_client.http.mixins import (
AsyncCreateMixin,
CreateMixin,
)
from mpt_api_client.models import Model


class Ledger(Model):
"""Ledger resource."""


class LedgersServiceConfig:
"""Ledgers service configuration."""

_endpoint = "/public/v1/billing/ledgers"
_model_class = Ledger
_collection_key = "data"


class LedgersService(
CreateMixin[Ledger],
Service[Ledger],
LedgersServiceConfig,
):
"""Ledgers service."""


class AsyncLedgersService(
AsyncCreateMixin[Ledger],
AsyncService[Ledger],
LedgersServiceConfig,
):
"""Async Ledgers service."""
76 changes: 76 additions & 0 deletions mpt_api_client/resources/billing/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,79 @@ async def accept(self, resource_id: str, resource_data: ResourceData | None = No
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
resource_id, "POST", "accept", json=resource_data
)


class RecalculatableMixin[Model]:
"""Recalculatable mixin adds the ability to recalculate resources."""

def recalculate(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
"""Recalculate resource.

Args:
resource_id: Resource ID
resource_data: Resource data will be updated
"""
return self._resource_action( # type: ignore[attr-defined, no-any-return]
resource_id, "POST", "recalculate", json=resource_data
)

def accept(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
"""Accept resource.

Args:
resource_id: Resource ID
resource_data: Resource data will be updated
"""
return self._resource_action( # type: ignore[attr-defined, no-any-return]
resource_id, "POST", "accept", json=resource_data
)

def queue(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
"""Queue resource.

Args:
resource_id: Resource ID
resource_data: Resource data will be updated
"""
return self._resource_action( # type: ignore[attr-defined, no-any-return]
resource_id, "POST", "queue", json=resource_data
)


class AsyncRecalculatableMixin[Model]:
"""Recalculatable mixin adds the ability to recalculate resources."""

async def recalculate(
self, resource_id: str, resource_data: ResourceData | None = None
) -> Model:
"""Recalculate resource.

Args:
resource_id: Resource ID
resource_data: Resource data will be updated
"""
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
resource_id, "POST", "recalculate", json=resource_data
)

async def accept(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
"""Accept resource.

Args:
resource_id: Resource ID
resource_data: Resource data will be updated
"""
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
resource_id, "POST", "accept", json=resource_data
)

async def queue(self, resource_id: str, resource_data: ResourceData | None = None) -> Model:
"""Queue resource.

Args:
resource_id: Resource ID
resource_data: Resource data will be updated
"""
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
resource_id, "POST", "queue", json=resource_data
)
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ per-file-ignores =
tests/http/test_service.py: WPS204 WPS202
tests/http/test_mixins.py: WPS204 WPS202
tests/resources/catalog/test_products.py: WPS202 WPS210
tests/resources/catalog/test_mixins.py: WPS118 WPS202 WPS204
tests/resources/*/test_mixins.py: WPS118 WPS202 WPS204

tests/*:
# Allow magic strings.
Expand Down
3 changes: 3 additions & 0 deletions tests/resources/billing/test_billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from mpt_api_client.resources.billing.billing import AsyncBilling, Billing
from mpt_api_client.resources.billing.journals import AsyncJournalsService, JournalsService
from mpt_api_client.resources.billing.ledgers import AsyncLedgersService, LedgersService


@pytest.fixture
Expand All @@ -18,6 +19,7 @@ def async_billing(async_http_client):
("property_name", "expected_service_class"),
[
("journals", JournalsService),
("ledgers", LedgersService),
],
)
def test_billing_properties(billing, property_name, expected_service_class):
Expand All @@ -32,6 +34,7 @@ def test_billing_properties(billing, property_name, expected_service_class):
("property_name", "expected_service_class"),
[
("journals", AsyncJournalsService),
("ledgers", AsyncLedgersService),
],
)
def test_async_billing_properties(async_billing, property_name, expected_service_class):
Expand Down
29 changes: 29 additions & 0 deletions tests/resources/billing/test_ledgers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest

from mpt_api_client.resources.billing.ledgers import AsyncLedgersService, LedgersService


@pytest.fixture
def ledgers_service(http_client):
return LedgersService(http_client=http_client)


@pytest.fixture
def async_ledgers_service(async_http_client):
return AsyncLedgersService(http_client=async_http_client)


@pytest.mark.parametrize(
"method",
["get", "create"],
)
def test_mixins_present(ledgers_service, method):
assert hasattr(ledgers_service, method)


@pytest.mark.parametrize(
"method",
["get", "create"],
)
def test_async_mixins_present(async_ledgers_service, method):
assert hasattr(async_ledgers_service, method)
159 changes: 158 additions & 1 deletion tests/resources/billing/test_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

from mpt_api_client.http.async_service import AsyncService
from mpt_api_client.http.service import Service
from mpt_api_client.resources.billing.mixins import AsyncRegeneratableMixin, RegeneratableMixin
from mpt_api_client.resources.billing.mixins import (
AsyncRecalculatableMixin,
AsyncRegeneratableMixin,
RecalculatableMixin,
RegeneratableMixin,
)
from tests.conftest import DummyModel


Expand All @@ -26,6 +31,24 @@ class DummyAsyncRegeneratableService(
_collection_key = "data"


class DummyRecalculatableService(
RecalculatableMixin[DummyModel],
Service[DummyModel],
):
_endpoint = "/public/v1/dummy/recalculatable/"
_model_class = DummyModel
_collection_key = "data"


class DummyAsyncRecalculatableService(
AsyncRecalculatableMixin[DummyModel],
AsyncService[DummyModel],
):
_endpoint = "/public/v1/dummy/recalculatable/"
_model_class = DummyModel
_collection_key = "data"


@pytest.fixture
def regeneratable_service(http_client):
return DummyRegeneratableService(http_client=http_client)
Expand All @@ -36,6 +59,16 @@ def async_regeneratable_service(async_http_client):
return DummyAsyncRegeneratableService(http_client=async_http_client)


@pytest.fixture
def recalculatable_service(http_client):
return DummyRecalculatableService(http_client=http_client)


@pytest.fixture
def async_recalculatable_service(async_http_client):
return DummyAsyncRecalculatableService(http_client=async_http_client)


@pytest.mark.parametrize(
("action", "input_status"),
[
Expand Down Expand Up @@ -164,3 +197,127 @@ async def test_async_custom_resource_actions_no_data(
assert request.content == request_expected_content
assert journal.to_dict() == response_expected_data
assert isinstance(journal, DummyModel)


@pytest.mark.parametrize(
("action", "input_status"),
[
("recalculate", {"id": "OBJ-0000-0001", "status": "update"}),
("accept", {"id": "OBJ-0000-0001", "status": "update"}),
("queue", {"id": "OBJ-0000-0001", "status": "update"}),
],
)
def test_recalculate_resource_actions(recalculatable_service, action, input_status):
request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}'
response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"}
with respx.mock:
mock_route = respx.post(
f"https://api.example.com/public/v1/dummy/recalculatable/OBJ-0000-0001/{action}"
).mock(
return_value=httpx.Response(
status_code=httpx.codes.OK,
headers={"content-type": "application/json"},
json=response_expected_data,
)
)
recalc_obj = getattr(recalculatable_service, action)("OBJ-0000-0001", input_status)

assert mock_route.call_count == 1
request = mock_route.calls[0].request

assert request.content == request_expected_content
assert recalc_obj.to_dict() == response_expected_data
assert isinstance(recalc_obj, DummyModel)


@pytest.mark.parametrize(
("action", "input_status"),
[("recalculate", None), ("accept", None), ("queue", None)],
)
def test_recalculate_resource_actions_no_data(recalculatable_service, action, input_status):
request_expected_content = b""
response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"}
with respx.mock:
mock_route = respx.post(
f"https://api.example.com/public/v1/dummy/recalculatable/OBJ-0000-0001/{action}"
).mock(
return_value=httpx.Response(
status_code=httpx.codes.OK,
headers={"content-type": "application/json"},
json=response_expected_data,
)
)
recalc_obj = getattr(recalculatable_service, action)("OBJ-0000-0001", input_status)

assert mock_route.call_count == 1
request = mock_route.calls[0].request

assert request.content == request_expected_content
assert recalc_obj.to_dict() == response_expected_data
assert isinstance(recalc_obj, DummyModel)


@pytest.mark.parametrize(
("action", "input_status"),
[
("recalculate", {"id": "OBJ-0000-0001", "status": "update"}),
("accept", {"id": "OBJ-0000-0001", "status": "update"}),
("queue", {"id": "OBJ-0000-0001", "status": "update"}),
],
)
async def test_async_recalculate_resource_actions(
async_recalculatable_service, action, input_status
):
request_expected_content = b'{"id":"OBJ-0000-0001","status":"update"}'
response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"}
with respx.mock:
mock_route = respx.post(
f"https://api.example.com/public/v1/dummy/recalculatable/OBJ-0000-0001/{action}"
).mock(
return_value=httpx.Response(
status_code=httpx.codes.OK,
headers={"content-type": "application/json"},
json=response_expected_data,
)
)
recalc_obj = await getattr(async_recalculatable_service, action)(
"OBJ-0000-0001", input_status
)

assert mock_route.call_count == 1
request = mock_route.calls[0].request

assert request.content == request_expected_content
assert recalc_obj.to_dict() == response_expected_data
assert isinstance(recalc_obj, DummyModel)


@pytest.mark.parametrize(
("action", "input_status"),
[("recalculate", None), ("accept", None), ("queue", None)],
)
async def test_async_recalculate_resource_actions_no_data(
async_recalculatable_service, action, input_status
):
request_expected_content = b""
response_expected_data = {"id": "OBJ-0000-0001", "status": "new_status"}
with respx.mock:
mock_route = respx.post(
f"https://api.example.com/public/v1/dummy/recalculatable/OBJ-0000-0001/{action}"
).mock(
return_value=httpx.Response(
status_code=httpx.codes.OK,
headers={"content-type": "application/json"},
json=response_expected_data,
)
)
recalc_obj = await getattr(async_recalculatable_service, action)(
"OBJ-0000-0001", input_status
)

assert mock_route.call_count == 1
request = mock_route.calls[0].request

assert request.content == request_expected_content
assert recalc_obj.to_dict() == response_expected_data
assert isinstance(recalc_obj, DummyModel)