diff --git a/e2e_config.test.json b/e2e_config.test.json index a61e5d35..ae9ec468 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -12,6 +12,9 @@ "accounts.user.id": "USR-9673-3314", "accounts.user_group.id": "UGR-6822-0561", "audit.record.id": "AUD-3748-4760-1006-3938", + "billing.custom_ledger.attachment.id": "CLA-1777-7485", + "billing.custom_ledger.charge.id": "CHG-2665-3524-0000-0000-0020", + "billing.custom_ledger.id": "CLE-2665-3524", "billing.journal.attachment.id": "JOA-6425-9776", "billing.journal.id": "BJO-6562-0928", "catalog.authorization.id": "AUT-9288-6146", diff --git a/mpt_api_client/constants.py b/mpt_api_client/constants.py index 438cc0cb..c0b1552d 100644 --- a/mpt_api_client/constants.py +++ b/mpt_api_client/constants.py @@ -1 +1,2 @@ APPLICATION_JSON = "application/json" +MIMETYPE_EXCEL_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" diff --git a/mpt_api_client/resources/billing/custom_ledger_upload.py b/mpt_api_client/resources/billing/custom_ledger_upload.py deleted file mode 100644 index 47351df2..00000000 --- a/mpt_api_client/resources/billing/custom_ledger_upload.py +++ /dev/null @@ -1,31 +0,0 @@ -from mpt_api_client.http import AsyncService, Service -from mpt_api_client.http.mixins import AsyncFilesOperationsMixin, FilesOperationsMixin -from mpt_api_client.models import Model - - -class CustomLedgerUpload(Model): - """Custom Ledger Upload resource.""" - - -class CustomLedgerUploadServiceConfig: - """Custom Ledger Upload service configuration.""" - - _endpoint = "/public/v1/billing/custom-ledgers/{custom_ledger_id}/upload" - _model_class = CustomLedgerUpload - _collection_key = "data" - - -class CustomLedgerUploadService( - FilesOperationsMixin[CustomLedgerUpload], - Service[CustomLedgerUpload], - CustomLedgerUploadServiceConfig, -): - """Custom Ledger Upload service.""" - - -class AsyncCustomLedgerUploadService( - AsyncFilesOperationsMixin[CustomLedgerUpload], - AsyncService[CustomLedgerUpload], - CustomLedgerUploadServiceConfig, -): - """Async Custom Ledger Upload service.""" diff --git a/mpt_api_client/resources/billing/custom_ledgers.py b/mpt_api_client/resources/billing/custom_ledgers.py index e5253f2f..1d1b45f7 100644 --- a/mpt_api_client/resources/billing/custom_ledgers.py +++ b/mpt_api_client/resources/billing/custom_ledgers.py @@ -1,3 +1,8 @@ +import pathlib +from typing import cast +from urllib.parse import urljoin + +from mpt_api_client.constants import MIMETYPE_EXCEL_XLSX from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, @@ -5,6 +10,7 @@ CollectionMixin, ManagedResourceMixin, ) +from mpt_api_client.http.types import FileContent, FileTypes from mpt_api_client.models import Model from mpt_api_client.resources.billing.custom_ledger_attachments import ( AsyncCustomLedgerAttachmentsService, @@ -14,10 +20,6 @@ AsyncCustomLedgerChargesService, CustomLedgerChargesService, ) -from mpt_api_client.resources.billing.custom_ledger_upload import ( - AsyncCustomLedgerUploadService, - CustomLedgerUploadService, -) from mpt_api_client.resources.billing.mixins import AcceptableMixin, AsyncAcceptableMixin @@ -31,6 +33,8 @@ class CustomLedgersServiceConfig: _endpoint = "/public/v1/billing/custom-ledgers" _model_class = CustomLedger _collection_key = "data" + _upload_file_key = "file" + _upload_data_key = "id" class CustomLedgersService( @@ -42,6 +46,41 @@ class CustomLedgersService( ): """Custom Ledgers service.""" + def upload(self, custom_ledger_id: str, file: FileTypes) -> CustomLedger: + """Upload custom ledger file. + + Args: + custom_ledger_id: Custom Ledger ID. + file: Custom Ledger file. + + Returns: + CustomLedger: Created resource. + """ + files: dict[str, FileTypes] = {} + + filename = pathlib.Path(getattr(file, "name", "uploaded_file.xlsx")).name + + # Mimetype is set to Excel XLSX to prevent 415 response from the server + files[self._upload_file_key] = ( + filename, + cast("FileContent", file), + MIMETYPE_EXCEL_XLSX, + ) # UNUSED type: ignore[attr-defined] + files[self._upload_data_key] = custom_ledger_id # UNUSED type: ignore + + path = urljoin(f"{self.path}/", f"{custom_ledger_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 charges(self, custom_ledger_id: str) -> CustomLedgerChargesService: """Return custom ledger charges service.""" return CustomLedgerChargesService( @@ -49,13 +88,6 @@ def charges(self, custom_ledger_id: str) -> CustomLedgerChargesService: endpoint_params={"custom_ledger_id": custom_ledger_id}, ) - def upload(self, custom_ledger_id: str) -> CustomLedgerUploadService: - """Get the Custom Ledger Upload service.""" - return CustomLedgerUploadService( - http_client=self.http_client, - endpoint_params={"custom_ledger_id": custom_ledger_id}, - ) - def attachments(self, custom_ledger_id: str) -> CustomLedgerAttachmentsService: """Return custom ledger attachments service.""" return CustomLedgerAttachmentsService( @@ -73,6 +105,41 @@ class AsyncCustomLedgersService( ): """Async Custom Ledgers service.""" + async def upload(self, custom_ledger_id: str, file: FileTypes) -> CustomLedger: + """Upload custom ledger file. + + Args: + custom_ledger_id: Custom Ledger ID. + file: Custom Ledger file. + + Returns: + CustomLedger: Created resource. + """ + files: dict[str, FileTypes] = {} + + filename = pathlib.Path(getattr(file, "name", "uploaded_file.xlsx")).name + + # Mimetype is set to Excel XLSX to prevent 415 response from the server + files[self._upload_file_key] = ( + filename, + cast("FileContent", file), + MIMETYPE_EXCEL_XLSX, + ) # UNUSED type: ignore[attr-defined] + files[self._upload_data_key] = custom_ledger_id # UNUSED type: ignore + + path = urljoin(f"{self.path}/", f"{custom_ledger_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 charges(self, custom_ledger_id: str) -> AsyncCustomLedgerChargesService: """Return custom ledger charges service.""" return AsyncCustomLedgerChargesService( @@ -80,13 +147,6 @@ def charges(self, custom_ledger_id: str) -> AsyncCustomLedgerChargesService: endpoint_params={"custom_ledger_id": custom_ledger_id}, ) - def upload(self, custom_ledger_id: str) -> AsyncCustomLedgerUploadService: - """Get the Async Custom Ledger Upload service.""" - return AsyncCustomLedgerUploadService( - http_client=self.http_client, - endpoint_params={"custom_ledger_id": custom_ledger_id}, - ) - def attachments(self, custom_ledger_id: str) -> AsyncCustomLedgerAttachmentsService: """Return custom ledger attachments service.""" return AsyncCustomLedgerAttachmentsService( diff --git a/pyproject.toml b/pyproject.toml index e8d1c03d..9b7623c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,13 +119,14 @@ per-file-ignores = [ "mpt_api_client/mpt_client.py: WPS214 WPS235", "mpt_api_client/resources/*: WPS215", "mpt_api_client/resources/accounts/*.py: WPS202 WPS215 WPS214 WPS235 WPS453", - "mpt_api_client/resources/billing/*.py: WPS202 WPS204 WPS214 WPS215 WPS235", + "mpt_api_client/resources/billing/*.py: WPS202 WPS204 WPS214 WPS215 WPS235 WPS110", "mpt_api_client/resources/catalog/*.py: WPS110 WPS214 WPS215 WPS235", "mpt_api_client/resources/catalog/mixins.py: WPS110 WPS202 WPS214 WPS215 WPS235", "mpt_api_client/resources/catalog/products.py: WPS204 WPS214 WPS215 WPS235", "mpt_api_client/resources/commerce/*.py: WPS235 WPS215", "mpt_api_client/rql/query_builder.py: WPS110 WPS115 WPS210 WPS214", "tests/e2e/accounts/*.py: WPS430 WPS202", + "tests/e2e/billing/*.py: WPS202 WPS421 WPS118", "tests/e2e/catalog/*.py: WPS202 WPS421", "tests/e2e/catalog/items/*.py: WPS110 WPS202", "tests/e2e/commerce/*.py: WPS202 WPS204 WPS453", diff --git a/tests/data/test_custom_ledger.xlsx b/tests/data/test_custom_ledger.xlsx new file mode 100644 index 00000000..960c48b6 Binary files /dev/null and b/tests/data/test_custom_ledger.xlsx differ diff --git a/tests/e2e/billing/custom_ledger/attachment/conftest.py b/tests/e2e/billing/custom_ledger/attachment/conftest.py new file mode 100644 index 00000000..b90b1842 --- /dev/null +++ b/tests/e2e/billing/custom_ledger/attachment/conftest.py @@ -0,0 +1,24 @@ +import pytest + + +@pytest.fixture +def custom_ledger_attachment_id(e2e_config): + return e2e_config["billing.custom_ledger.attachment.id"] + + +@pytest.fixture +def invalid_custom_ledger_attachment_id(): + return "CLA-0000-0000" + + +@pytest.fixture +def custom_ledger_attachment_factory(): + def factory( + name: str = "E2E Created Custom Ledger Attachment", + ): + return { + "name": name, + "description": name, + } + + return factory diff --git a/tests/e2e/billing/custom_ledger/attachment/test_async_custom_ledger_attachment.py b/tests/e2e/billing/custom_ledger/attachment/test_async_custom_ledger_attachment.py new file mode 100644 index 00000000..081b3fe0 --- /dev/null +++ b/tests/e2e/billing/custom_ledger/attachment/test_async_custom_ledger_attachment.py @@ -0,0 +1,110 @@ +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_custom_ledger_attachment( + async_mpt_ops, custom_ledger_attachment_factory, custom_ledger_id, pdf_fd +): + new_custom_ledger_attachment_request_data = custom_ledger_attachment_factory( + name="E2E Created Custom Ledger Attachment", + ) + custom_ledger_attachments = async_mpt_ops.billing.custom_ledgers.attachments(custom_ledger_id) + + created_attachment = await custom_ledger_attachments.create( + new_custom_ledger_attachment_request_data, file=pdf_fd + ) + + yield created_attachment + + try: + await custom_ledger_attachments.delete(created_attachment.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete custom ledger attachment: {error.title}") # noqa: WPS421 + + +@pytest.fixture +def custom_ledger_attachments(async_mpt_ops, custom_ledger_id): + return async_mpt_ops.billing.custom_ledgers.attachments(custom_ledger_id) + + +async def test_get_custom_ledger_attachment_by_id( + custom_ledger_attachments, custom_ledger_attachment_id +): + result = await custom_ledger_attachments.get(custom_ledger_attachment_id) + + assert result is not None + + +async def test_get_custom_ledger_attachment_by_id_not_found( + custom_ledger_attachments, invalid_custom_ledger_attachment_id +): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await custom_ledger_attachments.get(invalid_custom_ledger_attachment_id) + + +async def test_list_custom_ledger_attachments(custom_ledger_attachments): + limit = 10 + + result = await custom_ledger_attachments.fetch_page(limit=limit) + + assert len(result) > 0 + + +async def test_filter_custom_ledger_attachments( + custom_ledger_attachments, custom_ledger_attachment_id +): + select_fields = ["-price"] + filtered_attachments = ( + custom_ledger_attachments.filter(RQLQuery(id=custom_ledger_attachment_id)) + .filter(RQLQuery(name="test_custom_ledger.xlsx")) + .select(*select_fields) + ) + + result = [attachment async for attachment in filtered_attachments.iterate()] + + assert len(result) == 1 + + +def test_create_billing_custom_ledger_attachment(created_custom_ledger_attachment): + result = created_custom_ledger_attachment + + assert result is not None + + +async def test_update_billing_custom_ledger_attachment( + custom_ledger_attachments, created_custom_ledger_attachment +): + updated_name = "E2E Updated Custom Ledger Attachment" + + update_data = { + "name": updated_name, + } + + updated_attachment = await custom_ledger_attachments.update( + created_custom_ledger_attachment.id, + update_data, + ) + + assert updated_attachment.name == updated_name + + +async def test_delete_billing_custom_ledger_attachment( + custom_ledger_attachments, created_custom_ledger_attachment +): + result = created_custom_ledger_attachment + + await custom_ledger_attachments.delete(result.id) + + +async def test_download_billing_custom_ledger_attachment( + custom_ledger_attachments, created_custom_ledger_attachment +): + result = await custom_ledger_attachments.download(created_custom_ledger_attachment.id) + + assert result.file_contents is not None + assert result.filename is not None diff --git a/tests/e2e/billing/custom_ledger/attachment/test_sync_custom_ledger_attachment.py b/tests/e2e/billing/custom_ledger/attachment/test_sync_custom_ledger_attachment.py new file mode 100644 index 00000000..c7ab5401 --- /dev/null +++ b/tests/e2e/billing/custom_ledger/attachment/test_sync_custom_ledger_attachment.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_custom_ledger_attachment( + mpt_ops, custom_ledger_attachment_factory, custom_ledger_id, pdf_fd +): + new_custom_ledger_attachment_request_data = custom_ledger_attachment_factory( + name="E2E Created Custom Ledger Attachment", + ) + custom_ledger_attachments = mpt_ops.billing.custom_ledgers.attachments(custom_ledger_id) + + created_attachment = custom_ledger_attachments.create( + new_custom_ledger_attachment_request_data, file=pdf_fd + ) + + yield created_attachment + + try: + custom_ledger_attachments.delete(created_attachment.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete custom ledger attachment: {error.title}") # noqa: WPS421 + + +@pytest.fixture +def custom_ledger_attachments(mpt_ops, custom_ledger_id): + return mpt_ops.billing.custom_ledgers.attachments(custom_ledger_id) + + +def test_get_custom_ledger_attachment_by_id(custom_ledger_attachments, custom_ledger_attachment_id): + result = custom_ledger_attachments.get(custom_ledger_attachment_id) + + assert result is not None + + +def test_get_custom_ledger_attachment_by_id_not_found( + custom_ledger_attachments, invalid_custom_ledger_attachment_id +): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + custom_ledger_attachments.get(invalid_custom_ledger_attachment_id) + + +def test_list_custom_ledger_attachments(custom_ledger_attachments): + limit = 10 + + result = custom_ledger_attachments.fetch_page(limit=limit) + + assert len(result) > 0 + + +def test_filter_custom_ledger_attachments(custom_ledger_attachments, custom_ledger_attachment_id): + select_fields = ["-price"] + filtered_attachments = ( + custom_ledger_attachments.filter(RQLQuery(id=custom_ledger_attachment_id)) + .filter(RQLQuery(name="test_custom_ledger.xlsx")) + .select(*select_fields) + ) + + result = list(filtered_attachments.iterate()) + + assert len(result) == 1 + + +def test_create_billing_custom_ledger_attachment(created_custom_ledger_attachment): + result = created_custom_ledger_attachment + + assert result is not None + + +def test_update_billing_custom_ledger_attachment( + custom_ledger_attachments, created_custom_ledger_attachment +): + updated_name = "E2E Updated Custom Ledger Attachment" + update_data = { + "name": updated_name, + } + + result = custom_ledger_attachments.update( + created_custom_ledger_attachment.id, + update_data, + ) + + assert result is not None + + +def test_delete_billing_custom_ledger_attachment( + custom_ledger_attachments, created_custom_ledger_attachment +): + result = created_custom_ledger_attachment + + custom_ledger_attachments.delete(result.id) + + +def test_download_billing_custom_ledger_attachment( + custom_ledger_attachments, created_custom_ledger_attachment +): + result = custom_ledger_attachments.download(created_custom_ledger_attachment.id) + + assert result.file_contents is not None + assert result.filename is not None diff --git a/tests/e2e/billing/custom_ledger/charge/conftest.py b/tests/e2e/billing/custom_ledger/charge/conftest.py new file mode 100644 index 00000000..7199a377 --- /dev/null +++ b/tests/e2e/billing/custom_ledger/charge/conftest.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.fixture +def custom_ledger_charge_id(e2e_config): + return e2e_config["billing.custom_ledger.charge.id"] + + +@pytest.fixture +def invalid_custom_ledger_charge_id(): + return "CHG-0000-0000-0000-0000-0000" diff --git a/tests/e2e/billing/custom_ledger/charge/test_async_custom_ledger_charges.py b/tests/e2e/billing/custom_ledger/charge/test_async_custom_ledger_charges.py new file mode 100644 index 00000000..a5fb3293 --- /dev/null +++ b/tests/e2e/billing/custom_ledger/charge/test_async_custom_ledger_charges.py @@ -0,0 +1,45 @@ +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 custom_ledger_charges(async_mpt_ops, custom_ledger_id): + return async_mpt_ops.billing.custom_ledgers.charges(custom_ledger_id) + + +async def test_get_custom_ledger_charge_by_id(custom_ledger_charges, custom_ledger_charge_id): + result = await custom_ledger_charges.get(custom_ledger_charge_id) + + assert result is not None + + +async def test_get_custom_ledger_charge_by_id_not_found( + custom_ledger_charges, invalid_custom_ledger_charge_id +): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await custom_ledger_charges.get(invalid_custom_ledger_charge_id) + + +async def test_list_custom_ledger_charges(custom_ledger_charges): + limit = 10 + + result = await custom_ledger_charges.fetch_page(limit=limit) + + assert len(result) > 0 + + +async def test_filter_custom_ledger_charges(custom_ledger_charges, custom_ledger_charge_id): + select_fields = ["-price"] + filtered_charges = ( + custom_ledger_charges.filter(RQLQuery(id=custom_ledger_charge_id)) + .filter(RQLQuery(description__value2="Description 2")) + .select(*select_fields) + ) + + result = [charge async for charge in filtered_charges.iterate()] + + assert len(result) == 1 diff --git a/tests/e2e/billing/custom_ledger/charge/test_sync_custom_ledger_charges.py b/tests/e2e/billing/custom_ledger/charge/test_sync_custom_ledger_charges.py new file mode 100644 index 00000000..8db1bc4c --- /dev/null +++ b/tests/e2e/billing/custom_ledger/charge/test_sync_custom_ledger_charges.py @@ -0,0 +1,45 @@ +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 custom_ledger_charges(mpt_ops, custom_ledger_id): + return mpt_ops.billing.custom_ledgers.charges(custom_ledger_id) + + +def test_get_custom_ledger_charge_by_id(custom_ledger_charges, custom_ledger_charge_id): + result = custom_ledger_charges.get(custom_ledger_charge_id) + + assert result is not None + + +def test_get_custom_ledger_charge_by_id_not_found( + custom_ledger_charges, invalid_custom_ledger_charge_id +): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + custom_ledger_charges.get(invalid_custom_ledger_charge_id) + + +def test_list_custom_ledger_charges(custom_ledger_charges): + limit = 10 + + result = custom_ledger_charges.fetch_page(limit=limit) + + assert len(result) > 0 + + +def test_filter_custom_ledger_charges(custom_ledger_charges, custom_ledger_charge_id): + select_fields = ["-price"] + filtered_charges = ( + custom_ledger_charges.filter(RQLQuery(id=custom_ledger_charge_id)) + .filter(RQLQuery(description__value2="Description 2")) + .select(*select_fields) + ) + + result = list(filtered_charges.iterate()) + + assert len(result) == 1 diff --git a/tests/e2e/billing/custom_ledger/conftest.py b/tests/e2e/billing/custom_ledger/conftest.py new file mode 100644 index 00000000..6ebefbae --- /dev/null +++ b/tests/e2e/billing/custom_ledger/conftest.py @@ -0,0 +1,53 @@ +import pathlib + +import pytest +from freezegun import freeze_time + + +@pytest.fixture +def custom_ledger_id(e2e_config): + return e2e_config["billing.custom_ledger.id"] + + +@pytest.fixture +def invalid_custom_ledger_id(): + return "CLD-0000-0000" + + +@pytest.fixture +@freeze_time("2025-12-01T10:00:00.000Z") +def custom_ledger_factory( + seller_id, + account_id, +): + def factory( + name: str = "E2E Created Custom Ledger", + notes: str = "E2E Created Custom Ledger", + ): + return { + "name": name, + "seller": {"id": seller_id}, + "vendor": {"id": account_id}, + "price": { + "currency": { + "purchase": "USD", + "sale": "USD", + }, + }, + "notes": notes, + "billingStartDate": "2025-12-01T07:00:00.000Z", + "billingEndDate": "2026-11-30T07:00:00.000Z", + "externalIds": {}, + } + + return factory + + +@pytest.fixture +def billing_custom_ledger_fd(): + file_path = pathlib.Path("tests/data/test_custom_ledger.xlsx").resolve() + fd = file_path.open("rb") + try: + yield fd + finally: + fd.close() diff --git a/tests/e2e/billing/custom_ledger/test_async_custom_ledger.py b/tests/e2e/billing/custom_ledger/test_async_custom_ledger.py new file mode 100644 index 00000000..162837d1 --- /dev/null +++ b/tests/e2e/billing/custom_ledger/test_async_custom_ledger.py @@ -0,0 +1,89 @@ +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_custom_ledger(async_mpt_ops, custom_ledger_factory): + new_custom_ledger_request_data = custom_ledger_factory() + + created_custom_ledger = await async_mpt_ops.billing.custom_ledgers.create( + new_custom_ledger_request_data + ) + + yield created_custom_ledger + + try: + await async_mpt_ops.billing.custom_ledgers.delete(created_custom_ledger.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete custom ledger: {error.title}") + + +async def test_get_custom_ledger_by_id(async_mpt_ops, custom_ledger_id): + result = await async_mpt_ops.billing.custom_ledgers.get(custom_ledger_id) + + assert result is not None + + +async def test_list_custom_ledgers(async_mpt_ops): + limit = 10 + + result = await async_mpt_ops.billing.custom_ledgers.fetch_page(limit=limit) + + assert len(result) > 0 + + +async def test_get_custom_ledger_by_id_not_found(async_mpt_ops, invalid_custom_ledger_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.billing.custom_ledgers.get(invalid_custom_ledger_id) + + +async def test_filter_custom_ledgers(async_mpt_ops, custom_ledger_id): + select_fields = ["-price"] + filtered_custom_ledgers = ( + async_mpt_ops.billing.custom_ledgers.filter(RQLQuery(id=custom_ledger_id)) + .filter(RQLQuery(name="E2E Seeded Custom Ledger")) + .select(*select_fields) + ) + + result = [custom_ledger async for custom_ledger in filtered_custom_ledgers.iterate()] + + assert len(result) == 1 + + +def test_create_custom_ledger(created_custom_ledger): + result = created_custom_ledger + + assert result is not None + + +async def test_update_custom_ledger(async_mpt_ops, created_custom_ledger): + name = "E2E Updated Custom Ledger" + update_data = { + "name": name, + } + + result = await async_mpt_ops.billing.custom_ledgers.update( + created_custom_ledger.id, + update_data, + ) + + assert result is not None + + +async def test_delete_custom_ledger(async_mpt_ops, created_custom_ledger): + result = created_custom_ledger + + await async_mpt_ops.billing.custom_ledgers.delete(result.id) + + +async def test_upload_custom_ledger(async_mpt_ops, created_custom_ledger, billing_custom_ledger_fd): + result = await async_mpt_ops.billing.custom_ledgers.upload( + custom_ledger_id=created_custom_ledger.id, + file=billing_custom_ledger_fd, + ) + + assert result is not None diff --git a/tests/e2e/billing/custom_ledger/test_sync_custom_ledger.py b/tests/e2e/billing/custom_ledger/test_sync_custom_ledger.py new file mode 100644 index 00000000..5e537b30 --- /dev/null +++ b/tests/e2e/billing/custom_ledger/test_sync_custom_ledger.py @@ -0,0 +1,87 @@ +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_custom_ledger(mpt_ops, custom_ledger_factory): + new_custom_ledger_request_data = custom_ledger_factory() + + created_custom_ledger = mpt_ops.billing.custom_ledgers.create(new_custom_ledger_request_data) + + yield created_custom_ledger + + try: + mpt_ops.billing.custom_ledgers.delete(created_custom_ledger.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete custom ledger: {error.title}") + + +def test_get_custom_ledger_by_id(mpt_ops, custom_ledger_id): + result = mpt_ops.billing.custom_ledgers.get(custom_ledger_id) + + assert result is not None + + +def test_list_custom_ledgers(mpt_ops): + limit = 10 + + result = mpt_ops.billing.custom_ledgers.fetch_page(limit=limit) + + assert len(result) > 0 + + +def test_get_custom_ledger_by_id_not_found(mpt_ops, invalid_custom_ledger_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.billing.custom_ledgers.get(invalid_custom_ledger_id) + + +def test_filter_custom_ledgers(mpt_ops, custom_ledger_id): + select_fields = ["-price"] + filtered_custom_ledgers = ( + mpt_ops.billing.custom_ledgers.filter(RQLQuery(id=custom_ledger_id)) + .filter(RQLQuery(name="E2E Seeded Custom Ledger")) + .select(*select_fields) + ) + + result = list(filtered_custom_ledgers.iterate()) + + assert len(result) == 1 + + +def test_create_custom_ledger(created_custom_ledger): + result = created_custom_ledger + + assert result is not None + + +def test_update_custom_ledger(mpt_ops, created_custom_ledger): + name = "E2E Updated Custom Ledger" + update_data = { + "name": name, + } + + result = mpt_ops.billing.custom_ledgers.update( + created_custom_ledger.id, + update_data, + ) + + assert result is not None + + +def test_delete_custom_ledger(mpt_ops, created_custom_ledger): + result = created_custom_ledger + + mpt_ops.billing.custom_ledgers.delete(result.id) + + +def test_upload_custom_ledger(mpt_ops, created_custom_ledger, billing_custom_ledger_fd): + result = mpt_ops.billing.custom_ledgers.upload( + custom_ledger_id=created_custom_ledger.id, + file=billing_custom_ledger_fd, + ) + + assert result is not None diff --git a/tests/unit/resources/billing/test_custom_ledger_upload.py b/tests/unit/resources/billing/test_custom_ledger_upload.py deleted file mode 100644 index 9d0a7255..00000000 --- a/tests/unit/resources/billing/test_custom_ledger_upload.py +++ /dev/null @@ -1,50 +0,0 @@ -import pytest - -from mpt_api_client.resources.billing.custom_ledger_upload import ( - AsyncCustomLedgerUploadService, - CustomLedgerUploadService, -) - - -@pytest.fixture -def custom_ledger_upload_service(http_client): - return CustomLedgerUploadService( - http_client=http_client, endpoint_params={"custom_ledger_id": "LDG-0000-0001"} - ) - - -@pytest.fixture -def async_custom_ledger_upload_service(http_client): - return AsyncCustomLedgerUploadService( - http_client=http_client, endpoint_params={"custom_ledger_id": "LDG-0000-0001"} - ) - - -def test_endpoint(custom_ledger_upload_service): - result = custom_ledger_upload_service.path == ( - "/public/v1/billing/custom-ledgers/LDG-0000-0001/upload" - ) - - assert result is True - - -def test_async_endpoint(async_custom_ledger_upload_service): - result = async_custom_ledger_upload_service.path == ( - "/public/v1/billing/custom-ledgers/LDG-0000-0001/upload" - ) - - assert result is True - - -@pytest.mark.parametrize("method", ["create"]) -def test_mixins_present(custom_ledger_upload_service, method): - result = hasattr(custom_ledger_upload_service, method) - - assert result is True - - -@pytest.mark.parametrize("method", ["create"]) -def test_async_mixins_present(async_custom_ledger_upload_service, method): - result = hasattr(async_custom_ledger_upload_service, method) - - assert result is True diff --git a/tests/unit/resources/billing/test_custom_ledgers.py b/tests/unit/resources/billing/test_custom_ledgers.py index e44b9443..ca141c37 100644 --- a/tests/unit/resources/billing/test_custom_ledgers.py +++ b/tests/unit/resources/billing/test_custom_ledgers.py @@ -1,4 +1,6 @@ +import httpx import pytest +import respx from mpt_api_client.resources.billing.custom_ledger_attachments import ( AsyncCustomLedgerAttachmentsService, @@ -8,10 +10,6 @@ AsyncCustomLedgerChargesService, CustomLedgerChargesService, ) -from mpt_api_client.resources.billing.custom_ledger_upload import ( - AsyncCustomLedgerUploadService, - CustomLedgerUploadService, -) from mpt_api_client.resources.billing.custom_ledgers import ( AsyncCustomLedgersService, CustomLedgersService, @@ -24,18 +22,22 @@ def custom_ledgers_service(http_client): @pytest.fixture -def async_custom_ledgers_service(http_client): - return AsyncCustomLedgersService(http_client=http_client) +def async_custom_ledgers_service(async_http_client): + return AsyncCustomLedgersService(http_client=async_http_client) -@pytest.mark.parametrize("method", ["get", "create", "update", "delete", "accept", "queue"]) +@pytest.mark.parametrize( + "method", ["get", "create", "update", "delete", "accept", "queue", "upload"] +) def test_mixins_present(custom_ledgers_service, method): result = hasattr(custom_ledgers_service, method) assert result is True -@pytest.mark.parametrize("method", ["get", "create", "update", "delete", "accept", "queue"]) +@pytest.mark.parametrize( + "method", ["get", "create", "update", "delete", "accept", "queue", "upload"] +) def test_async_mixins_present(async_custom_ledgers_service, method): result = hasattr(async_custom_ledgers_service, method) @@ -46,7 +48,6 @@ def test_async_mixins_present(async_custom_ledgers_service, method): ("service_method", "expected_service_class"), [ ("charges", CustomLedgerChargesService), - ("upload", CustomLedgerUploadService), ("attachments", CustomLedgerAttachmentsService), ], ) @@ -61,7 +62,6 @@ def test_property_services(custom_ledgers_service, service_method, expected_serv ("service_method", "expected_service_class"), [ ("charges", AsyncCustomLedgerChargesService), - ("upload", AsyncCustomLedgerUploadService), ("attachments", AsyncCustomLedgerAttachmentsService), ], ) @@ -72,3 +72,34 @@ def test_async_property_services( assert isinstance(result, expected_service_class) assert result.endpoint_params == {"custom_ledger_id": "LDG-0000-0001"} + + +def test_upload(custom_ledgers_service, tmp_path): + file_path = tmp_path / "test_upload.xlsx" + file_path.write_bytes(b"Test content") + with file_path.open("rb") as file_obj, respx.mock: + mock_route = respx.post( + "https://api.example.com/public/v1/billing/custom-ledgers/LDG-0000-0001/upload" + ).mock(return_value=httpx.Response(200, json={"result": "ok"})) + + result = custom_ledgers_service.upload(custom_ledger_id="LDG-0000-0001", file=file_obj) + + assert mock_route.called + assert result is not None + + +async def test_async_upload(async_custom_ledgers_service, tmp_path): + file_path = tmp_path / "test_upload.xlsx" + file_path.write_bytes(b"Test content") + with file_path.open("rb") as file_obj, respx.mock: + mock_route = respx.post( + "https://api.example.com/public/v1/billing/custom-ledgers/LDG-0000-0001/upload" + ).mock(return_value=httpx.Response(200, json={"result": "ok"})) + + result = await async_custom_ledgers_service.upload( + custom_ledger_id="LDG-0000-0001", + file=file_obj, + ) + + assert mock_route.called + assert result is not None