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
14 changes: 14 additions & 0 deletions mpt_api_client/resources/billing/billing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from mpt_api_client.http import AsyncHTTPClient, HTTPClient
from mpt_api_client.resources.billing.custom_ledgers import (
AsyncCustomLedgersService,
CustomLedgersService,
)
from mpt_api_client.resources.billing.invoices import AsyncInvoicesService, InvoicesService
from mpt_api_client.resources.billing.journals import AsyncJournalsService, JournalsService
from mpt_api_client.resources.billing.ledgers import AsyncLedgersService, LedgersService
Expand Down Expand Up @@ -31,6 +35,11 @@ def invoices(self) -> InvoicesService:
"""Invoices service."""
return InvoicesService(http_client=self.http_client)

@property
def custom_ledgers(self) -> CustomLedgersService:
"""Custom ledgers service."""
return CustomLedgersService(http_client=self.http_client)


class AsyncBilling:
"""Billing MPT API Module."""
Expand All @@ -57,3 +66,8 @@ def statements(self) -> AsyncStatementsService:
def invoices(self) -> AsyncInvoicesService:
"""Invoices service."""
return AsyncInvoicesService(http_client=self.http_client)

@property
def custom_ledgers(self) -> AsyncCustomLedgersService:
"""Custom ledgers service."""
return AsyncCustomLedgersService(http_client=self.http_client)
45 changes: 45 additions & 0 deletions mpt_api_client/resources/billing/custom_ledgers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from mpt_api_client.http import AsyncService, Service
from mpt_api_client.http.mixins import (
AsyncCreateMixin,
AsyncDeleteMixin,
AsyncUpdateMixin,
CreateMixin,
DeleteMixin,
UpdateMixin,
)
from mpt_api_client.models import Model
from mpt_api_client.resources.billing.mixins import AcceptableMixin, AsyncAcceptableMixin


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


class CustomLedgersServiceConfig:
"""Custom Ledgers service configuration."""

_endpoint = "/public/v1/billing/custom-ledgers"
_model_class = CustomLedger
_collection_key = "data"


class CustomLedgersService(
CreateMixin[CustomLedger],
DeleteMixin,
UpdateMixin[CustomLedger],
AcceptableMixin[CustomLedger],
Service[CustomLedger],
CustomLedgersServiceConfig,
):
"""Custom Ledgers service."""


