Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ca0bf7a
Expose validation errors on LineItem and Transaction schemas (PIP-304)
wscourge Mar 17, 2026
6daf595
Add account ID and include query param support (PIP-120, PIP-76)
wscourge Mar 17, 2026
383f43a
Add Invoice update-status and disable endpoint support
wscourge Mar 17, 2026
0b7a467
Add flat-param interface and disable/enable for SubscriptionEvent
wscourge Mar 17, 2026
c445577
Add missing test coverage for recent SDK changes
wscourge Mar 17, 2026
51a3bee
Address PR review comments
wscourge Mar 18, 2026
53f9aaf
Refactor: move SubscriptionEvent methods into class body, add disable…
wscourge Mar 24, 2026
53719fc
Fix Account.churn_when_zero_mrr schema type: String → Raw
wscourge Mar 24, 2026
a9ff4d3
Add PIP-306: data_source_uuid + external_id query param support
wscourge Apr 7, 2026
b1ba8da
Fix flake8: remove extra blank line in invoice.py
wscourge Apr 7, 2026
41680bf
Add disabled fields to nested LineItem schema in Invoice
wscourge Apr 7, 2026
1aa6f88
Simplify SubscriptionEvent disable/enable to delegate to modify
wscourge Apr 7, 2026
8657e0c
Use _preProcessParams for query_params, handle_as_user_edit via _bool…
wscourge Apr 7, 2026
955d01e
Update tests to use overloaded method names instead of *_by_external_id
wscourge Apr 7, 2026
b83304b
Move ext_id dispatch and toggle_disabled into base Resource class
wscourge Apr 7, 2026
ff3fb2a
Add uuid path support to LineItem retrieve/modify/destroy
wscourge Apr 8, 2026
1ba4161
Add uuid-based tests for LineItem retrieve/modify/destroy
wscourge Apr 8, 2026
738b062
Add create and disable methods for LineItem and Transaction
wscourge Apr 8, 2026
654f1bc
Add uuid-based tests for Transaction retrieve/modify/destroy
wscourge Apr 8, 2026
65f96d1
Unify disable and toggle_disabled into a single disable method
wscourge Apr 8, 2026
ff44ec0
Remove SubscriptionEvent.modify/destroy, update _with_params to accep…
wscourge Apr 8, 2026
f80f854
Support id/external_id/data_source_uuid as separate kwargs on Subscri…
wscourge Apr 8, 2026
f503892
Unwrap ext_id retrieve to return single object instead of list
wscourge Apr 9, 2026
9b85c9e
Simplify _unwrap: replace nested def with lambda
wscourge Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions chartmogul/api/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions chartmogul/api/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
41 changes: 41 additions & 0 deletions chartmogul/api/subscription_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,54 @@ 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):
return SubscriptionEvent(**data)

_schema = _Schema(unknown=EXCLUDE)

@classmethod
def destroy(cls, config, **kwargs):
"""Accept flat params and wrap in subscription_event envelope for the API."""
Comment thread
loomchild marked this conversation as resolved.
Outdated
data = dict(kwargs.get("data", {}))
if "subscription_event" not in data:
data = {"subscription_event": data}
return cls.destroy_with_params(config, data=data)
Comment thread
loomchild marked this conversation as resolved.
Outdated

@classmethod
def modify(cls, config, **kwargs):
Comment thread
loomchild marked this conversation as resolved.
Outdated
"""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):
Comment thread
loomchild marked this conversation as resolved.
"""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}
Comment thread
loomchild marked this conversation as resolved.
Outdated
return cls.modify_with_params(config, data=data)

@classmethod
def enable(cls, config, **kwargs):
"""Enable a subscription event by setting disabled to false."""
Comment thread
MariaBraganca marked this conversation as resolved.
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(
Expand Down
1 change: 1 addition & 0 deletions chartmogul/api/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion chartmogul/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Comment thread
loomchild marked this conversation as resolved.
raise ArgumentMissingError("Please pass 'uuid' parameter")
if method in ["create", "modify"] and "data" not in kwargs:
raise ArgumentMissingError("Please pass 'data' parameter")
Expand Down
100 changes: 98 additions & 2 deletions test/api/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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
Expand All @@ -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"]},
)
Loading
Loading