From 832937c0c735b6c5389d0c4c08aa880b15a429ed Mon Sep 17 00:00:00 2001 From: Robert Segal Date: Mon, 15 Dec 2025 10:15:18 -0700 Subject: [PATCH] Add e2e tests for billing journals --- e2e_config.test.json | 9 +- .../resources/billing/journal_upload.py | 38 ------ mpt_api_client/resources/billing/journals.py | 79 ++++++++++--- tests/data/test_billing_journal.jsonl | 1 + tests/data/test_billing_journal.xlsx | Bin 0 -> 9400 bytes tests/e2e/billing/conftest.py | 18 +++ tests/e2e/billing/journal/conftest.py | 21 ++++ .../e2e/billing/journal/test_async_journal.py | 111 ++++++++++++++++++ .../e2e/billing/journal/test_sync_journal.py | 105 +++++++++++++++++ .../resources/billing/test_journal_upload.py | 46 -------- tests/unit/resources/billing/test_journals.py | 76 ++++++++++-- 11 files changed, 392 insertions(+), 112 deletions(-) delete mode 100644 mpt_api_client/resources/billing/journal_upload.py create mode 100644 tests/data/test_billing_journal.jsonl create mode 100644 tests/data/test_billing_journal.xlsx create mode 100644 tests/e2e/billing/conftest.py create mode 100644 tests/e2e/billing/journal/conftest.py create mode 100644 tests/e2e/billing/journal/test_async_journal.py create mode 100644 tests/e2e/billing/journal/test_sync_journal.py delete mode 100644 tests/unit/resources/billing/test_journal_upload.py diff --git a/e2e_config.test.json b/e2e_config.test.json index 03507e6d..deb30fae 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -11,6 +11,7 @@ "accounts.seller.id": "SEL-7310-3075", "accounts.user.id": "USR-9673-3314", "accounts.user_group.id": "UGR-6822-0561", + "billing.journal.id": "BJO-6562-0928", "catalog.authorization.id": "AUT-9288-6146", "catalog.listing.id": "LST-5489-0806", "catalog.price_list.id": "PRC-7255-3950-0245", @@ -24,9 +25,9 @@ "catalog.product.terms.id": "TCS-7255-3950-0001", "catalog.product.terms.variant.id": "TCV-7255-3950-0001-0001", "catalog.unit.id": "UNT-1229", - "commerce.agreement.attachment.id": "ATT-9850-2169-6098-0001", - "commerce.agreement.id": "AGR-9850-2169-6098", - "commerce.agreement.subscription.line.id": "ALI-9850-2169-6098-0001", + "commerce.agreement.attachment.id": "ATT-0078-7880-7436-0001", + "commerce.agreement.id": "AGR-0078-7880-7436", + "commerce.agreement.subscription.line.id": "ALI-0078-7880-7436-0001", "commerce.assets.agreement.id": "AGR-2473-3299-1721", "commerce.assets.agreement.line.id": "ALI-9320-4904-7940-0001", "commerce.assets.id": "AST-0625-6526-6154", @@ -37,7 +38,7 @@ "commerce.assets.product.template.id": "", "commerce.authorization.id": "AUT-0031-2873", "commerce.client.id": "ACC-1086-6867", - "commerce.order.id": "ORD-6969-3541-5426", + "commerce.order.id": "ORD-0557-5037-6263", "commerce.product.id": "PRD-1767-7355", "commerce.product.item.id": "ITM-1767-7355-0001", "commerce.product.listing.id": "LST-5489-0806", diff --git a/mpt_api_client/resources/billing/journal_upload.py b/mpt_api_client/resources/billing/journal_upload.py deleted file mode 100644 index dd9e9774..00000000 --- a/mpt_api_client/resources/billing/journal_upload.py +++ /dev/null @@ -1,38 +0,0 @@ -from mpt_api_client.http import AsyncService, Service -from mpt_api_client.http.mixins import ( - AsyncCollectionMixin, - AsyncFilesOperationsMixin, - CollectionMixin, - FilesOperationsMixin, -) -from mpt_api_client.models import Model - - -class JournalUpload(Model): - """Journal Upload resource.""" - - -class JournalUploadServiceConfig: - """Journal Upload service configuration.""" - - _endpoint = "/public/v1/billing/journals/{journal_id}/upload" - _model_class = JournalUpload - _collection_key = "data" - - -class JournalUploadService( - FilesOperationsMixin[JournalUpload], - CollectionMixin[JournalUpload], - Service[JournalUpload], - JournalUploadServiceConfig, -): - """Journal Upload service.""" - - -class AsyncJournalUploadService( - AsyncFilesOperationsMixin[JournalUpload], - AsyncCollectionMixin[JournalUpload], - AsyncService[JournalUpload], - JournalUploadServiceConfig, -): - """Journal Upload service.""" diff --git a/mpt_api_client/resources/billing/journals.py b/mpt_api_client/resources/billing/journals.py index 208b5a49..e4377277 100644 --- a/mpt_api_client/resources/billing/journals.py +++ b/mpt_api_client/resources/billing/journals.py @@ -1,3 +1,5 @@ +from urllib.parse import urljoin + from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, @@ -5,6 +7,7 @@ CollectionMixin, ManagedResourceMixin, ) +from mpt_api_client.http.types import FileTypes from mpt_api_client.models import Model from mpt_api_client.resources.billing.journal_attachments import ( AsyncJournalAttachmentsService, @@ -18,10 +21,6 @@ AsyncJournalSellersService, JournalSellersService, ) -from mpt_api_client.resources.billing.journal_upload import ( - AsyncJournalUploadService, - JournalUploadService, -) from mpt_api_client.resources.billing.mixins import AsyncRegeneratableMixin, RegeneratableMixin @@ -35,6 +34,8 @@ class JournalsServiceConfig: _endpoint = "/public/v1/billing/journals" _model_class = Journal _collection_key = "data" + _upload_file_key = "file" + _upload_data_key = "id" class JournalsService( @@ -46,6 +47,35 @@ class JournalsService( ): """Journals service.""" + def upload(self, journal_id: str, file: FileTypes | None = None) -> Journal: # noqa: WPS110 + """Upload journal file. + + Args: + journal_id: Journal ID. + file: journal file. + + Returns: + Journal: Created resource. + """ + files = {} + + if file: + files[self._upload_file_key] = file # UNUSED type: ignore[attr-defined] + files[self._upload_data_key] = journal_id # UNUSED type: ignore + + path = urljoin(f"{self.path}/", f"{journal_id}/upload") + + response = self.http_client.request( # UNUSED type: ignore[attr-defined] + "post", + path, # UNUSED type: ignore[attr-defined] + files=files, + force_multipart=True, + ) + + return self._model_class.from_response( + response + ) # UNUSED type: ignore[attr-defined, no-any-return] + def attachments(self, journal_id: str) -> JournalAttachmentsService: """Return journal attachments service.""" return JournalAttachmentsService( @@ -65,12 +95,6 @@ def charges(self, journal_id: str) -> JournalChargesService: http_client=self.http_client, endpoint_params={"journal_id": journal_id} ) - def upload(self, journal_id: str) -> JournalUploadService: - """Return journal upload service.""" - return JournalUploadService( - http_client=self.http_client, endpoint_params={"journal_id": journal_id} - ) - class AsyncJournalsService( AsyncRegeneratableMixin[Journal], @@ -81,6 +105,35 @@ class AsyncJournalsService( ): """Async Journals service.""" + async def upload(self, journal_id: str, file: FileTypes | None = None) -> Journal: # noqa: WPS110 + """Upload journal file. + + Args: + journal_id: Journal ID. + file: journal file. + + Returns: + Journal: Created resource. + """ + files = {} + + if file: + files[self._upload_file_key] = file # UNUSED type: ignore[attr-defined] + files[self._upload_data_key] = journal_id # UNUSED type: ignore + + path = urljoin(f"{self.path}/", f"{journal_id}/upload") + + response = await self.http_client.request( # UNUSED type: ignore[attr-defined] + "post", + path, # UNUSED type: ignore[attr-defined] + files=files, + force_multipart=True, + ) + + return self._model_class.from_response( + response + ) # UNUSED type: ignore[attr-defined, no-any-return] + def attachments(self, journal_id: str) -> AsyncJournalAttachmentsService: """Return journal attachments service.""" return AsyncJournalAttachmentsService( @@ -99,9 +152,3 @@ def charges(self, journal_id: str) -> AsyncJournalChargesService: return AsyncJournalChargesService( http_client=self.http_client, endpoint_params={"journal_id": journal_id} ) - - def upload(self, journal_id: str) -> AsyncJournalUploadService: - """Return journal upload service.""" - return AsyncJournalUploadService( - http_client=self.http_client, endpoint_params={"journal_id": journal_id} - ) diff --git a/tests/data/test_billing_journal.jsonl b/tests/data/test_billing_journal.jsonl new file mode 100644 index 00000000..a4906804 --- /dev/null +++ b/tests/data/test_billing_journal.jsonl @@ -0,0 +1 @@ +{"externalIds": {"vendor": "ext-seeded-billing-sub-vendor-id", "invoice": "INV12345", "reference": "ORD-7924-7691-0805"}, "search": {"source": {"type": "Subscription", "criteria": "id", "value": "SUB-5839-4140-9574"},"order":{"criteria":"order.id","value":"ORD-7924-7691-0805"}, "item": {"criteria": "item.id", "value": "ITM-1767-7355-0001"}}, "period": {"start": "2025-12-22", "end": "2026-12-21"}, "price": {"unitPP": 10, "PPx1": 8.33}, "quantity": 10, "segment": "COM", "description": {"value1": "desc-1", "value2": "desc-2"}} diff --git a/tests/data/test_billing_journal.xlsx b/tests/data/test_billing_journal.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..81203ebdbd1d3d587b7c440302c27d28fbce76a1 GIT binary patch literal 9400 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 literal 0 HcmV?d00001 diff --git a/tests/e2e/billing/conftest.py b/tests/e2e/billing/conftest.py new file mode 100644 index 00000000..47b08ed1 --- /dev/null +++ b/tests/e2e/billing/conftest.py @@ -0,0 +1,18 @@ +import pathlib + +import pytest + + +@pytest.fixture +def billing_journal_fd(): + file_path = pathlib.Path("tests/data/test_billing_journal.jsonl").resolve() + fd = file_path.open("rb") + try: + yield fd + finally: + fd.close() + + +@pytest.fixture +def billing_journal_id(e2e_config): + return e2e_config["billing.journal.id"] diff --git a/tests/e2e/billing/journal/conftest.py b/tests/e2e/billing/journal/conftest.py new file mode 100644 index 00000000..9ac8708b --- /dev/null +++ b/tests/e2e/billing/journal/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +@pytest.fixture +def invalid_billing_journal_id(): + return "BJO-0000-0000" + + +@pytest.fixture +def billing_journal_factory(authorization_id): + def factory( + name: str = "E2E Created Billing Journal", + ): + return { + "authorization": {"id": authorization_id}, + "dueDate": "2026-01-02T19:00:00.000Z", + "externalIds": {}, + "name": name, + } + + return factory diff --git a/tests/e2e/billing/journal/test_async_journal.py b/tests/e2e/billing/journal/test_async_journal.py new file mode 100644 index 00000000..e15ad605 --- /dev/null +++ b/tests/e2e/billing/journal/test_async_journal.py @@ -0,0 +1,111 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.rql.query_builder import RQLQuery + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +async def created_billing_journal(async_mpt_vendor, billing_journal_factory): + new_billing_journal_request_data = billing_journal_factory( + name="E2E Created Billing Journal", + ) + + created_billing_journal = await async_mpt_vendor.billing.journals.create( + new_billing_journal_request_data + ) + + yield created_billing_journal + + try: + await async_mpt_vendor.billing.journals.delete(created_billing_journal.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete billing journal: {error.title}") # noqa: WPS421 + + +@pytest.fixture +async def submitted_billing_journal(async_mpt_vendor, created_billing_journal, billing_journal_fd): + await async_mpt_vendor.billing.journals.submit(created_billing_journal.id) + await async_mpt_vendor.billing.journals.upload( + journal_id=created_billing_journal.id, + file=billing_journal_fd, + ) + + return created_billing_journal + + +@pytest.fixture +async def completed_billing_journal(async_mpt_vendor, submitted_billing_journal): + await async_mpt_vendor.billing.journals.accept(submitted_billing_journal.id) + await async_mpt_vendor.billing.journals.complete(submitted_billing_journal.id) + return submitted_billing_journal + + +async def test_get_billing_journal_by_id(async_mpt_vendor, billing_journal_id): + result = await async_mpt_vendor.billing.journals.get(billing_journal_id) + + assert result is not None + + +async def test_list_billing_journals(async_mpt_vendor): + limit = 10 + + result = await async_mpt_vendor.billing.journals.fetch_page(limit=limit) + + assert len(result) > 0 + + +async def test_get_billing_journal_by_id_not_found(async_mpt_vendor, invalid_billing_journal_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_vendor.billing.journals.get(invalid_billing_journal_id) + + +async def test_filter_billing_journals(async_mpt_vendor, billing_journal_id): + select_fields = ["-value"] + filtered_billing_journals = ( + async_mpt_vendor.billing.journals.filter(RQLQuery(id=billing_journal_id)) + .filter(RQLQuery(name="E2E Seeded Billing Journal")) + .select(*select_fields) + ) + + result = [billing_journal async for billing_journal in filtered_billing_journals.iterate()] + + assert len(result) == 1 + + +def test_create_billing_journal(created_billing_journal): + result = created_billing_journal + + assert result is not None + + +async def test_update_billing_journal( + async_mpt_vendor, created_billing_journal, billing_journal_factory +): + updated_name = "E2E Updated Billing Journal Name" + updated_billing_journal_data = billing_journal_factory(name=updated_name) + + result = await async_mpt_vendor.billing.journals.update( + created_billing_journal.id, + updated_billing_journal_data, + ) + + assert result.name == updated_name + + +async def test_delete_billing_journal(async_mpt_vendor, created_billing_journal): + result = created_billing_journal + + await async_mpt_vendor.billing.journals.delete(result.id) + + +async def test_upload_billing_journal( + async_mpt_vendor, created_billing_journal, billing_journal_fd +): + result = await async_mpt_vendor.billing.journals.upload( + journal_id=created_billing_journal.id, + file=billing_journal_fd, + ) + + assert result is not None diff --git a/tests/e2e/billing/journal/test_sync_journal.py b/tests/e2e/billing/journal/test_sync_journal.py new file mode 100644 index 00000000..7b99d0b3 --- /dev/null +++ b/tests/e2e/billing/journal/test_sync_journal.py @@ -0,0 +1,105 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.rql.query_builder import RQLQuery + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +def created_billing_journal(mpt_vendor, billing_journal_factory): + new_billing_journal_request_data = billing_journal_factory( + name="E2E Created Billing Journal", + ) + + created_billing_journal = mpt_vendor.billing.journals.create(new_billing_journal_request_data) + + yield created_billing_journal + + try: + mpt_vendor.billing.journals.delete(created_billing_journal.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete billing journal: {error.title}") # noqa: WPS421 + + +@pytest.fixture +def submitted_billing_journal(mpt_vendor, created_billing_journal, billing_journal_fd): + mpt_vendor.billing.journals.submit(created_billing_journal.id) + mpt_vendor.billing.journals.upload( + journal_id=created_billing_journal.id, + file=billing_journal_fd, + ) + + return created_billing_journal + + +@pytest.fixture +def completed_billing_journal(mpt_vendor, submitted_billing_journal): + mpt_vendor.billing.journals.accept(submitted_billing_journal.id) + mpt_vendor.billing.journals.complete(submitted_billing_journal.id) + return submitted_billing_journal + + +def test_get_billing_journal_by_id(mpt_vendor, billing_journal_id): + result = mpt_vendor.billing.journals.get(billing_journal_id) + + assert result is not None + + +def test_list_billing_journals(mpt_vendor): + limit = 10 + + result = mpt_vendor.billing.journals.fetch_page(limit=limit) + + assert len(result) > 0 + + +def test_get_billing_journal_by_id_not_found(mpt_vendor, invalid_billing_journal_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_vendor.billing.journals.get(invalid_billing_journal_id) + + +def test_filter_billing_journals(mpt_vendor, billing_journal_id): + select_fields = ["-value"] + filtered_billing_journals = ( + mpt_vendor.billing.journals.filter(RQLQuery(id=billing_journal_id)) + .filter(RQLQuery(name="E2E Seeded Billing Journal")) + .select(*select_fields) + ) + + result = list(filtered_billing_journals.iterate()) + + assert len(result) == 1 + + +def test_create_billing_journal(created_billing_journal): + result = created_billing_journal + + assert result is not None + + +def test_update_billing_journal(mpt_vendor, created_billing_journal, billing_journal_factory): + updated_name = "E2E Updated Billing Journal Name" + updated_billing_journal_data = billing_journal_factory(name=updated_name) + + result = mpt_vendor.billing.journals.update( + created_billing_journal.id, + updated_billing_journal_data, + ) + + assert result.name == updated_name + + +def test_delete_billing_journal(mpt_vendor, created_billing_journal): + result = created_billing_journal + + mpt_vendor.billing.journals.delete(result.id) + + +def test_upload_billing_journal(mpt_vendor, created_billing_journal, billing_journal_fd): + result = mpt_vendor.billing.journals.upload( + journal_id=created_billing_journal.id, + file=billing_journal_fd, + ) + + assert result is not None diff --git a/tests/unit/resources/billing/test_journal_upload.py b/tests/unit/resources/billing/test_journal_upload.py deleted file mode 100644 index 350d7354..00000000 --- a/tests/unit/resources/billing/test_journal_upload.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest - -from mpt_api_client.resources.billing.journal_upload import ( - AsyncJournalUploadService, - JournalUploadService, -) - - -@pytest.fixture -def journal_upload_service(http_client): - return JournalUploadService( - http_client=http_client, endpoint_params={"journal_id": "JRN-0000-0001"} - ) - - -@pytest.fixture -def async_journal_upload_service(async_http_client): - return AsyncJournalUploadService( - http_client=async_http_client, endpoint_params={"journal_id": "JRN-0000-0001"} - ) - - -def test_endpoint(journal_upload_service) -> None: - result = journal_upload_service.path == "/public/v1/billing/journals/JRN-0000-0001/upload" - - assert result is True - - -def test_async_endpoint(async_journal_upload_service) -> None: - result = async_journal_upload_service.path == "/public/v1/billing/journals/JRN-0000-0001/upload" - - assert result is True - - -@pytest.mark.parametrize("method", ["create"]) -def test_methods_present(journal_upload_service, method: str) -> None: - result = hasattr(journal_upload_service, method) - - assert result is True - - -@pytest.mark.parametrize("method", ["create"]) -def test_async_methods_present(async_journal_upload_service, method: str) -> None: - result = hasattr(async_journal_upload_service, method) - - assert result is True diff --git a/tests/unit/resources/billing/test_journals.py b/tests/unit/resources/billing/test_journals.py index 62b1ed1f..af05a0c3 100644 --- a/tests/unit/resources/billing/test_journals.py +++ b/tests/unit/resources/billing/test_journals.py @@ -1,4 +1,6 @@ +import httpx import pytest +import respx from mpt_api_client.resources.billing.journal_attachments import ( AsyncJournalAttachmentsService, @@ -12,10 +14,6 @@ AsyncJournalSellersService, JournalSellersService, ) -from mpt_api_client.resources.billing.journal_upload import ( - AsyncJournalUploadService, - JournalUploadService, -) from mpt_api_client.resources.billing.journals import AsyncJournalsService, JournalsService @@ -31,7 +29,7 @@ def async_journals_service(async_http_client): @pytest.mark.parametrize( "method", - ["get", "create", "update", "delete", "regenerate", "submit", "enquiry", "accept"], + ["get", "create", "update", "delete", "regenerate", "submit", "enquiry", "accept", "upload"], ) def test_mixins_present(journals_service, method): result = hasattr(journals_service, method) @@ -41,7 +39,7 @@ def test_mixins_present(journals_service, method): @pytest.mark.parametrize( "method", - ["get", "create", "update", "delete", "regenerate", "submit", "enquiry", "accept"], + ["get", "create", "update", "delete", "regenerate", "submit", "enquiry", "accept", "upload"], ) def test_async_mixins_present(async_journals_service, method): result = hasattr(async_journals_service, method) @@ -55,7 +53,6 @@ def test_async_mixins_present(async_journals_service, method): ("attachments", JournalAttachmentsService), ("sellers", JournalSellersService), ("charges", JournalChargesService), - ("upload", JournalUploadService), ], ) def test_property_services(journals_service, service_method, expected_service_class): @@ -71,7 +68,6 @@ def test_property_services(journals_service, service_method, expected_service_cl ("attachments", AsyncJournalAttachmentsService), ("sellers", AsyncJournalSellersService), ("charges", AsyncJournalChargesService), - ("upload", AsyncJournalUploadService), ], ) def test_async_property_services(async_journals_service, service_method, expected_service_class): @@ -79,3 +75,67 @@ def test_async_property_services(async_journals_service, service_method, expecte assert isinstance(result, expected_service_class) assert result.endpoint_params == {"journal_id": "JRN-0000-0001"} + + +def test_upload(journals_service, tmp_path) -> None: + file_path = tmp_path / "journal.jsonl" + file_path.write_text("test data") + with file_path.open("rb") as file_obj, respx.mock: + mock_route = respx.post( + "https://api.example.com/public/v1/billing/journals/JRN-0000-0001/upload" + ).mock(return_value=httpx.Response(200, json={"result": "ok"})) + + result = journals_service.upload( + journal_id="JRN-0000-0001", + file=file_obj, + ) + + assert mock_route.called + assert result is not None + + +async def test_async_upload(async_journals_service, tmp_path) -> None: + file_path = tmp_path / "journal.jsonl" + file_path.write_text("test data") + with file_path.open("rb") as file_obj, respx.mock: + mock_route = respx.post( + "https://api.example.com/public/v1/billing/journals/JRN-0000-0001/upload" + ).mock(return_value=httpx.Response(200, json={"result": "ok"})) + + result = await async_journals_service.upload( + journal_id="JRN-0000-0001", + file=file_obj, + ) + + assert mock_route.called + assert result is not None + + +def test_upload_without_file(journals_service) -> None: + with respx.mock: + mock_route = respx.post( + "https://api.example.com/public/v1/billing/journals/JRN-0000-0001/upload" + ).mock(return_value=httpx.Response(200, json={"result": "ok"})) + + result = journals_service.upload( + journal_id="JRN-0000-0001", + file=None, + ) + + assert mock_route.called + assert result is not None + + +async def test_async_upload_without_file(async_journals_service) -> None: + with respx.mock: + mock_route = respx.post( + "https://api.example.com/public/v1/billing/journals/JRN-0000-0001/upload" + ).mock(return_value=httpx.Response(200, json={"result": "ok"})) + + result = await async_journals_service.upload( + journal_id="JRN-0000-0001", + file=None, + ) + + assert mock_route.called + assert result is not None