class AsyncCustomLedgersService(
AsyncCreateMixin[CustomLedger],
AsyncDeleteMixin,
AsyncUpdateMixin[CustomLedger],
AsyncAcceptableMixin[CustomLedger],
AsyncService[CustomLedger],
CustomLedgersServiceConfig,
):
"""Async Custom Ledgers service."""
52 changes: 52 additions & 0 deletions mpt_api_client/resources/billing/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,55 @@ async def recalculate(
return await self._resource_action( # type: ignore[attr-defined, no-any-return]
resource_id, "POST", "recalculate", json=resource_data
)


class AcceptableMixin[Model]:
"""Acceptable mixin adds the ability to accept resources."""

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 AsyncAcceptableMixin[Model]:
"""Acceptable mixin adds the ability to accept resources."""

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 @@ -32,7 +32,7 @@ extend-ignore =


per-file-ignores =
mpt_api_client/resources/billing/*.py: WPS215
mpt_api_client/resources/billing/*.py: WPS215 WPS202
mpt_api_client/resources/catalog/*.py: WPS110 WPS215 WPS214
mpt_api_client/resources/commerce/*.py: WPS215
mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214
Expand Down
6 changes: 6 additions & 0 deletions tests/resources/billing/test_billing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import pytest

from mpt_api_client.resources.billing.billing import AsyncBilling, Billing
from mpt_api_client.resources.billing.custom_ledgers import (
AsyncCustomLedgersService,
CustomLedgersService,
)
from mpt_api_client.resources.billing.invoices import AsyncInvoicesService, InvoicesService
from mpt_api_client.resources.billing.journals import AsyncJournalsService, JournalsService
from mpt_api_client.resources.billing.ledgers import AsyncLedgersService, LedgersService
Expand All @@ -24,6 +28,7 @@ def async_billing(async_http_client):
("ledgers", LedgersService),
("statements", StatementsService),
("invoices", InvoicesService),
("custom_ledgers", CustomLedgersService),
],
)
def test_billing_properties(billing, property_name, expected_service_class):
Expand All @@ -41,6 +46,7 @@ def test_billing_properties(billing, property_name, expected_service_class):
("ledgers", AsyncLedgersService),
("statements", AsyncStatementsService),
("invoices", AsyncInvoicesService),
("custom_ledgers", AsyncCustomLedgersService),
],
)
def test_async_billing_properties(async_billing, property_name, expected_service_class):
Expand Down
26 changes: 26 additions & 0 deletions tests/resources/billing/test_custom_ledgers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import pytest

from mpt_api_client.resources.billing.custom_ledgers import (
AsyncCustomLedgersService,
CustomLedgersService,
)


@pytest.fixture
def custom_ledgers_service(http_client):
return CustomLedgersService(http_client=http_client)


@pytest.fixture
def async_custom_ledgers_service(http_client):
return AsyncCustomLedgersService(http_client=http_client)


@pytest.mark.parametrize("method", ["get", "create", "update", "delete", "accept", "queue"])
def test_mixins_present(custom_ledgers_service, method):
assert hasattr(custom_ledgers_service, method)


@pytest.mark.parametrize("method", ["get", "create", "update", "delete", "accept", "queue"])
def test_async_mixins_present(async_custom_ledgers_service, method):
assert hasattr(async_custom_ledgers_service, method)
4 changes: 2 additions & 2 deletions tests/resources/billing/test_journals.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ def async_journals_service(async_http_client):

@pytest.mark.parametrize(
"method",
["get", "create", "update", "delete"],
["get", "create", "update", "delete", "regenerate", "submit", "enquiry", "accept"],
)
def test_mixins_present(journals_service, method):
assert hasattr(journals_service, method)


@pytest.mark.parametrize(
"method",
["get", "create", "update", "delete"],
["get", "create", "update", "delete", "regenerate", "submit", "enquiry", "accept"],
)
def test_async_mixins_present(async_journals_service, method):
assert hasattr(async_journals_service, method)
Expand Down
146 changes: 146 additions & 0 deletions tests/resources/billing/test_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
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 (
AcceptableMixin,
AsyncAcceptableMixin,
AsyncIssuableMixin,
AsyncRecalculatableMixin,
AsyncRegeneratableMixin,
Expand Down Expand Up @@ -69,6 +71,24 @@ class DummyAsyncIssuableService(
_collection_key = "data"


class DummyAcceptableService(
AcceptableMixin[DummyModel],
Service[DummyModel],
):
_endpoint = "/public/v1/dummy/acceptable/"
_model_class = DummyModel
_collection_key = "data"


class DummyAsyncAcceptableService(
AsyncAcceptableMixin[DummyModel],
AsyncService[DummyModel],
):
_endpoint = "/public/v1/dummy/acceptable/"
_model_class = DummyModel
_collection_key = "data"


@pytest.fixture
def regeneratable_service(http_client):
return DummyRegeneratableService(http_client=http_client)
Expand Down Expand Up @@ -99,6 +119,16 @@ def async_issuable_service(async_http_client):
return DummyAsyncIssuableService(http_client=async_http_client)


@pytest.fixture
def acceptable_service(http_client):
return DummyAcceptableService(http_client=http_client)


@pytest.fixture
def async_acceptable_service(async_http_client):
return DummyAsyncAcceptableService(http_client=async_http_client)


@pytest.mark.parametrize(
("action", "input_status"),
[
Expand Down Expand Up @@ -493,3 +523,119 @@ async def test_async_issuable_resource_actions_no_data(
assert request.content == request_expected_content
assert issuable_obj.to_dict() == response_expected_data
assert isinstance(issuable_obj, DummyModel)


@pytest.mark.parametrize(
("action", "input_status"),
[
("accept", {"id": "OBJ-0000-0001", "status": "update"}),
("queue", {"id": "OBJ-0000-0001", "status": "update"}),
],
)
def test_acceptable_resource_actions(acceptable_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/acceptable/OBJ-0000-0001/{action}"
).mock(
return_value=httpx.Response(
status_code=httpx.codes.OK,
headers={"content-type": "application/json"},
json=response_expected_data,
)
)
accept_obj = getattr(acceptable_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 accept_obj.to_dict() == response_expected_data
assert isinstance(accept_obj, DummyModel)


@pytest.mark.parametrize(
("action", "input_status"),
[("accept", None), ("queue", None)],
)
def test_acceptable_resource_actions_no_data(acceptable_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/acceptable/OBJ-0000-0001/{action}"
).mock(
return_value=httpx.Response(
status_code=httpx.codes.OK,
headers={"content-type": "application/json"},
json=response_expected_data,
)
)
accept_obj = getattr(acceptable_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 accept_obj.to_dict() == response_expected_data
assert isinstance(accept_obj, DummyModel)


@pytest.mark.parametrize(
("action", "input_status"),
[
("accept", {"id": "OBJ-0000-0001", "status": "update"}),
("queue", {"id": "OBJ-0000-0001", "status": "update"}),
],
)
async def test_async_acceptable_resource_actions(async_acceptable_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/acceptable/OBJ-0000-0001/{action}"
).mock(
return_value=httpx.Response(
status_code=httpx.codes.OK,
headers={"content-type": "application/json"},
json=response_expected_data,
)
)
accept_obj = await getattr(async_acceptable_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 accept_obj.to_dict() == response_expected_data
assert isinstance(accept_obj, DummyModel)


@pytest.mark.parametrize(
("action", "input_status"),
[("accept", None), ("queue", None)],
)
async def test_async_acceptable_resource_actions_no_data(
async_acceptable_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/acceptable/OBJ-0000-0001/{action}"
).mock(
return_value=httpx.Response(
status_code=httpx.codes.OK,
headers={"content-type": "application/json"},
json=response_expected_data,
)
)
accept_obj = await getattr(async_acceptable_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 accept_obj.to_dict() == response_expected_data
assert isinstance(accept_obj, DummyModel)