diff --git a/paddle_billing_python_sdk/Entities/Reports/ReportFilters.py b/paddle_billing_python_sdk/Entities/Reports/ReportFilters.py index 49d3230..d86c757 100644 --- a/paddle_billing_python_sdk/Entities/Reports/ReportFilters.py +++ b/paddle_billing_python_sdk/Entities/Reports/ReportFilters.py @@ -1,5 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import asdict, dataclass from paddle_billing_python_sdk.Entities.Reports.ReportName import ReportName from paddle_billing_python_sdk.Entities.Reports.ReportOperator import ReportOperator @@ -15,3 +15,7 @@ class ReportFilters: @staticmethod def from_dict(data: dict) -> ReportFilters: return ReportFilters(**data) + + + def get_parameters(self) -> dict: + return asdict(self) diff --git a/paddle_billing_python_sdk/Resources/Reports/Operations/CreateReport.py b/paddle_billing_python_sdk/Resources/Reports/Operations/CreateReport.py index d893c02..8277850 100644 --- a/paddle_billing_python_sdk/Resources/Reports/Operations/CreateReport.py +++ b/paddle_billing_python_sdk/Resources/Reports/Operations/CreateReport.py @@ -1,4 +1,4 @@ -from dataclasses import asdict, dataclass, field +from dataclasses import dataclass, field from paddle_billing_python_sdk.Undefined import Undefined @@ -11,7 +11,7 @@ @dataclass class CreateReport: type: ReportType - filters: list[ReportFilters] | None | Undefined = field(default_factory=list) + filters: list[ReportFilters] = field(default_factory=list) def __post_init__(self): @@ -22,4 +22,9 @@ def __post_init__(self): def get_parameters(self) -> dict: - return asdict(self) + parameters = {'type': self.type} + + if self.filters is not None and self.filters != []: + parameters.update({'filters': [filter.get_parameters() for filter in self.filters]}) + + return parameters diff --git a/paddle_billing_python_sdk/Resources/Reports/Operations/ListReports.py b/paddle_billing_python_sdk/Resources/Reports/Operations/ListReports.py index ded9dfa..d1c37cd 100644 --- a/paddle_billing_python_sdk/Resources/Reports/Operations/ListReports.py +++ b/paddle_billing_python_sdk/Resources/Reports/Operations/ListReports.py @@ -1,7 +1,7 @@ from paddle_billing_python_sdk.EnumStringify import enum_stringify from paddle_billing_python_sdk.HasParameters import HasParameters -from paddle_billing_python_sdk.Entities.Shared.Status import Status +from paddle_billing_python_sdk.Entities.Reports.ReportStatus import ReportStatus from paddle_billing_python_sdk.Exceptions.SdkExceptions.InvalidArgumentException import InvalidArgumentException @@ -11,14 +11,14 @@ class ListReports(HasParameters): def __init__( self, - pager: Pager = None, - statuses: list[Status] = None, + pager: Pager = None, + statuses: list[ReportStatus] = None, ): self.pager = pager self.statuses = statuses if statuses is not None else [] # Validation - for field_name, field_value, field_type in [('statuses', self.statuses, Status),]: + for field_name, field_value, field_type in [('statuses', self.statuses, ReportStatus)]: invalid_items = [item for item in field_value if not isinstance(item, field_type)] if invalid_items: raise InvalidArgumentException(field_name, field_type.__name__, invalid_items) diff --git a/paddle_billing_python_sdk/__VERSION__.py b/paddle_billing_python_sdk/__VERSION__.py index 7c94d90..709116d 100644 --- a/paddle_billing_python_sdk/__VERSION__.py +++ b/paddle_billing_python_sdk/__VERSION__.py @@ -1 +1 @@ -__VERSION__ = '0.0.1a77' +__VERSION__ = '0.0.1a78' diff --git a/tests/Functional/Resources/Reports/__init__.py b/tests/Functional/Resources/Reports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/Functional/Resources/Reports/_fixtures/request/create_basic.json b/tests/Functional/Resources/Reports/_fixtures/request/create_basic.json new file mode 100644 index 0000000..8778cb5 --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/request/create_basic.json @@ -0,0 +1,3 @@ +{ + "type": "transactions" +} diff --git a/tests/Functional/Resources/Reports/_fixtures/request/create_full.json b/tests/Functional/Resources/Reports/_fixtures/request/create_full.json new file mode 100644 index 0000000..fe16d15 --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/request/create_full.json @@ -0,0 +1,10 @@ +{ + "type": "transactions", + "filters": [ + { + "name": "updated_at", + "value": "2023-12-30", + "operator": "lt" + } + ] +} \ No newline at end of file diff --git a/tests/Functional/Resources/Reports/_fixtures/response/full_entity.json b/tests/Functional/Resources/Reports/_fixtures/response/full_entity.json new file mode 100644 index 0000000..d3bc23b --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/response/full_entity.json @@ -0,0 +1,21 @@ +{ + "data": { + "id": "rep_01hhq4c3b03g3x2kpkj8aecjv6", + "type": "transactions", + "rows": null, + "status": "pending", + "filters": [ + { + "name": "updated_at", + "value": "2023-12-30", + "operator": "lt" + } + ], + "expires_at": null, + "created_at": "2024-01-05T16:18:53.92Z", + "updated_at": "2024-01-05T16:18:53.92Z" + }, + "meta": { + "request_id": "70d20619-e988-41ce-81f3-0c1a6a6136e2" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Reports/_fixtures/response/list_default.json b/tests/Functional/Resources/Reports/_fixtures/response/list_default.json new file mode 100644 index 0000000..4ed4ee9 --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/response/list_default.json @@ -0,0 +1,36 @@ +{ + "data": [ + { + "id": "rep_01hhq4c3b03g3x2kpkj8aecjv6", + "type": "transactions", + "rows": 10, + "status": "ready", + "filters": [ + { + "name": "updated_at", + "value": "2023-12-30", + "operator": "lt" + }, + { + "name": "collection_mode", + "value": [ + "manual" + ], + "operator": null + } + ], + "expires_at": "2024-02-05T16:18:53.92Z", + "created_at": "2024-01-05T16:18:53.92Z", + "updated_at": "2024-01-05T16:18:53.92Z" + } + ], + "meta": { + "request_id": "31858add-0308-4c26-90fb-266e16be0c8c", + "pagination": { + "per_page": 50, + "next": "https://api.paddle.com/reports?after=rep_01hhq4c3b03g3x2kpkj8aecjv6", + "has_more": false, + "estimated_total": 1 + } + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Reports/_fixtures/response/report_csv_entity.json b/tests/Functional/Resources/Reports/_fixtures/response/report_csv_entity.json new file mode 100644 index 0000000..18377ab --- /dev/null +++ b/tests/Functional/Resources/Reports/_fixtures/response/report_csv_entity.json @@ -0,0 +1,8 @@ +{ + "data": { + "url": "https://reports.paddle.com/transactions-10889-2023-12-05-17-44-51.csv" + }, + "meta": { + "request_id": "f34d4a9c-2088-447d-a3a1-1da5ce74f507" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/Reports/test_ReportsClient.py b/tests/Functional/Resources/Reports/test_ReportsClient.py new file mode 100644 index 0000000..f522606 --- /dev/null +++ b/tests/Functional/Resources/Reports/test_ReportsClient.py @@ -0,0 +1,203 @@ +from json import loads +from pytest import mark +from urllib.parse import unquote + +from paddle_billing_python_sdk.Entities.Report import Report +from paddle_billing_python_sdk.Entities.ReportCSV import ReportCSV +from paddle_billing_python_sdk.Entities.Reports.ReportFilters import ReportFilters +from paddle_billing_python_sdk.Entities.Reports.ReportName import ReportName +from paddle_billing_python_sdk.Entities.Reports.ReportOperator import ReportOperator +from paddle_billing_python_sdk.Entities.Reports.ReportStatus import ReportStatus +from paddle_billing_python_sdk.Entities.Reports.ReportType import ReportType + +from paddle_billing_python_sdk.Entities.Collections.ReportCollection import ReportCollection + +from paddle_billing_python_sdk.Resources.Reports.Operations.CreateReport import CreateReport +from paddle_billing_python_sdk.Resources.Reports.Operations.ListReports import ListReports +from paddle_billing_python_sdk.Resources.Shared.Operations.List.Pager import Pager + +from tests.Utils.TestClient import mock_requests, test_client +from tests.Utils.ReadsFixture import ReadsFixtures + + +class TestReportsClient: + @mark.parametrize( + 'operation, expected_request_body, expected_response_status, expected_response_body, expected_url', + [ + ( + CreateReport(type=ReportType.Transactions), + ReadsFixtures.read_raw_json_fixture('request/create_basic'), + 200, + ReadsFixtures.read_raw_json_fixture('response/full_entity'), + '/reports', + ), ( + CreateReport( + type = ReportType.Transactions, + filters = [ReportFilters(name=ReportName.UpdatedAt, operator=ReportOperator.Lt, value='2023-12-30')], + ), + ReadsFixtures.read_raw_json_fixture('request/create_full'), + 200, + ReadsFixtures.read_raw_json_fixture('response/full_entity'), + '/reports', + ), + ], + ids=[ + "Create report with basic data", + "Create report with filters", + ], + ) + def test_create_report_uses_expected_payload( + self, + test_client, + mock_requests, + operation, + expected_request_body, + expected_response_status, + expected_response_body, + expected_url, + ): + expected_url = f"{test_client.base_url}{expected_url}" + mock_requests.post(expected_url, status_code=expected_response_status, text=expected_response_body) + + response = test_client.client.reports.create(operation) + response_json = test_client.client.reports.response.json() + request_json = test_client.client.payload + last_request = mock_requests.last_request + + assert isinstance(response, Report) + assert last_request is not None + assert last_request.method == 'POST' + assert test_client.client.status_code == expected_response_status + assert unquote(last_request.url) == expected_url, \ + "The URL does not match the expected URL, verify the query string is correct" + assert loads(request_json) == loads(expected_request_body), \ + "The request JSON doesn't match the expected fixture JSON" + assert response_json == loads(str(expected_response_body)), \ + "The response JSON doesn't match the expected fixture JSON" + + + @mark.parametrize( + 'operation, expected_response_status, expected_response_body, expected_url', + [ + ( + ListReports(), + 200, + ReadsFixtures.read_raw_json_fixture('response/list_default'), + '/reports', + ), ( + ListReports(Pager()), + 200, + ReadsFixtures.read_raw_json_fixture('response/list_default'), + '/reports?order_by=id[asc]&per_page=50', + ), ( + ListReports(Pager(after='rep_01hhq4c3b03g3x2kpkj8aecjv6')), + 200, + ReadsFixtures.read_raw_json_fixture('response/list_default'), + '/reports?after=rep_01hhq4c3b03g3x2kpkj8aecjv6&order_by=id[asc]&per_page=50', + ), ( + ListReports(statuses=[ReportStatus.Ready]), + 200, + ReadsFixtures.read_raw_json_fixture('response/list_default'), + '/reports?status=ready', + ), + ], + ids=[ + "List reports without pagination", + "List reports with default pagination", + "List paginated reports after specified report id", + "List reports filtered by status", + ], + ) + def test_list_reports_returns_expected_response( + self, + test_client, + mock_requests, + operation, + expected_response_status, + expected_response_body, + expected_url, + ): + expected_url = f"{test_client.base_url}{expected_url}" + mock_requests.get(expected_url, status_code=expected_response_status) + + response = test_client.client.reports.list(operation) + last_request = mock_requests.last_request + + assert isinstance(response, ReportCollection) + assert last_request is not None + assert last_request.method == 'GET' + assert test_client.client.status_code == expected_response_status + assert unquote(last_request.url) == expected_url, \ + "The URL does not match the expected URL, verify the query string is correct" + + + @mark.parametrize( + 'report_id, expected_response_status, expected_response_body, expected_url', + [( + 'rep_01hhq4c3b03g3x2kpkj8aecjv6', + 200, + ReadsFixtures.read_raw_json_fixture('response/full_entity'), + '/reports/rep_01hhq4c3b03g3x2kpkj8aecjv6', + )], + ids=["Get a report by its id"], + ) + def test_get_report_returns_expected_response( + self, + test_client, + mock_requests, + report_id, + expected_response_status, + expected_response_body, + expected_url, + ): + expected_url = f"{test_client.base_url}{expected_url}" + mock_requests.get(expected_url, status_code=expected_response_status, text=expected_response_body) + + response = test_client.client.reports.get(report_id) + response_json = test_client.client.reports.response.json() + last_request = mock_requests.last_request + + assert isinstance(response, Report) + assert last_request is not None + assert last_request.method == 'GET' + assert test_client.client.status_code == expected_response_status + assert unquote(last_request.url) == expected_url, \ + "The URL does not match the expected URL, verify the query string is correct" + assert response_json == loads(str(expected_response_body)), \ + "The response JSON generated by ResponseParser() doesn't match the expected fixture JSON" + + + @mark.parametrize( + 'report_id, expected_response_status, expected_response_body, expected_url', + [( + 'rep_01hhq4c3b03g3x2kpkj8aecjv6', + 200, + ReadsFixtures.read_raw_json_fixture('response/report_csv_entity'), + '/reports/rep_01hhq4c3b03g3x2kpkj8aecjv6/download-url', + )], + ids=["Get a report csv by its id"], + ) + def test_get_report_csv_returns_expected_response( + self, + test_client, + mock_requests, + report_id, + expected_response_status, + expected_response_body, + expected_url, + ): + expected_url = f"{test_client.base_url}{expected_url}" + mock_requests.get(expected_url, status_code=expected_response_status, text=expected_response_body) + + response = test_client.client.reports.get_report_csv(report_id) + response_json = test_client.client.reports.response.json() + last_request = mock_requests.last_request + + assert isinstance(response, ReportCSV) + assert last_request is not None + assert last_request.method == 'GET' + assert test_client.client.status_code == expected_response_status + assert unquote(last_request.url) == expected_url, \ + "The URL does not match the expected URL, verify the query string is correct" + assert response_json == loads(str(expected_response_body)), \ + "The response JSON generated by ResponseParser() doesn't match the expected fixture JSON"