Skip to content

Add missing features and new endpoints#129

Closed
wscourge wants to merge 24 commits intomainfrom
wiktor/pip-310-python-sdk-feature-updates
Closed

Add missing features and new endpoints#129
wscourge wants to merge 24 commits intomainfrom
wiktor/pip-310-python-sdk-feature-updates

Conversation

@wscourge
Copy link
Copy Markdown
Contributor

@wscourge wscourge commented Mar 17, 2026

Summary

  • PIP-304: Expose errors field on LineItem and Transaction schemas (Invoice already had it)
  • PIP-120 / PIP-76: Add id, churn_recognition, churn_when_zero_mrr fields to Account schema; support include query param on /account
  • PIP-306: Add data_source_uuid + external_id query param support for Invoices, Line Items, and Transactions. All existing methods (retrieve, destroy, modify, disable) auto-dispatch to the query-param codepath when data_source_uuid + external_id are passed instead of uuid.
  • Invoice: Add update_status (PATCH /invoices/{uuid}) and disable (PATCH /invoices/{uuid}/disabled_state)
  • LineItem: New standalone LineItem resource with create (POST /import/invoices/{uuid}/line_items), retrieve, modify, destroy, disable — all supporting both uuid and ext_id identification
  • Transaction: Add standalone retrieve, modify, destroy, disable methods at /transactions — all supporting both uuid and ext_id identification
  • SubscriptionEvent: Update destroy_with_params() / modify_with_params() to accept both flat params and envelope-wrapped params; add disable() / enable() convenience methods

Backwards compatibility review

All changes are purely additive — no existing public API surface was modified or removed.

File Change Breaking?
resource.py Added query_params param to _request(); ext_id dispatch in _method() via _ext_id_path; _build_ext_id_params() helper; "disable" in uuid-required validation list No — default values, no existing caller affected
account.py Added id, churn_recognition, churn_when_zero_mrr schema fields No — allow_none=True, old responses still deserialize
invoice.py Added errors, disabled, disabled_at, disabled_by to nested LineItem._Schema; _ext_id_path; update_status, disable methods No — additive fields and new methods only
transaction.py Added disabled, disabled_at, disabled_by, transaction_fees_currency, errors schema fields; _ext_id_path; standalone retrieve, modify, destroy, disable methods No — additive fields and new methods
line_item.py New file — standalone LineItem(Resource) with create, retrieve, modify, destroy, disable No — new file, new export
subscription_event.py Added disabled field to schema; updated destroy_with_params / modify_with_params to accept flat params; added disable / enable classmethods No — envelope-wrapped calls still work; flat params are a new accepted format
__init__.py Added LineItem export No — additive

Test plan

  • Existing 103 tests still pass
  • Added 55 new unit tests covering all new methods, both uuid and ext_id paths, error cases, handle_as_user_edit support
  • 158 total tests passing
  • Flake8 linting passes

Testing instructions

Integration tests were run against account WiktorOnboarding (acc_d0ea225e-f0f1-40ab-92cf-659dce5f2b76). To obtain the API key, impersonate wiktor@chartmogul.com in the admin panel and find it under Profile > API keys. The full test script is at sdks/tests/py/test_pip_310.py — set the CHARTMOGUL_API_KEY env var and run it.

cd sdks/py
pip3 install -e .
export CHARTMOGUL_API_KEY='...'
python3 sdks/tests/py/test_pip_310.py

Account.id field (PIP-120)

account = chartmogul.Account.retrieve(config).get()
assert account.id is not None and account.id.startswith('acc_')

Account include query param (PIP-76)

account = chartmogul.Account.retrieve(
    config, include='churn_recognition,churn_when_zero_mrr'
).get()
assert hasattr(account, 'churn_recognition')
assert hasattr(account, 'churn_when_zero_mrr')

# Without include, fields should be absent
account_basic = chartmogul.Account.retrieve(config).get()
assert not hasattr(account_basic, 'churn_recognition')

LineItem and Transaction errors field (PIP-304)

invoice = chartmogul.Invoice.retrieve(
    config, uuid='inv_...', validation_type='all'
).get()

# Invoice-level errors accessible
print(f"invoice.errors = {getattr(invoice, 'errors', 'NOT SET')}")

# LineItem/Transaction schema supports errors field
# (API omits the key when no validation errors exist)
for li in invoice.line_items:
    print(f"li.errors = {getattr(li, 'errors', 'absent (no errors)')}")

Invoice.update_status

# PUT /data_sources/{ds_uuid}/invoices/{external_id}/status
# Valid statuses: "voided", "written_off"
result = chartmogul.Invoice.update_status(
    config,
    uuid='inv_...',
    data={'status': 'voided'}
).get()

