Conversation
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the Python SDK to support additional API response fields and adds/adjusts client methods for invoice and subscription event operations, with accompanying test coverage.
Changes:
- Add deserialization support for
errorson invoice line items/transactions and for additional account fields (id, churn settings). - Add invoice endpoints for updating status and disabling invoices.
- Add
SubscriptionEvent.destroy/modify/disable/enablehelpers that accept “flat” params and wrap them in the requiredsubscription_eventenvelope, plus tests for these behaviors.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
chartmogul/api/transaction.py |
Adds errors field support on Transaction schema. |
chartmogul/api/invoice.py |
Adds errors support on invoice-related schemas and introduces update_status / disable methods. |
chartmogul/api/account.py |
Extends Account schema with optional id and churn-related fields. |
chartmogul/api/subscription_event.py |
Adds wrapper methods to accept flat params and to enable/disable subscription events. |
test/api/test_invoice.py |
Adds tests for nested errors fields and new invoice status/disable endpoints. |
test/api/test_account.py |
Adds tests for newly supported account fields and include query behavior. |
test/api/test_subscription_event.py |
Adds tests for flat-parameter wrapping and enable/disable helpers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
- Invoice.disable: rename method from "patch" to "disable" and add "disable" to _validate_arguments uuid check so calling without uuid raises ArgumentMissingError instead of silently producing a bad URL - SubscriptionEvent._disable/_enable: copy caller dict before mutation to avoid side effects; when payload is already wrapped in a subscription_event envelope, set the disabled flag inside it rather than at the top level Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d field, clean up tests - Move destroy/modify/disable/enable from module-scope into SubscriptionEvent as proper classmethods - Add disabled field to SubscriptionEvent schema so API responses are fully deserialized - Defensive dict() copy in destroy/modify for consistency with disable/enable - Rename camelCase test fixtures to snake_case, reduce duplication via spread and helper Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The API returns a boolean for churn_when_zero_mrr, not a string. Using fields.Raw allows any JSON type to be deserialized correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
loomchild
left a comment
There was a problem hiding this comment.
I don't really understand the changes. Maybe best ask someone who implemented the SDK before. I see it was changed a while ago, and many people aren't here anymore. @MariaBraganca - do you remember how the method is supposed to work by any chance?
mynameispedro
left a comment
There was a problem hiding this comment.
I'm not too familiar with the API.
But it looks good to me.
|
I'll try to test it manually as much as I understand the changes. General comment for future: it'd be much easier to review and test if you separated independent changes into individual PRs, instead of cramming everything together. |
|
I found a few potential issues during testing, please let me know what do you think:
|
Add retrieve_by_external_id, update_by_external_id, destroy_by_external_id, and toggle_disabled_by_external_id methods to Invoice, Transaction, and a new standalone LineItem resource. Matches Ruby SDK's ExternalIdOperations. - Add query_params support to Resource._request() for non-GET verbs - Create standalone LineItem resource (chartmogul/api/line_item.py) - Add disabled/disabled_at/disabled_by fields to Transaction schema - Export LineItem from chartmogul package - Add 18 unit tests for external_id operations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 4: this is the natural next step, but for backwards compatibility we first introduce the new, simpler way to use it, and in time we'll release the mayor update with the breaking change
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@loomchild also answering your 1st question
that was my initial approach, until I started actually implementing it: retrieve currently validates that uuid is present, and one would need to change it to "uuid OR (data_source_uuid AND external_id)", which complicates the base I opted on adding separate methods, they make the intent clear. |
|
Thanks for your replies. 2. So now I understand I think valid statuses that can be set are 4. Ah, so the envelope was accepted before in the SDK. I thought you were just adding it in this PR. Then it makes sense for backward compatibility. Regarding retrieve, I disagree that it violates SOLID and it makes DX worse by unnecessarily multiplying methods instead of overriding them. Actually, the same criticism can be said about unwrapping convenience logic - you introduced an alternative way of calling the method, instead of adding a new method. We also allow it in other parts of the SDK - e.g. Maybe that's a difference in philosophy of Python's only one way of doing things vs Ruby's expressiveness, but I still think adding a different method for every set of parameters seems over the top. What do other's think? |
| _schema = _Schema(unknown=EXCLUDE) | ||
|
|
||
| @classmethod | ||
| def retrieve_by_external_id(cls, config, **kwargs): |
There was a problem hiding this comment.
Invoice, LineItem, and Transaction each define the same 4 methods, except for the hardcoded path string - is this correct?
Not a blocker but maybe there is a refactoring potential in here.
There was a problem hiding this comment.
good point, there is, I've been already thinking about it thanks to Jarek's comments, I'm on it - thanks for paying attention
|
@loomchild this actually convinces me:
and I am going to extend |
| def _request(cls, config, method, http_verb, path, data=None, query_params=None, **kwargs): | ||
| if http_verb == "get": | ||
| params = cls._preProcessParams(kwargs) | ||
| params = query_params if query_params is not None else cls._preProcessParams(kwargs) |
There was a problem hiding this comment.
You can preprocess query params as well.
params = cls._preProcessParams(query_params if query_params is not None else kwargs)
Now you have access to _bool_query_params and can add 'handle_as_user_edit' on the respective classes.
This was a claude suggestion, you should be able to simplify this:
if kwargs.get("handle_as_user_edit") is not None:
params["handle_as_user_edit"] = str(kwargs["handle_as_user_edit"]).lower()
to
params["handle_as_user_edit"] = kwargs["handle_as_user_edit"]
on _build_ext_id_params()
There was a problem hiding this comment.
thank you, that's what I did 👍
The standalone LineItem resource had disabled/disabled_at/disabled_by but the nested LineItem DataObject used for Invoice deserialization was missing them. The API returns these fields on line items inside invoice responses. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of duplicating the envelope-wrapping logic, disable/enable now unwrap the envelope if present, set the disabled flag, and delegate to modify() which handles the wrapping. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…_query_params Instead of manually converting handle_as_user_edit to a string in _build_ext_id_params, route query_params through _preProcessParams which handles boolean-to-string conversion via _bool_query_params. Added handle_as_user_edit to _bool_query_params on Invoice, LineItem, and Transaction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of duplicating dispatch logic in Invoice, Transaction, and LineItem, the _method() factory in Resource now detects data_source_uuid + external_id kwargs and dispatches to the query-param codepath when the class declares _ext_id_path. toggle_disabled is also now a generic classmethod on Resource using _ext_id_path + "/disabled_state". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@loomchild @MariaBraganca I updated moving the params resolution to resource, I am still testing this but it looks good so far and I think you can already take a look - thanks for the thorough reviews |
| _schema = _Schema(unknown=EXCLUDE) | ||
|
|
||
|
|
||
| LineItem.retrieve = LineItem._method("retrieve", "get", "/line_items") |
There was a problem hiding this comment.
why are we not using the same pattern with uuid here?
is it different? dont the validations require it?
There was a problem hiding this comment.
👍 it is not, I added them, I got so focused on the dsc_id + ext_id that I omitted this one, thank you
|
I'll review & testit this afternoon, sorry for delay. |
LineItem API endpoints support both uuid path params and data_source_uuid + external_id query params. The _ext_id_path dispatch handles the latter, uuid in path handles the former. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- LineItem.create: POST /import/invoices/{uuid}/line_items
- LineItem.disable: PATCH /line_items/{uuid}/disabled_state
- Transaction.disable: PATCH /transactions/{uuid}/disabled_state
- Fix Invoice.disable path from /disable to /disabled_state
- Add tests for all new methods
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The _method ext_id dispatch now preserves path suffixes after {/uuid},
so disable (PATCH /{resource}{/uuid}/disabled_state) automatically
dispatches to /{resource}/disabled_state with query params when
data_source_uuid + external_id are provided. Removes toggle_disabled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…t flat params modify_with_params and destroy_with_params now accept both flat params (auto-wrapped into subscription_event envelope) and pre-wrapped envelope (passed through). The separate modify/destroy methods are removed since they were new and had no existing callers. disable/enable delegate to modify_with_params. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
I finally did some more testing, I see that you just fixed an issue in Here are my comments based on the issues I found previously:
|
…ptionEvent
modify_with_params, destroy_with_params, disable, and enable now accept
identifier params as top-level kwargs matching other resources' patterns:
SubscriptionEvent.modify_with_params(config, id=123, data={'amount': 2000})
All three existing call styles remain supported (backwards compatible):
- New: (config, id=123, data={...})
- Flat: (config, data={'id': 123, ...})
- Envelope: (config, data={'subscription_event': {'id': 123, ...}})
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
|
|
3. it is already there, and the method naming is consistent with other SDKs, I need to keep it like this for the backwards-compatibility |
When retrieve is called with data_source_uuid + external_id, the API returns a list. The SDK now unwraps the first item so the return type matches uuid-based retrieve (single object or None if not found). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
So after you broke it down to separate PRs, we don't have to worry about this one anymore, right? |
|
yes, I decided to break it down into multiple smaller PRs, because it was both harder to work on it and to review it I imagine - closing it |


