diff --git a/chartmogul/api/account.py b/chartmogul/api/account.py index 97d24bf..8019ffb 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.Raw(allow_none=True) @post_load def make(self, data, **kwargs): diff --git a/chartmogul/api/invoice.py b/chartmogul/api/invoice.py index c2b20b4..d67244f 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): @@ -97,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("disable", "patch", "/invoices{/uuid}/disable") diff --git a/chartmogul/api/subscription_event.py b/chartmogul/api/subscription_event.py index f0f788d..132dd39 100644 --- a/chartmogul/api/subscription_event.py +++ b/chartmogul/api/subscription_event.py @@ -31,6 +31,7 @@ class _Schema(Schema): retracted_event_id = fields.String(allow_none=True) external_id = fields.String(allow_none=True) event_order = fields.Int(allow_none=True) + disabled = fields.Bool(allow_none=True) @post_load def make(self, data, **kwargs): @@ -38,6 +39,46 @@ def make(self, data, **kwargs): _schema = _Schema(unknown=EXCLUDE) + @classmethod + def destroy(cls, config, **kwargs): + """Accept flat params and wrap in subscription_event envelope for the API.""" + data = dict(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 = dict(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 = dict(kwargs.get("data", {})) + if "subscription_event" in data: + data = {"subscription_event": dict(data["subscription_event"])} + data["subscription_event"]["disabled"] = True + else: + data["disabled"] = True + 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 = dict(kwargs.get("data", {})) + if "subscription_event" in data: + data = {"subscription_event": dict(data["subscription_event"])} + data["subscription_event"]["disabled"] = False + else: + data["disabled"] = False + data = {"subscription_event": data} + return cls.modify_with_params(config, data=data) + SubscriptionEvent.all = SubscriptionEvent._method("all", "get", "/subscription_events") SubscriptionEvent.destroy_with_params = SubscriptionEvent._method( 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/chartmogul/resource.py b/chartmogul/resource.py index a9d6d61..ca2d381 100644 --- a/chartmogul/resource.py +++ b/chartmogul/resource.py @@ -174,7 +174,7 @@ def _expandPath(cls, path, kwargs): def _validate_arguments(cls, method, kwargs): # This enforces user to pass argument, otherwise we could call # wrong URL. - if method in ["destroy", "cancel", "retrieve", "modify", "update"] and "uuid" not in kwargs: + if method in ["destroy", "cancel", "retrieve", "modify", "update", "disable"] and "uuid" not in kwargs: raise ArgumentMissingError("Please pass 'uuid' parameter") if method in ["create", "modify"] and "data" not in kwargs: raise ArgumentMissingError("Please pass 'data' parameter") diff --git a/test/api/test_account.py b/test/api/test_account.py index 1f68608..46fbcb8 100644 --- a/test/api/test_account.py +++ b/test/api/test_account.py @@ -5,13 +5,24 @@ from chartmogul import Account, Config, APIError -jsonResponse = { +base_response = { "name": "Example Test Company", "currency": "EUR", "time_zone": "Europe/Berlin", "week_start_on": "sunday", } +response_with_id = { + **base_response, + "id": "acct_a1b2c3d4", +} + +response_with_include = { + **response_with_id, + "churn_recognition": "immediate", + "churn_when_zero_mrr": "ignore", +} + class AccountTestCase(unittest.TestCase): """ @@ -25,7 +36,7 @@ def test_retrieve(self, mock_requests): "https://api.chartmogul.com/v1/account", request_headers={"Authorization": "Basic dG9rZW46"}, status_code=200, - json=jsonResponse, + json=base_response, ) config = Config("token") # is actually checked in mock @@ -35,3 +46,88 @@ 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=response_with_id, + ) + + 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=response_with_include, + ) + + 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"]}, + ) + + @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=base_response, + ) + + 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): + single_include_response = { + **response_with_id, + "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=single_include_response, + ) + + 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 74f74e9..ba5e906 100644 --- a/test/api/test_invoice.py +++ b/test/api/test_invoice.py @@ -8,6 +8,7 @@ from chartmogul import Config from chartmogul import APIError +from chartmogul import ArgumentMissingError from chartmogul import Invoice @@ -621,3 +622,242 @@ 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) + + def _make_errors_response(self, li_errors=None, tx_errors=None, include_errors=True): + """Build a minimal invoice response with configurable errors fields.""" + li = { + "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, + } + tx = { + "uuid": "tr_test", + "external_id": None, + "type": "payment", + "date": "2015-11-05T00:04:03.000Z", + "result": "successful", + } + if include_errors: + li["errors"] = li_errors + tx["errors"] = tx_errors + return { + "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": [li], + "transactions": [tx], + } + + @requests_mock.mock() + def test_line_item_and_transaction_errors(self, mock_requests): + response = self._make_errors_response( + li_errors={"amount_in_cents": ["must be positive"]}, + tx_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=response, + ) + + 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"]) + + @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) + + @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): + response = self._make_errors_response(li_errors=None, tx_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=response, + ) + + 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): + response = self._make_errors_response(include_errors=False) + + 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=response, + ) + + 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")) + + def test_disable_missing_uuid_raises(self): + config = Config("token") + with self.assertRaises(ArgumentMissingError): + Invoice.disable(config) diff --git a/test/api/test_subscription_event.py b/test/api/test_subscription_event.py index 0cf5da9..65516e2 100644 --- a/test/api/test_subscription_event.py +++ b/test/api/test_subscription_event.py @@ -224,6 +224,245 @@ 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_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( @@ -265,3 +504,65 @@ def test_all_subscription_events_with_filters(self, mock_requests): sorted(expected.subscription_events[0].external_id), ) self.assertTrue(isinstance(subscription_events.subscription_events[0], SubscriptionEvent)) + + @requests_mock.mock() + def test_disable_passthrough_envelope_sets_flag_inside(self, mock_requests): + """When caller passes pre-wrapped envelope, disabled flag must go inside it.""" + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + caller_data = {"subscription_event": {"id": 7654321}} + config = Config("token") + SubscriptionEvent.disable(config, data=caller_data).get() + + body = mock_requests.last_request.json() + # disabled must be inside the envelope, not at top level + self.assertNotIn("disabled", body) + self.assertTrue(body["subscription_event"]["disabled"]) + self.assertEqual(body["subscription_event"]["id"], 7654321) + # caller's dict must not be mutated + self.assertNotIn("disabled", caller_data["subscription_event"]) + + @requests_mock.mock() + def test_enable_passthrough_envelope_sets_flag_inside(self, mock_requests): + """When caller passes pre-wrapped envelope, disabled=False must go inside it.""" + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + caller_data = {"subscription_event": {"id": 7654321}} + config = Config("token") + SubscriptionEvent.enable(config, data=caller_data).get() + + body = mock_requests.last_request.json() + self.assertNotIn("disabled", body) + self.assertFalse(body["subscription_event"]["disabled"]) + self.assertEqual(body["subscription_event"]["id"], 7654321) + self.assertNotIn("disabled", caller_data["subscription_event"]) + + @requests_mock.mock() + def test_disable_does_not_mutate_caller_dict(self, mock_requests): + """Flat-param disable must not mutate the caller's dict in-place.""" + mock_requests.register_uri( + "PATCH", + "https://api.chartmogul.com/v1/subscription_events", + request_headers={"Authorization": "Basic dG9rZW46"}, + status_code=200, + json=expected_sub_ev, + ) + + caller_data = {"id": 7654321} + config = Config("token") + SubscriptionEvent.disable(config, data=caller_data).get() + + # caller's original dict should not have been modified + self.assertNotIn("disabled", caller_data)