Invoice.disable (uuid-based)

# PATCH /invoices/{uuid}/disabled_state
chartmogul.Invoice.disable(
    config, uuid='inv_...', data={'disabled': True}
).get()

# Missing uuid raises ArgumentMissingError
try:
    chartmogul.Invoice.disable(config)
except chartmogul.ArgumentMissingError:
    print("Correctly raised")

Invoice.disable (ext_id-based, PIP-306)

# PATCH /invoices/disabled_state?data_source_uuid=...&external_id=...
chartmogul.Invoice.disable(
    config,
    data_source_uuid='ds_...',
    external_id='inv_ext_...',
    data={'disabled': True}
).get()

Invoice.retrieve (ext_id-based, PIP-306)

# GET /invoices?data_source_uuid=...&external_id=...
result = chartmogul.Invoice.retrieve(
    config,
    data_source_uuid='ds_...',
    external_id='in_...'
).get()
assert len(result.invoices) > 0

Invoice.update_status (ext_id-based, PIP-306)

# PATCH /invoices?data_source_uuid=...&external_id=...
result = chartmogul.Invoice.update_status(
    config,
    data_source_uuid='ds_...',
    external_id='in_...',
    data={'currency': 'USD'}
).get()

# With handle_as_user_edit
result = chartmogul.Invoice.update_status(
    config,
    data_source_uuid='ds_...',
    external_id='in_...',
    handle_as_user_edit=True,
    data={'currency': 'USD'}
).get()

Invoice.destroy (ext_id-based, PIP-306)

# DELETE /invoices?data_source_uuid=...&external_id=...
chartmogul.Invoice.destroy(
    config,
    data_source_uuid='ds_...',
    external_id='in_...'
).get()

LineItem — all operations (PIP-306)

# Create (POST /import/invoices/{uuid}/line_items)
li = chartmogul.LineItem.create(
    config, uuid='inv_...', data={...}
).get()

# Retrieve by uuid
li = chartmogul.LineItem.retrieve(config, uuid='li_...').get()

# Retrieve by ext_id
li = chartmogul.LineItem.retrieve(
    config, data_source_uuid='ds_...', external_id='li_ext_...'
).get()

# Modify by uuid
chartmogul.LineItem.modify(config, uuid='li_...', data={...}).get()

# Modify by ext_id (with handle_as_user_edit)
chartmogul.LineItem.modify(
    config, data_source_uuid='ds_...', external_id='li_ext_...',
    handle_as_user_edit=True, data={...}
).get()

# Destroy by uuid
chartmogul.LineItem.destroy(config, uuid='li_...').get()

# Destroy by ext_id
chartmogul.LineItem.destroy(
    config, data_source_uuid='ds_...', external_id='li_ext_...'
).get()

# Disable by uuid
chartmogul.LineItem.disable(
    config, uuid='li_...', data={'disabled': True}
).get()

# Disable by ext_id
chartmogul.LineItem.disable(
    config, data_source_uuid='ds_...', external_id='li_ext_...',
    data={'disabled': True}
).get()

Transaction — all operations (PIP-306)

# Retrieve / modify / destroy / disable all support both uuid and ext_id:
tx = chartmogul.Transaction.retrieve(config, uuid='tr_...').get()
tx = chartmogul.Transaction.retrieve(
    config, data_source_uuid='ds_...', external_id='tr_ext_...'
).get()

chartmogul.Transaction.disable(
    config, uuid='tr_...', data={'disabled': True}
).get()
chartmogul.Transaction.disable(
    config, data_source_uuid='ds_...', external_id='tr_ext_...',
    data={'disabled': True}
).get()

SubscriptionEvent.modify_with_params — flat and envelope

# Flat params (auto-wrapped into subscription_event envelope)
se = chartmogul.SubscriptionEvent.modify_with_params(
    config, data={'id': 123}
).get()

# Envelope passthrough (already wrapped — passed through as-is)
se = chartmogul.SubscriptionEvent.modify_with_params(
    config, data={'subscription_event': {'id': 123}}
).get()

SubscriptionEvent.destroy_with_params — flat and envelope

# Flat params
chartmogul.SubscriptionEvent.destroy_with_params(
    config, data={'id': 123}
).get()

# Also works with external_id + data_source_uuid
chartmogul.SubscriptionEvent.destroy_with_params(
    config, data={'external_id': '...', 'data_source_uuid': 'ds_...'}
).get()

# Envelope passthrough
chartmogul.SubscriptionEvent.destroy_with_params(
    config, data={'subscription_event': {'id': 123}}
).get()

SubscriptionEvent.disable and enable

