From ca0bf7ac29cb8e72ed447144b04e3046aa9de2f3 Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 17 Mar 2026 17:04:38 +0100 Subject: [PATCH 1/5] Expose validation errors on LineItem and Transaction schemas (PIP-304) Invoice already has an `errors` field, but LineItem and Transaction did not. Add `errors = fields.Dict(allow_none=True)` to both schemas so validation errors returned by the API are accessible on nested objects. Co-Authored-By: Claude Opus 4.6 --- chartmogul/api/invoice.py | 1 + chartmogul/api/transaction.py | 1 + test/api/test_invoice.py | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/chartmogul/api/invoice.py b/chartmogul/api/invoice.py index c2b20b4..99401d0 100644 --- a/chartmogul/api/invoice.py +++ b/chartmogul/api/invoice.py @@ -27,6 +27,7 @@ class _Schema(Schema): account_code = fields.String(allow_none=True) description = fields.String(allow_none=True) event_order = fields.Int(allow_none=True) + errors = fields.Dict(allow_none=True) @post_load def make(self, data, **kwargs): diff --git a/chartmogul/api/transaction.py b/chartmogul/api/transaction.py index 2720606..181c846 100644 --- a/chartmogul/api/transaction.py +++ b/chartmogul/api/transaction.py @@ -17,6 +17,7 @@ class _Schema(Schema): result = fields.String() amount_in_cents = fields.Int(allow_none=True) transaction_fees_in_cents = fields.Int(allow_none=True) + errors = fields.Dict(allow_none=True) @post_load def make(self, data, **kwargs): diff --git a/test/api/test_invoice.py b/test/api/test_invoice.py index 74f74e9..1f35855 100644 --- a/test/api/test_invoice.py +++ b/test/api/test_invoice.py @@ -621,3 +621,55 @@ def test_all_invoices_with_all_params(self, mock_requests): self.assertEqual(qs["with_disabled"], ["true"]) self.assertTrue(isinstance(result, Invoice._many)) self.assertEqual(len(result.invoices), 1) + + @requests_mock.mock() + def test_line_item_and_transaction_errors(self, mock_requests): + responseWithErrors = { + "uuid": "inv_test", + "external_id": "INV0001", + "date": "2015-11-01T00:00:00.000Z", + "due_date": "2015-11-15T00:00:00.000Z", + "currency": "USD", + "line_items": [ + { + "uuid": "li_test", + "external_id": None, + "type": "subscription", + "prorated": False, + "amount_in_cents": 5000, + "quantity": 1, + "discount_amount_in_cents": 0, + "tax_amount_in_cents": 0, + "transaction_fees_in_cents": 0, + "errors": {"amount_in_cents": ["must be positive"]}, + }, + ], + "transactions": [ + { + "uuid": "tr_test", + "external_id": None, + "type": "payment", + "date": "2015-11-05T00:04:03.000Z", + "result": "successful", + "errors": {"date": ["is in the future"]}, + }, + ], + } + + mock_requests.register_uri( + "GET", + "https://api.chartmogul.com/v1/invoices/inv_test", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=responseWithErrors, + ) + + config = Config("token") + result = Invoice.retrieve(config, uuid="inv_test").get() + + self.assertTrue(isinstance(result, Invoice)) + self.assertIsNotNone(result.line_items[0].errors) + self.assertEqual(result.line_items[0].errors["amount_in_cents"], ["must be positive"]) + self.assertIsNotNone(result.transactions[0].errors) + self.assertEqual(result.transactions[0].errors["date"], ["is in the future"]) From 6daf5950b9f5867d7dabd73a4977866905dbf2fb Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 17 Mar 2026 17:05:54 +0100 Subject: [PATCH 2/5] Add account ID and include query param support (PIP-120, PIP-76) Add `id` field to Account schema so the account identifier is accessible. Add `churn_recognition` and `churn_when_zero_mrr` fields to support the optional `include` query parameter on the /account endpoint. Co-Authored-By: Claude Opus 4.6 --- chartmogul/api/account.py | 3 ++ test/api/test_account.py | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/chartmogul/api/account.py b/chartmogul/api/account.py index 97d24bf..20120cc 100644 --- a/chartmogul/api/account.py +++ b/chartmogul/api/account.py @@ -10,10 +10,13 @@ class Account(Resource): _path = "/account" class _Schema(Schema): + id = fields.String(allow_none=True) name = fields.String() currency = fields.String() time_zone = fields.String() week_start_on = fields.String() + churn_recognition = fields.String(allow_none=True) + churn_when_zero_mrr = fields.String(allow_none=True) @post_load def make(self, data, **kwargs): diff --git a/test/api/test_account.py b/test/api/test_account.py index 1f68608..00844e7 100644 --- a/test/api/test_account.py +++ b/test/api/test_account.py @@ -12,6 +12,24 @@ "week_start_on": "sunday", } +jsonResponseWithId = { + "id": "acct_a1b2c3d4", + "name": "Example Test Company", + "currency": "EUR", + "time_zone": "Europe/Berlin", + "week_start_on": "sunday", +} + +jsonResponseWithInclude = { + "id": "acct_a1b2c3d4", + "name": "Example Test Company", + "currency": "EUR", + "time_zone": "Europe/Berlin", + "week_start_on": "sunday", + "churn_recognition": "immediate", + "churn_when_zero_mrr": "ignore", +} + class AccountTestCase(unittest.TestCase): """ @@ -35,3 +53,43 @@ def test_retrieve(self, mock_requests): self.assertEqual(account.currency, "EUR") self.assertEqual(account.time_zone, "Europe/Berlin") self.assertEqual(account.week_start_on, "sunday") + + @requests_mock.mock() + def test_retrieve_with_id(self, mock_requests): + mock_requests.register_uri( + "GET", + "https://api.chartmogul.com/v1/account", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=jsonResponseWithId, + ) + + config = Config("token") + account = Account.retrieve(config).get() + self.assertTrue(isinstance(account, Account)) + self.assertEqual(account.id, "acct_a1b2c3d4") + + @requests_mock.mock() + def test_retrieve_with_include(self, mock_requests): + mock_requests.register_uri( + "GET", + "https://api.chartmogul.com/v1/account?include=churn_recognition,churn_when_zero_mrr", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=jsonResponseWithInclude, + ) + + config = Config("token") + account = Account.retrieve( + config, + include="churn_recognition,churn_when_zero_mrr" + ).get() + self.assertTrue(isinstance(account, Account)) + self.assertEqual(account.id, "acct_a1b2c3d4") + self.assertEqual(account.churn_recognition, "immediate") + self.assertEqual(account.churn_when_zero_mrr, "ignore") + self.assertEqual( + mock_requests.last_request.qs, + {"include": ["churn_recognition,churn_when_zero_mrr"]}, + ) From 383f43a73f3f4f7a219e83f010b32b48941bf23f Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 17 Mar 2026 17:06:44 +0100 Subject: [PATCH 3/5] Add Invoice update-status and disable endpoint support Add Invoice.update_status (PATCH /invoices/{uuid}) for updating invoice status and Invoice.disable (PATCH /invoices/{uuid}/disable) for disabling invoices. Co-Authored-By: Claude Opus 4.6 --- chartmogul/api/invoice.py | 2 ++ test/api/test_invoice.py | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/chartmogul/api/invoice.py b/chartmogul/api/invoice.py index 99401d0..5173f80 100644 --- a/chartmogul/api/invoice.py +++ b/chartmogul/api/invoice.py @@ -98,3 +98,5 @@ def all(cls, config, **kwargs): "/data_sources{/data_source_uuid}/customers{/customer_uuid}/invoices", ) Invoice.retrieve = Invoice._method("retrieve", "get", "/invoices{/uuid}") +Invoice.update_status = Invoice._method("modify", "patch", "/invoices{/uuid}") +Invoice.disable = Invoice._method("patch", "patch", "/invoices{/uuid}/disable") diff --git a/test/api/test_invoice.py b/test/api/test_invoice.py index 1f35855..b8762ad 100644 --- a/test/api/test_invoice.py +++ b/test/api/test_invoice.py @@ -673,3 +673,52 @@ def test_line_item_and_transaction_errors(self, mock_requests): self.assertEqual(result.line_items[0].errors["amount_in_cents"], ["must be positive"]) self.assertIsNotNone(result.transactions[0].errors) self.assertEqual(result.transactions[0].errors["date"], ["is in the future"]) + + @requests_mock.mock() + def test_update_status(self, mock_requests): + updatedInvoice = dict(retrieveInvoiceExample) + updatedInvoice["disabled"] = False + + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/invoices/inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=updatedInvoice, + ) + + config = Config("token") + result = Invoice.update_status( + config, + uuid="inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", + data={"disabled": False} + ).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + self.assertTrue(isinstance(result, Invoice)) + self.assertEqual(result.uuid, "inv_22910fc6-c931-48e7-ac12-90d2cb5f0059") + + @requests_mock.mock() + def test_disable(self, mock_requests): + disabledInvoice = dict(retrieveInvoiceExample) + disabledInvoice["disabled"] = True + + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/invoices/inv_22910fc6-c931-48e7-ac12-90d2cb5f0059/disable", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=disabledInvoice, + ) + + config = Config("token") + result = Invoice.disable( + config, + uuid="inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", + ).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + self.assertTrue(isinstance(result, Invoice)) + self.assertTrue(result.disabled) From 0b7a46797ed5afdc22f932839a7c08026240905d Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 17 Mar 2026 17:07:37 +0100 Subject: [PATCH 4/5] Add flat-param interface and disable/enable for SubscriptionEvent Add SubscriptionEvent.destroy() and .modify() that accept flat params (e.g. data={"id": 123}) and auto-wrap in the subscription_event envelope. The old _with_params methods are preserved for backwards compatibility. Add SubscriptionEvent.disable() and .enable() convenience methods for toggling the disabled state of subscription events. Co-Authored-By: Claude Opus 4.6 --- chartmogul/api/subscription_event.py | 44 ++++++++++++++++ test/api/test_subscription_event.py | 79 ++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/chartmogul/api/subscription_event.py b/chartmogul/api/subscription_event.py index f0f788d..c3f694b 100644 --- a/chartmogul/api/subscription_event.py +++ b/chartmogul/api/subscription_event.py @@ -46,3 +46,47 @@ def make(self, data, **kwargs): SubscriptionEvent.modify_with_params = SubscriptionEvent._method( "modify_with_params", "patch", "/subscription_events" ) + + +@classmethod +def _destroy(cls, config, **kwargs): + """Accept flat params and wrap in subscription_event envelope for the API.""" + data = kwargs.get("data", {}) + if "subscription_event" not in data: + data = {"subscription_event": data} + return cls.destroy_with_params(config, data=data) + + +@classmethod +def _modify(cls, config, **kwargs): + """Accept flat params and wrap in subscription_event envelope for the API.""" + data = kwargs.get("data", {}) + if "subscription_event" not in data: + data = {"subscription_event": data} + return cls.modify_with_params(config, data=data) + + +@classmethod +def _disable(cls, config, **kwargs): + """Disable a subscription event by setting disabled to true.""" + data = kwargs.get("data", {}) + data["disabled"] = True + if "subscription_event" not in data: + data = {"subscription_event": data} + return cls.modify_with_params(config, data=data) + + +@classmethod +def _enable(cls, config, **kwargs): + """Enable a subscription event by setting disabled to false.""" + data = kwargs.get("data", {}) + data["disabled"] = False + if "subscription_event" not in data: + data = {"subscription_event": data} + return cls.modify_with_params(config, data=data) + + +SubscriptionEvent.destroy = _destroy +SubscriptionEvent.modify = _modify +SubscriptionEvent.disable = _disable +SubscriptionEvent.enable = _enable diff --git a/test/api/test_subscription_event.py b/test/api/test_subscription_event.py index 0cf5da9..5eb839a 100644 --- a/test/api/test_subscription_event.py +++ b/test/api/test_subscription_event.py @@ -224,6 +224,85 @@ def test_all_subscription_events(self, mock_requests): ) self.assertTrue(isinstance(subscription_events.subscription_events[0], SubscriptionEvent)) + @requests_mock.mock() + def test_destroy_flat_params(self, mock_requests): + mock_requests.register_uri( + "DELETE", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=204, + ) + + config = Config("token") + result = SubscriptionEvent.destroy(config, data={"id": 7654321}).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + self.assertEqual( + mock_requests.last_request.json(), + {"subscription_event": {"id": 7654321}}, + ) + self.assertTrue(result is None) + + @requests_mock.mock() + def test_modify_flat_params(self, mock_requests): + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + config = Config("token") + sub_ev = SubscriptionEvent.modify( + config, data={"id": 7654321, "amount_in_cents": 10} + ).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + self.assertEqual( + mock_requests.last_request.json(), + {"subscription_event": {"id": 7654321, "amount_in_cents": 10}}, + ) + self.assertTrue(isinstance(sub_ev, SubscriptionEvent)) + self.assertEqual(sub_ev.id, 7654321) + + @requests_mock.mock() + def test_disable_subscription_event(self, mock_requests): + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + config = Config("token") + sub_ev = SubscriptionEvent.disable(config, data={"id": 7654321}).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + body = mock_requests.last_request.json() + self.assertEqual(body["subscription_event"]["id"], 7654321) + self.assertTrue(body["subscription_event"]["disabled"]) + self.assertTrue(isinstance(sub_ev, SubscriptionEvent)) + + @requests_mock.mock() + def test_enable_subscription_event(self, mock_requests): + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + config = Config("token") + sub_ev = SubscriptionEvent.enable(config, data={"id": 7654321}).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + body = mock_requests.last_request.json() + self.assertEqual(body["subscription_event"]["id"], 7654321) + self.assertFalse(body["subscription_event"]["disabled"]) + @requests_mock.mock() def test_all_subscription_events_with_filters(self, mock_requests): mock_requests.register_uri( From c44557770b2018836170f06965b82020efaf78ec Mon Sep 17 00:00:00 2001 From: Wiktor Plaga Date: Tue, 17 Mar 2026 17:10:19 +0100 Subject: [PATCH 5/5] Add missing test coverage for recent SDK changes Add edge case, error path, and backwards-compatibility tests: Invoice: - Verify update_status request body is sent correctly - 404 error paths for update_status and disable - Verify disable sends no request body - LineItem/Transaction errors=None and errors-absent cases SubscriptionEvent: - Flat destroy/modify with external_id+data_source_uuid - Passthrough when caller already wraps in envelope (no double-wrap) - disable/enable with external_id+data_source_uuid identification Account: - Graceful handling when id field absent from response - Single include param Co-Authored-By: Claude Opus 4.6 --- test/api/test_account.py | 49 ++++++++ test/api/test_invoice.py | 185 ++++++++++++++++++++++++++++ test/api/test_subscription_event.py | 160 ++++++++++++++++++++++++ 3 files changed, 394 insertions(+) diff --git a/test/api/test_account.py b/test/api/test_account.py index 00844e7..9b29dd7 100644 --- a/test/api/test_account.py +++ b/test/api/test_account.py @@ -93,3 +93,52 @@ def test_retrieve_with_include(self, mock_requests): mock_requests.last_request.qs, {"include": ["churn_recognition,churn_when_zero_mrr"]}, ) + + @requests_mock.mock() + def test_retrieve_without_id_field(self, mock_requests): + """Old API responses without id field should not break deserialization.""" + mock_requests.register_uri( + "GET", + "https://api.chartmogul.com/v1/account", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=jsonResponse, + ) + + config = Config("token") + account = Account.retrieve(config).get() + self.assertTrue(isinstance(account, Account)) + self.assertFalse(hasattr(account, "id")) + + @requests_mock.mock() + def test_retrieve_with_single_include(self, mock_requests): + singleIncludeResponse = { + "id": "acct_a1b2c3d4", + "name": "Example Test Company", + "currency": "EUR", + "time_zone": "Europe/Berlin", + "week_start_on": "sunday", + "churn_recognition": "immediate", + } + + mock_requests.register_uri( + "GET", + "https://api.chartmogul.com/v1/account?include=churn_recognition", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=singleIncludeResponse, + ) + + config = Config("token") + account = Account.retrieve( + config, + include="churn_recognition" + ).get() + self.assertTrue(isinstance(account, Account)) + self.assertEqual(account.churn_recognition, "immediate") + self.assertFalse(hasattr(account, "churn_when_zero_mrr")) + self.assertEqual( + mock_requests.last_request.qs, + {"include": ["churn_recognition"]}, + ) diff --git a/test/api/test_invoice.py b/test/api/test_invoice.py index b8762ad..05a022f 100644 --- a/test/api/test_invoice.py +++ b/test/api/test_invoice.py @@ -722,3 +722,188 @@ def test_disable(self, mock_requests): self.assertEqual(mock_requests.call_count, 1, "expected call") self.assertTrue(isinstance(result, Invoice)) self.assertTrue(result.disabled) + + @requests_mock.mock() + def test_update_status_verifies_request_body(self, mock_requests): + updatedInvoice = dict(retrieveInvoiceExample) + updatedInvoice["disabled"] = False + + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/invoices/inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=updatedInvoice, + ) + + config = Config("token") + Invoice.update_status( + config, + uuid="inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", + data={"disabled": False} + ).get() + + self.assertEqual( + mock_requests.last_request.json(), + {"disabled": False}, + ) + + @requests_mock.mock() + def test_update_status_not_found(self, mock_requests): + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/invoices/inv_nonexistent", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=404, + json={"error": "Invoice not found"}, + ) + + config = Config("token") + with self.assertRaises(APIError): + Invoice.update_status( + config, + uuid="inv_nonexistent", + data={"disabled": False} + ).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + + @requests_mock.mock() + def test_disable_no_request_body(self, mock_requests): + disabledInvoice = dict(retrieveInvoiceExample) + disabledInvoice["disabled"] = True + + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/invoices/inv_22910fc6-c931-48e7-ac12-90d2cb5f0059/disable", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=disabledInvoice, + ) + + config = Config("token") + Invoice.disable( + config, + uuid="inv_22910fc6-c931-48e7-ac12-90d2cb5f0059", + ).get() + + self.assertIsNone(mock_requests.last_request.body) + + @requests_mock.mock() + def test_disable_not_found(self, mock_requests): + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/invoices/inv_nonexistent/disable", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=404, + json={"error": "Invoice not found"}, + ) + + config = Config("token") + with self.assertRaises(APIError): + Invoice.disable(config, uuid="inv_nonexistent").get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + + @requests_mock.mock() + def test_line_item_errors_none(self, mock_requests): + responseWithNoneErrors = { + "uuid": "inv_test", + "external_id": "INV0001", + "date": "2015-11-01T00:00:00.000Z", + "due_date": "2015-11-15T00:00:00.000Z", + "currency": "USD", + "line_items": [ + { + "uuid": "li_test", + "external_id": None, + "type": "subscription", + "prorated": False, + "amount_in_cents": 5000, + "quantity": 1, + "discount_amount_in_cents": 0, + "tax_amount_in_cents": 0, + "transaction_fees_in_cents": 0, + "errors": None, + }, + ], + "transactions": [ + { + "uuid": "tr_test", + "external_id": None, + "type": "payment", + "date": "2015-11-05T00:04:03.000Z", + "result": "successful", + "errors": None, + }, + ], + } + + mock_requests.register_uri( + "GET", + "https://api.chartmogul.com/v1/invoices/inv_test", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=responseWithNoneErrors, + ) + + config = Config("token") + result = Invoice.retrieve(config, uuid="inv_test").get() + + self.assertTrue(isinstance(result, Invoice)) + self.assertIsNone(result.line_items[0].errors) + self.assertIsNone(result.transactions[0].errors) + + @requests_mock.mock() + def test_line_item_errors_absent(self, mock_requests): + responseNoErrors = { + "uuid": "inv_test", + "external_id": "INV0001", + "date": "2015-11-01T00:00:00.000Z", + "due_date": "2015-11-15T00:00:00.000Z", + "currency": "USD", + "line_items": [ + { + "uuid": "li_test", + "external_id": None, + "type": "subscription", + "prorated": False, + "amount_in_cents": 5000, + "quantity": 1, + "discount_amount_in_cents": 0, + "tax_amount_in_cents": 0, + "transaction_fees_in_cents": 0, + }, + ], + "transactions": [ + { + "uuid": "tr_test", + "external_id": None, + "type": "payment", + "date": "2015-11-05T00:04:03.000Z", + "result": "successful", + }, + ], + } + + mock_requests.register_uri( + "GET", + "https://api.chartmogul.com/v1/invoices/inv_test_no_errors", + request_headers={"Authorization": "Basic dG9rZW46"}, + headers={"Content-Type": "application/json"}, + status_code=200, + json=responseNoErrors, + ) + + config = Config("token") + result = Invoice.retrieve(config, uuid="inv_test_no_errors").get() + + self.assertTrue(isinstance(result, Invoice)) + # When errors field is absent from response, the attribute should not be set + self.assertFalse(hasattr(result.line_items[0], "errors")) + self.assertFalse(hasattr(result.transactions[0], "errors")) diff --git a/test/api/test_subscription_event.py b/test/api/test_subscription_event.py index 5eb839a..6e91fe7 100644 --- a/test/api/test_subscription_event.py +++ b/test/api/test_subscription_event.py @@ -303,6 +303,166 @@ def test_enable_subscription_event(self, mock_requests): self.assertEqual(body["subscription_event"]["id"], 7654321) self.assertFalse(body["subscription_event"]["disabled"]) + @requests_mock.mock() + def test_destroy_flat_with_external_id_and_ds_uuid(self, mock_requests): + mock_requests.register_uri( + "DELETE", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=204, + ) + + config = Config("token") + result = SubscriptionEvent.destroy( + config, + data={ + "external_id": "evnt_026", + "data_source_uuid": "ds_1fm3eaac-62d0-31ec-clf4-4bf0mbe81aba", + } + ).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + self.assertEqual( + mock_requests.last_request.json(), + { + "subscription_event": { + "external_id": "evnt_026", + "data_source_uuid": "ds_1fm3eaac-62d0-31ec-clf4-4bf0mbe81aba", + } + }, + ) + self.assertTrue(result is None) + + @requests_mock.mock() + def test_modify_flat_with_external_id_and_ds_uuid(self, mock_requests): + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + config = Config("token") + sub_ev = SubscriptionEvent.modify( + config, + data={ + "external_id": "evnt_026", + "data_source_uuid": "ds_1fm3eaac-62d0-31ec-clf4-4bf0mbe81aba", + "amount_in_cents": 10, + } + ).get() + + self.assertEqual(mock_requests.call_count, 1, "expected call") + self.assertEqual( + mock_requests.last_request.json(), + { + "subscription_event": { + "external_id": "evnt_026", + "data_source_uuid": "ds_1fm3eaac-62d0-31ec-clf4-4bf0mbe81aba", + "amount_in_cents": 10, + } + }, + ) + self.assertTrue(isinstance(sub_ev, SubscriptionEvent)) + + @requests_mock.mock() + def test_destroy_flat_passthrough_envelope(self, mock_requests): + """If caller already wraps in subscription_event, don't double-wrap.""" + mock_requests.register_uri( + "DELETE", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=204, + ) + + config = Config("token") + result = SubscriptionEvent.destroy( + config, + data={"subscription_event": {"id": 7654321}} + ).get() + + self.assertEqual( + mock_requests.last_request.json(), + {"subscription_event": {"id": 7654321}}, + ) + self.assertTrue(result is None) + + @requests_mock.mock() + def test_modify_flat_passthrough_envelope(self, mock_requests): + """If caller already wraps in subscription_event, don't double-wrap.""" + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + config = Config("token") + sub_ev = SubscriptionEvent.modify( + config, + data={"subscription_event": {"id": 7654321, "amount_in_cents": 10}} + ).get() + + self.assertEqual( + mock_requests.last_request.json(), + {"subscription_event": {"id": 7654321, "amount_in_cents": 10}}, + ) + self.assertTrue(isinstance(sub_ev, SubscriptionEvent)) + + @requests_mock.mock() + def test_disable_with_external_id_and_ds_uuid(self, mock_requests): + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + config = Config("token") + sub_ev = SubscriptionEvent.disable( + config, + data={ + "external_id": "evnt_026", + "data_source_uuid": "ds_1fm3eaac-62d0-31ec-clf4-4bf0mbe81aba", + } + ).get() + + body = mock_requests.last_request.json() + self.assertEqual(body["subscription_event"]["external_id"], "evnt_026") + self.assertEqual( + body["subscription_event"]["data_source_uuid"], + "ds_1fm3eaac-62d0-31ec-clf4-4bf0mbe81aba", + ) + self.assertTrue(body["subscription_event"]["disabled"]) + self.assertTrue(isinstance(sub_ev, SubscriptionEvent)) + + @requests_mock.mock() + def test_enable_with_external_id_and_ds_uuid(self, mock_requests): + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + config = Config("token") + sub_ev = SubscriptionEvent.enable( + config, + data={ + "external_id": "evnt_026", + "data_source_uuid": "ds_1fm3eaac-62d0-31ec-clf4-4bf0mbe81aba", + } + ).get() + + body = mock_requests.last_request.json() + self.assertEqual(body["subscription_event"]["external_id"], "evnt_026") + self.assertFalse(body["subscription_event"]["disabled"]) + self.assertTrue(isinstance(sub_ev, SubscriptionEvent)) + @requests_mock.mock() def test_all_subscription_events_with_filters(self, mock_requests): mock_requests.register_uri(