Summary
errorsfield onLineItemandTransactionschemas (Invoice already had it)id,churn_recognition,churn_when_zero_mrrfields toAccountschema; supportincludequery param on/accountdata_source_uuid+external_idquery param support for Invoices, Line Items, and Transactions. All existing methods (retrieve,destroy,modify,disable) auto-dispatch to the query-param codepath whendata_source_uuid+external_idare passed instead ofuuid.update_status(PATCH/invoices/{uuid}) anddisable(PATCH/invoices/{uuid}/disabled_state)LineItemresource withcreate(POST/import/invoices/{uuid}/line_items),retrieve,modify,destroy,disable— all supporting both uuid and ext_id identificationretrieve,modify,destroy,disablemethods at/transactions— all supporting both uuid and ext_id identificationdestroy_with_params()/modify_with_params()to accept both flat params and envelope-wrapped params; adddisable()/enable()convenience methodsBackwards compatibility review
All changes are purely additive — no existing public API surface was modified or removed.
resource.pyquery_paramsparam to_request(); ext_id dispatch in_method()via_ext_id_path;_build_ext_id_params()helper;"disable"in uuid-required validation listaccount.pyid,churn_recognition,churn_when_zero_mrrschema fieldsallow_none=True, old responses still deserializeinvoice.pyerrors,disabled,disabled_at,disabled_byto nestedLineItem._Schema;_ext_id_path;update_status,disablemethodstransaction.pydisabled,disabled_at,disabled_by,transaction_fees_currency,errorsschema fields;_ext_id_path; standaloneretrieve,modify,destroy,disablemethodsline_item.pyLineItem(Resource)withcreate,retrieve,modify,destroy,disablesubscription_event.pydisabledfield to schema; updateddestroy_with_params/modify_with_paramsto accept flat params; addeddisable/enableclassmethods__init__.pyLineItemexportTest plan
handle_as_user_editsupportTesting instructions
Integration tests were run against account WiktorOnboarding (
acc_d0ea225e-f0f1-40ab-92cf-659dce5f2b76). To obtain the API key, impersonatewiktor@chartmogul.comin the admin panel and find it under Profile > API keys. The full test script is atsdks/tests/py/test_pip_310.py— set theCHARTMOGUL_API_KEYenv var and run it.Account.id field (PIP-120)
Account include query param (PIP-76)
LineItem and Transaction errors field (PIP-304)
Invoice.update_status
Invoice.disable (uuid-based)
Invoice.disable (ext_id-based, PIP-306)
Invoice.retrieve (ext_id-based, PIP-306)
Invoice.update_status (ext_id-based, PIP-306)
Invoice.destroy (ext_id-based, PIP-306)
LineItem — all operations (PIP-306)
Transaction — all operations (PIP-306)
SubscriptionEvent.modify_with_params — flat and envelope
SubscriptionEvent.destroy_with_params — flat and envelope
SubscriptionEvent.disable and enable
🤖 Generated with Claude Code