se = chartmogul.SubscriptionEvent.disable(config, data={'id': 123}).get()
se = chartmogul.SubscriptionEvent.enable(config, data={'id': 123}).get()

# Works with external_id + data_source_uuid
chartmogul.SubscriptionEvent.disable(
    config, data={'external_id': '...', 'data_source_uuid': 'ds_...'}
).get()

# Works with envelope
chartmogul.SubscriptionEvent.disable(
    config, data={'subscription_event': {'id': 123}}
).get()

🤖 Generated with Claude Code

wscourge and others added 5 commits March 17, 2026 17:04
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>
@wscourge wscourge marked this pull request as ready for review March 17, 2026 16:16
@wscourge wscourge requested a review from Copilot March 17, 2026 16:18
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 errors on 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/enable helpers that accept “flat” params and wrap them in the required subscription_event envelope, 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.

Comment thread chartmogul/api/invoice.py Outdated
Comment thread chartmogul/api/subscription_event.py Outdated
Comment thread chartmogul/api/subscription_event.py Outdated
- 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>
@wscourge wscourge changed the title Wiktor/pip 310 python sdk feature updates Add missing SDK features (PIP-304, PIP-120, PIP-76) and new endpoints Mar 18, 2026
@wscourge wscourge changed the title Add missing SDK features (PIP-304, PIP-120, PIP-76) and new endpoints Add missing features and new endpoints Mar 20, 2026
wscourge and others added 2 commits March 24, 2026 13:25
…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>
@wscourge wscourge requested review from loomchild and pkopac April 1, 2026 10:29
@pkopac pkopac requested review from jessicahearn and mynameispedro and removed request for pkopac April 1, 2026 11:43
Copy link
Copy Markdown

@loomchild loomchild left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread chartmogul/resource.py
Comment thread chartmogul/api/subscription_event.py Outdated
Comment thread chartmogul/api/subscription_event.py Outdated
Comment thread chartmogul/api/subscription_event.py Outdated
Comment thread chartmogul/api/subscription_event.py
Copy link
Copy Markdown
Contributor

@mynameispedro mynameispedro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too familiar with the API.
But it looks good to me.

@loomchild
Copy link
Copy Markdown

loomchild commented Apr 6, 2026

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.

@loomchild
Copy link
Copy Markdown

I found a few potential issues during testing, please let me know what do you think:

  1. chartmogul.Invoice.retrieve_by_external_id(config, data_source_uuid='2c6d4a4a-7c1e-11ef-950c-a77409b7d671', external_id='aaa').get() returns AttributeError: type object 'Invoice' has no attribute 'retrieve_by_external_id'. Am I doing something wrong, does it work for you? BTW, can't we extend existing retreive method and allow alternative parameters instead of creating a new method (same for update, etc.)?
  2. When I try to disable a Recurly invoice, I still get 404. Does it work for you? BTW, why do we have two separate methods for the same thing - does update_status offer more options, or is it for backwards-compatibility (but I understand from description that this method is just being added)? I would drop this method entirely if possible and accept status field when calling modify instead.
  3. chartmogul.SubscriptionEvent.modify(config, data={id:544776074}) produces Please pass 'id' parameter or 'data_source_uuid' and 'external_id'. Does it work for you? BTW shouldn't we pass id as a separate parameter in addition to data, similar to uuid in other objects?
  4. Why do we want to allow passing envelope object at all? Shouldn't it always be flat params, like for other objects? I want to be certain, since if we introduce it, it'll be impossible to remove later without breaking backwards compatibility.

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>
@wscourge
Copy link
Copy Markdown
Contributor Author

wscourge commented Apr 7, 2026

@loomchild

  • 1, 2, 3: I am so so sorry, I didn't push my most recent changes and have only updated the PR description 🤦.
  • 2: status and disabled are different invoice fields, and my understanding is that we aim to have dedicated SDK methods for the public API endpoints, and there are separate Update an Invoice Status and Disable an Invoice
image - 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>
@wscourge
Copy link
Copy Markdown
Contributor Author

wscourge commented Apr 7, 2026

@loomchild also answering your 1st question

BTW, can't we extend existing retreive method and allow alternative parameters instead of creating a new method (same for update, etc.)?

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 Resource validation logic for all resources - it'd be a classic violation of solId's Interface Segregation Principle

I opted on adding separate methods, they make the intent clear.

@loomchild
Copy link
Copy Markdown

loomchild commented Apr 7, 2026

Thanks for your replies.

2. So now I understand Invoice.update_status is different from disable / enable. What confused me is the example in the description:

