From b21f44aa5a4ce426c85afe07eeea24c26f9b1407 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 30 Dec 2025 14:35:26 +0100 Subject: [PATCH 1/8] Add tests confirming that retrieve accepts query params --- test/api/test_invoice.py | 114 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/test/api/test_invoice.py b/test/api/test_invoice.py index 0f362e7..f4466b5 100644 --- a/test/api/test_invoice.py +++ b/test/api/test_invoice.py @@ -447,3 +447,117 @@ def test_retrieve_invoice(self, mock_requests): self.assertTrue(isinstance(result, Invoice)) self.assertEqual(result.uuid, "inv_22910fc6-c931-48e7-ac12-90d2cb5f0059") + + @requests_mock.mock() + def test_retrieve_invoice_with_validation_type(self, mock_requests): + mock_requests.register_uri( + "GET", + ("https://api.chartmogul.com/v1/invoices/inv_22910fc6-c931-48e7-ac12-90d2cb5f0059" + "?validation_type=all"), + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=retrieveInvoiceExample, + ) + + config = Config("token") # is actually checked in mock + result = Invoice.retrieve( + config, + uuid="inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", + validation_type="all" + ).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + vt = [] + vt.append("all") + self.assertEqual(mock_requests.last_request.qs, {"validation_type": vt}) + + # Struct too complex to do 1:1 comparison + self.assertTrue(isinstance(result, Invoice)) + + self.assertEqual(result.uuid, "inv_22910fc6-c931-48e7-ac12-90d2cb5f0059") + + @requests_mock.mock() + def test_retrieve_invoice_with_all_params(self, mock_requests): + mock_requests.register_uri( + "GET", + ("https://api.chartmogul.com/v1/invoices/inv_22910fc6-c931-48e7-ac12-90d2cb5f0059" + "?validation_type=invalid&include_edit_histories=true&with_disabled=false"), + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=retrieveInvoiceExample, + ) + + config = Config("token") # is actually checked in mock + result = Invoice.retrieve( + config, + uuid="inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", + validation_type="invalid", + include_edit_histories=True, + with_disabled=False + ).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + qs = mock_requests.last_request.qs + self.assertEqual(qs["validation_type"], ["invalid"]) + self.assertEqual(qs["include_edit_histories"], ["true"]) + self.assertEqual(qs["with_disabled"], ["false"]) + + # Struct too complex to do 1:1 comparison + self.assertTrue(isinstance(result, Invoice)) + + self.assertEqual(result.uuid, "inv_22910fc6-c931-48e7-ac12-90d2cb5f0059") + + @requests_mock.mock() + def test_all_invoices_with_validation_type(self, mock_requests): + mock_requests.register_uri( + "GET", + "https://api.chartmogul.com/v1/invoices?validation_type=all", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=invoiceListExample, + ) + + config = Config("token") # is actually checked in mock + result = Invoice.all(config, validation_type="all").get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + vt = [] + vt.append("all") + self.assertEqual(mock_requests.last_request.qs, {"validation_type": vt}) + + # Struct too complex to do 1:1 comparison + self.assertTrue(isinstance(result, Invoice._many)) + self.assertEqual(len(result.invoices), 1) + + @requests_mock.mock() + def test_all_invoices_with_all_params(self, mock_requests): + mock_requests.register_uri( + "GET", + ("https://api.chartmogul.com/v1/invoices" + "?validation_type=valid&include_edit_histories=true&with_disabled=true"), + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=invoiceListExample, + ) + + config = Config("token") # is actually checked in mock + result = Invoice.all( + config, + validation_type="valid", + include_edit_histories=True, + with_disabled=True + ).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + qs = mock_requests.last_request.qs + self.assertEqual(qs["validation_type"], ["valid"]) + self.assertEqual(qs["include_edit_histories"], ["true"]) + self.assertEqual(qs["with_disabled"], ["true"]) + + # Struct too complex to do 1:1 comparison + self.assertTrue(isinstance(result, Invoice._many)) + self.assertEqual(len(result.invoices), 1) From b06878ac65abe4e687f8f676f226ff0a7f350fca Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 30 Dec 2025 15:29:37 +0100 Subject: [PATCH 2/8] Update test/api/test_invoice.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/api/test_invoice.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/api/test_invoice.py b/test/api/test_invoice.py index f4466b5..96d749e 100644 --- a/test/api/test_invoice.py +++ b/test/api/test_invoice.py @@ -524,9 +524,10 @@ def test_all_invoices_with_validation_type(self, mock_requests): result = Invoice.all(config, validation_type="all").get() self.assertEqual(mock_requests.call_count, 1, "expected call") - vt = [] - vt.append("all") - self.assertEqual(mock_requests.last_request.qs, {"validation_type": vt}) + self.assertEqual( + mock_requests.last_request.qs, + {"validation_type": ["all"]}, + ) # Struct too complex to do 1:1 comparison self.assertTrue(isinstance(result, Invoice._many)) From fac5d3f06cc22b6390ba99fb836ac294dba9bbfa Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 30 Dec 2025 15:29:52 +0100 Subject: [PATCH 3/8] Update test/api/test_invoice.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/api/test_invoice.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/api/test_invoice.py b/test/api/test_invoice.py index 96d749e..7aa7728 100644 --- a/test/api/test_invoice.py +++ b/test/api/test_invoice.py @@ -468,9 +468,10 @@ def test_retrieve_invoice_with_validation_type(self, mock_requests): ).get() self.assertEqual(mock_requests.call_count, 1, "expected call") - vt = [] - vt.append("all") - self.assertEqual(mock_requests.last_request.qs, {"validation_type": vt}) + self.assertEqual( + mock_requests.last_request.qs, + {"validation_type": ["all"]}, + ) # Struct too complex to do 1:1 comparison self.assertTrue(isinstance(result, Invoice)) From d56889517438d28e24f74f152ab70e059fd0fedb Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Wed, 31 Dec 2025 11:54:52 +0100 Subject: [PATCH 4/8] Add #errors, #edit_history_summary, #disabled, #disabled_at and #disabled_by to Invoice --- chartmogul/api/invoice.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/chartmogul/api/invoice.py b/chartmogul/api/invoice.py index 967cec9..f3e1935 100644 --- a/chartmogul/api/invoice.py +++ b/chartmogul/api/invoice.py @@ -58,6 +58,12 @@ class _Schema(Schema): date = fields.DateTime() due_date = fields.DateTime(allow_none=True) + disabled = fields.Boolean(allow_none=True) + disabled_at = fields.DateTime(allow_none=True) + disabled_by = fields.String(allow_none=True) + edit_history_summary = fields.Dict(allow_none=True) + errors = fields.Dict(allow_none=True) + line_items = fields.Nested(LineItem._Schema, many=True, unknown=EXCLUDE) transactions = fields.Nested(Transaction._Schema, many=True, unknown=EXCLUDE) From 44a6e8912287634045348fc264741452b6c5948f Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Wed, 31 Dec 2025 12:32:10 +0100 Subject: [PATCH 5/8] Update tests --- test/api/test_invoice.py | 71 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/test/api/test_invoice.py b/test/api/test_invoice.py index 7aa7728..10f9721 100644 --- a/test/api/test_invoice.py +++ b/test/api/test_invoice.py @@ -1,6 +1,6 @@ # pylama:ignore=W0212 import unittest -from datetime import datetime +from datetime import datetime, timezone import requests_mock @@ -175,6 +175,20 @@ "date": "2015-11-01T00:00:00.000Z", "due_date": "2015-11-15T00:00:00.000Z", "currency": "USD", + "disabled": False, + "disabled_at": None, + "disabled_by": None, + "edit_history_summary": { + "values_changed": { + "amount_in_cents": { + "original_value": 4500, + "edited_value": 5000 + } + }, + "latest_edit_author": "admin@example.com", + "latest_edit_performed_at": "2024-01-10T12:00:00.000Z" + }, + "errors": None, "line_items": [ { "uuid": "li_d72e6843-5793-41d0-bfdf-0269514c9c56", @@ -231,6 +245,27 @@ "date": "2015-11-01T00:00:00.000Z", "due_date": "2015-11-15T00:00:00.000Z", "currency": "USD", + "disabled": True, + "disabled_at": "2024-01-15T10:30:00.000Z", + "disabled_by": "user@example.com", + "edit_history_summary": { + "values_changed": { + "currency": { + "original_value": "EUR", + "edited_value": "USD" + }, + "date": { + "original_value": "2024-01-01T00:00:00.000Z", + "edited_value": "2024-01-02T00:00:00.000Z" + } + }, + "latest_edit_author": "editor@example.com", + "latest_edit_performed_at": "2024-01-20T15:45:00.000Z" + }, + "errors": { + "currency": ["Currency is invalid", "Currency must be supported"], + "date": ["Date is in the future"] + }, "line_items": [ { "uuid": "li_d72e6843-5793-41d0-bfdf-0269514c9c56", @@ -510,6 +545,40 @@ def test_retrieve_invoice_with_all_params(self, mock_requests): self.assertEqual(result.uuid, "inv_22910fc6-c931-48e7-ac12-90d2cb5f0059") + # Verify new fields are present + self.assertTrue(result.disabled) + self.assertEqual(result.disabled_at, datetime(2024, 1, 15, 10, 30, tzinfo=timezone.utc)) + self.assertEqual(result.disabled_by, "user@example.com") + self.assertIsNotNone(result.edit_history_summary) + self.assertIn("values_changed", result.edit_history_summary) + self.assertIn("currency", result.edit_history_summary["values_changed"]) + self.assertEqual( + result.edit_history_summary["values_changed"]["currency"]["original_value"], + "EUR" + ) + self.assertEqual( + result.edit_history_summary["values_changed"]["currency"]["edited_value"], + "USD" + ) + self.assertEqual( + result.edit_history_summary["latest_edit_author"], + "editor@example.com" + ) + self.assertEqual( + result.edit_history_summary["latest_edit_performed_at"], + "2024-01-20T15:45:00.000Z" + ) + self.assertIsNotNone(result.errors) + self.assertIn("currency", result.errors) + self.assertIsInstance(result.errors["currency"], list) + self.assertEqual(len(result.errors["currency"]), 2) + self.assertEqual(result.errors["currency"][0], "Currency is invalid") + self.assertEqual(result.errors["currency"][1], "Currency must be supported") + self.assertIn("date", result.errors) + self.assertIsInstance(result.errors["date"], list) + self.assertEqual(len(result.errors["date"]), 1) + self.assertEqual(result.errors["date"][0], "Date is in the future") + @requests_mock.mock() def test_all_invoices_with_validation_type(self, mock_requests): mock_requests.register_uri( From dcc59d24fddfdd37e5b4ef9dfde4c70e2e70e550 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Fri, 2 Jan 2026 15:42:36 +0100 Subject: [PATCH 6/8] Delete excessive comments --- test/api/test_invoice.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/test/api/test_invoice.py b/test/api/test_invoice.py index 10f9721..74f74e9 100644 --- a/test/api/test_invoice.py +++ b/test/api/test_invoice.py @@ -495,7 +495,7 @@ def test_retrieve_invoice_with_validation_type(self, mock_requests): json=retrieveInvoiceExample, ) - config = Config("token") # is actually checked in mock + config = Config("token") result = Invoice.retrieve( config, uuid="inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", @@ -507,10 +507,7 @@ def test_retrieve_invoice_with_validation_type(self, mock_requests): mock_requests.last_request.qs, {"validation_type": ["all"]}, ) - - # Struct too complex to do 1:1 comparison self.assertTrue(isinstance(result, Invoice)) - self.assertEqual(result.uuid, "inv_22910fc6-c931-48e7-ac12-90d2cb5f0059") @requests_mock.mock() @@ -525,7 +522,7 @@ def test_retrieve_invoice_with_all_params(self, mock_requests): json=retrieveInvoiceExample, ) - config = Config("token") # is actually checked in mock + config = Config("token") result = Invoice.retrieve( config, uuid="inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", @@ -539,13 +536,8 @@ def test_retrieve_invoice_with_all_params(self, mock_requests): self.assertEqual(qs["validation_type"], ["invalid"]) self.assertEqual(qs["include_edit_histories"], ["true"]) self.assertEqual(qs["with_disabled"], ["false"]) - - # Struct too complex to do 1:1 comparison self.assertTrue(isinstance(result, Invoice)) - self.assertEqual(result.uuid, "inv_22910fc6-c931-48e7-ac12-90d2cb5f0059") - - # Verify new fields are present self.assertTrue(result.disabled) self.assertEqual(result.disabled_at, datetime(2024, 1, 15, 10, 30, tzinfo=timezone.utc)) self.assertEqual(result.disabled_by, "user@example.com") @@ -590,7 +582,7 @@ def test_all_invoices_with_validation_type(self, mock_requests): json=invoiceListExample, ) - config = Config("token") # is actually checked in mock + config = Config("token") result = Invoice.all(config, validation_type="all").get() self.assertEqual(mock_requests.call_count, 1, "expected call") @@ -599,7 +591,6 @@ def test_all_invoices_with_validation_type(self, mock_requests): {"validation_type": ["all"]}, ) - # Struct too complex to do 1:1 comparison self.assertTrue(isinstance(result, Invoice._many)) self.assertEqual(len(result.invoices), 1) @@ -615,7 +606,7 @@ def test_all_invoices_with_all_params(self, mock_requests): json=invoiceListExample, ) - config = Config("token") # is actually checked in mock + config = Config("token") result = Invoice.all( config, validation_type="valid", @@ -628,7 +619,5 @@ def test_all_invoices_with_all_params(self, mock_requests): self.assertEqual(qs["validation_type"], ["valid"]) self.assertEqual(qs["include_edit_histories"], ["true"]) self.assertEqual(qs["with_disabled"], ["true"]) - - # Struct too complex to do 1:1 comparison self.assertTrue(isinstance(result, Invoice._many)) self.assertEqual(len(result.invoices), 1) From 5680ccf8b19d84fb784b0c520c37d9270ca9a4f3 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Mon, 5 Jan 2026 15:27:42 +0100 Subject: [PATCH 7/8] Add support for boolean query params --- chartmogul/api/data_source.py | 13 ------------- chartmogul/api/invoice.py | 4 ++++ chartmogul/resource.py | 9 +++++++++ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/chartmogul/api/data_source.py b/chartmogul/api/data_source.py index 9e8cb41..61dd870 100644 --- a/chartmogul/api/data_source.py +++ b/chartmogul/api/data_source.py @@ -42,19 +42,6 @@ class DataSource(Resource): defaults=[None, None, None] ) - @classmethod - def _preProcessParams(cls, params): - params = super()._preProcessParams(params) - - for query_param in cls._bool_query_params: - if query_param in params and isinstance(params[query_param], bool): - if params[query_param] is True: - params[query_param] = 'true' - else: - del params[query_param] - - return params - class _Schema(Schema): uuid = fields.String() name = fields.String() diff --git a/chartmogul/api/invoice.py b/chartmogul/api/invoice.py index f3e1935..c2b20b4 100644 --- a/chartmogul/api/invoice.py +++ b/chartmogul/api/invoice.py @@ -40,6 +40,10 @@ class Invoice(Resource): _path = "/import/customers{/uuid}/invoices" _root_key = "invoices" + _bool_query_params = [ + 'include_edit_histories', + 'with_disabled' + ] _many = namedtuple( "Invoices", [_root_key, "cursor", "has_more", "customer_uuid"], diff --git a/chartmogul/resource.py b/chartmogul/resource.py index 9a3ceb8..a9d6d61 100644 --- a/chartmogul/resource.py +++ b/chartmogul/resource.py @@ -121,6 +121,15 @@ def _preProcessParams(cls, params): if key in params: params[replacement] = params[key] del params[key] + + if hasattr(cls, '_bool_query_params'): + for query_param in cls._bool_query_params: + if query_param in params and isinstance(params[query_param], bool): + if params[query_param] is True: + params[query_param] = 'true' + elif params[query_param] is False: + params[query_param] = 'false' + return params @classmethod From 1dd51be775a10419c2c256b9ea57bb3aa1925926 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 6 Jan 2026 12:16:34 +0100 Subject: [PATCH 8/8] Delete DataSources _bool_query_params from _many args --- chartmogul/api/data_source.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/chartmogul/api/data_source.py b/chartmogul/api/data_source.py index 61dd870..e689dec 100644 --- a/chartmogul/api/data_source.py +++ b/chartmogul/api/data_source.py @@ -36,11 +36,7 @@ class DataSource(Resource): 'with_auto_churn_subscription_setting', 'with_invoice_handling_setting' ] - _many = namedtuple( - "DataSources", - [_root_key] + _bool_query_params, - defaults=[None, None, None] - ) + _many = namedtuple("DataSources", [_root_key]) class _Schema(Schema): uuid = fields.String()