result = chartmogul.Invoice.update_status(
    config,
    uuid='inv_e81f78fc-00ab-4a16-95ab-d1f0cd9cd481',
    data={'disabled': True}
).get()

I think valid statuses that can be set are written_off and voided. Please make sure to update the description and check if it works.

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. Invoice.all accepts customer_uuid and external_id in addition to uuid. Maybe you need to specialize the retrieve method only for relevant resources instead of modifying it for all of them.

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?

Comment thread chartmogul/api/line_item.py
Comment thread chartmogul/api/line_item.py Outdated
_schema = _Schema(unknown=EXCLUDE)

@classmethod
def retrieve_by_external_id(cls, config, **kwargs):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, there is, I've been already thinking about it thanks to Jarek's comments, I'm on it - thanks for paying attention

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated 👍

@wscourge
Copy link
Copy Markdown
Contributor Author

wscourge commented Apr 7, 2026

@loomchild this actually convinces me:

Invoice.all accepts customer_uuid and external_id in addition to uuid

and I am going to extend retrieve's logic, thanks. I'll let you know once it is done.

Comment thread chartmogul/resource.py Outdated
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, that's what I did 👍

wscourge and others added 5 commits April 7, 2026 14:15
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>
@wscourge
Copy link
Copy Markdown
Contributor Author

wscourge commented Apr 7, 2026

@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

Comment thread chartmogul/api/line_item.py Outdated
_schema = _Schema(unknown=EXCLUDE)


LineItem.retrieve = LineItem._method("retrieve", "get", "/line_items")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we not using the same pattern with uuid here?
is it different? dont the validations require it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 it is not, I added them, I got so focused on the dsc_id + ext_id that I omitted this one, thank you

@loomchild
Copy link
Copy Markdown

I'll review & testit this afternoon, sorry for delay.

wscourge and others added 5 commits April 8, 2026 12:02
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>
Comment thread chartmogul/api/subscription_event.py Outdated
…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>
@loomchild
Copy link
Copy Markdown

loomchild commented Apr 8, 2026

I finally did some more testing, I see that you just fixed an issue in SubscriptionEvents 👍

Here are my comments based on the issues I found previously:

  1. Retrieving an invoice by external ID returns a different result type than retrieving an invoice by UUID. It returns a list of entities instead of a single entity, and if unknown parameters are provided, it returns an empty list instead of raising a not found exception. Also, if you specify an empty external_id, it retrieves all invoices from the data source. I think this is incorrect, because there's a separate all method for retrieving entities in bulk.

  2. Disable seems to work now. However, when I try to retrieve a disabled invoice, I get a not found error. Is it expected? Is there a parameter that allows me to retrieve disabled invoices?

  3. Regarding subscription events, shouldn't we pass the IDs separately from data, similar to how modify works for other entities like Subscriptions, e.g. chartmogul.SubscriptionEvent.modify_with_params(config, id=544776074, data={<properties to update>})? BTW, why can't we merge modify and modify_with_params, what is the difference between them?

…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>
@wscourge
Copy link
Copy Markdown
Contributor Author

wscourge commented Apr 8, 2026

@loomchild

  1. You are right about that, sorry I thought you were aware it works this way when you suggested single retrieve implementation for both endpoints: The API endpoint GET /invoices?data_source_uuid=...&external_id=... returns {"invoices": [...]} (a list), while GET /invoices/{uuid} returns a single invoice object. Do you think I should unwrap the first result to match the uuid retrieve?
  2. Yes there is a query param with_disabled, you need to pass with_disabled=True as a query param: Invoice.retrieve(config, uuid='inv_...', with_disabled=True).
  3. Very very good point, thank you, I implemented it this way bcs API requires passing the id to the envelope, I'll update to match other methods

@loomchild
Copy link
Copy Markdown

loomchild commented Apr 8, 2026

  1. Yes, I think you should unwrap it. Or if you think current interface is fine, then don't change retrieve method at all, since fetching invoices by data_source_uuid & external_id already works via the all method and is consistent.
  2. Thanks, it works 👍
  3. Thanks, it seems to work for modify_with_params method. But why not for retrieve or modify (in a backwards-compatible manner)? Also, why do we need a new modify_with_params method instead of extending modify?

@wscourge
Copy link
Copy Markdown
Contributor Author

wscourge commented Apr 9, 2026

@loomchild

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
image

wscourge and others added 2 commits April 9, 2026 10:01
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>
@wscourge wscourge requested a review from loomchild April 9, 2026 08:46
@loomchild
Copy link
Copy Markdown

So after you broke it down to separate PRs, we don't have to worry about this one anymore, right?

@wscourge
Copy link
Copy Markdown
Contributor Author

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

@wscourge wscourge closed this Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants