Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e6656f8
Add SDK features for v4.12.0
jessicahearn Feb 13, 2026
0fe33d7
Add handle_as_user_edit parameter to write methods
jessicahearn Feb 13, 2026
b53e9c8
Add errors field to LineItem and Transaction resources
wscourge Mar 17, 2026
b84ff8c
Add account ID to Retrieve Account Details response
wscourge Mar 17, 2026
343d8fd
Support flat params for SubscriptionEvent destroy action
wscourge Mar 17, 2026
1bb31e2
Remove outdated post-install gem message
wscourge Mar 17, 2026
fe170b0
Add tests for standalone LineItem and Transaction resources
wscourge Mar 17, 2026
915baa9
Add tests for Invoice toggle_disabled, update_status, and by_external…
wscourge Mar 17, 2026
ab9a165
Add tests for SubscriptionEvent toggle_disabled and by_external_id me…
wscourge Mar 17, 2026
ab45cb8
Add tests for Account include parameter and id field
wscourge Mar 17, 2026
e07775a
Add tests for DataSource empty! and soft_purge! methods
wscourge Mar 17, 2026
575b79b
Add tests for JsonImport and Upload resources
wscourge Mar 17, 2026
5d7208c
URL-encode user-supplied query parameters in API request paths
wscourge Mar 18, 2026
6e256e2
Add missing middleware to Upload multipart connection
wscourge Mar 18, 2026
e2b6945
Extract shared concerns and helpers to reduce duplication
wscourge Mar 24, 2026
4797ca2
Update changelog date and remove unused VALID_INCLUDE_FIELDS constant
wscourge Mar 25, 2026
ba8a9f8
Remove DataSource empty! and soft_purge! methods
wscourge Mar 25, 2026
8c2ed0c
Add gem faraday-multipart as a dependency
wscourge Apr 1, 2026
c00f425
Fix self-review
wscourge Apr 1, 2026
4601e32
Merge branch 'main' into jessica/pip-309-ruby-sdk-feature-updates
wscourge Apr 8, 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
10 changes: 10 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# chartmogul-ruby Change Log

## Version 4.13.0 - Mar 31, 2026
- Add Account `include` parameter support for optional settings (churn_recognition, churn_when_zero_mrr, auto_churn_subscription, refund_handling, proximate_movement_reclassification)
- Add Invoice `update!`, `toggle_disabled!`, and `update_status!` methods
- Add SubscriptionEvent `toggle_disabled!` method
- Add standalone Transaction resource with CRUD operations and `toggle_disabled!`
- Add standalone LineItem resource with CRUD operations and `toggle_disabled!`
- Add external ID lookup methods (`*_by_external_id`) for Invoice, Transaction, LineItem, and SubscriptionEvent
- Add JsonImport resource for bulk JSON imports
- Add Upload resource for CSV file uploads (requires `faraday-multipart` gem)

## Version 4.12.0 - Mar 16, 2026
- Add `external_id` key to Contact model

Expand Down
7 changes: 1 addition & 6 deletions chartmogul-ruby.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,13 @@ Gem::Specification.new do |spec|
spec.license = 'MIT'
spec.required_ruby_version = '>= 3.2'

spec.post_install_message = %q{
Starting October 29 2021, we are updating our developer libraries to support the enhanced API Access Management. Please use the same API Key for both API Token and Secret Key.
[Deprecation] - account_token/secret_key combo is deprecated. Please use API key for both fields.
Version 3.x will introduce a breaking change in authentication configuration. For more details, please visit: https://dev.chartmogul.com/docs/authentication
}

spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ['lib']

spec.add_dependency 'faraday', '~> 2.8'
spec.add_dependency 'faraday-multipart', '~> 1.0'
spec.add_dependency 'faraday-retry', '~> 2.2'

spec.add_development_dependency 'cgi'
Expand Down
6 changes: 6 additions & 0 deletions lib/chartmogul.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
require 'chartmogul/concerns/pageable_with_cursor'
require 'chartmogul/concerns/auto_churn_subscription_setting'
require 'chartmogul/concerns/processing_status'
require 'chartmogul/concerns/toggle_disabled'
require 'chartmogul/concerns/external_id_operations'

require 'chartmogul/subscription'
require 'chartmogul/invoice'
Expand All @@ -70,6 +72,10 @@
require 'chartmogul/subscription_event'
require 'chartmogul/opportunity'
require 'chartmogul/task'
require 'chartmogul/transaction'
require 'chartmogul/line_item'
require 'chartmogul/json_import'
require 'chartmogul/upload'

require 'chartmogul/metrics/arpa'
require 'chartmogul/metrics/arr'
Expand Down
17 changes: 15 additions & 2 deletions lib/chartmogul/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,28 @@ class Account < APIResource
set_resource_name 'Account'
set_resource_path '/v1/account'

readonly_attr :id
readonly_attr :name
readonly_attr :currency
readonly_attr :time_zone
readonly_attr :week_start_on

# Optional attributes returned when using the include parameter
readonly_attr :churn_recognition
readonly_attr :churn_when_zero_mrr
readonly_attr :auto_churn_subscription
readonly_attr :refund_handling
readonly_attr :proximate_movement_reclassification

include API::Actions::Custom

def self.retrieve
custom!(:get, '/v1/account')
def self.retrieve(include: nil)
path = '/v1/account'
if include
fields = Array(include).map { |f| CGI.escape(f) }.join(',')
path += "?include=#{fields}"
end
Comment thread
wscourge marked this conversation as resolved.
custom!(:get, path)
end
end
end
27 changes: 27 additions & 0 deletions lib/chartmogul/api_resource.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "cgi"
require "uri"
require "forwardable"
require "set"

Expand Down Expand Up @@ -162,6 +164,31 @@ def self.custom_with_query_params!(http_method, body_data = {}, resource_key = n
custom!(http_method, path, attrs)
end

# Build a URL path with query parameters, handling ? vs & correctly
def self.build_query_path(base_path, **params)
filtered = params.compact
return base_path if filtered.empty?

separator = base_path.include?("?") ? "&" : "?"
"#{base_path}#{separator}#{URI.encode_www_form(filtered)}"
end

# Convenience for JSON PATCH requests
def self.json_patch(path, body)
connection.patch(path) do |req|
req.headers["Content-Type"] = "application/json"
req.body = JSON.dump(body)
end
end

# Convenience for JSON POST requests
def self.json_post(path, body)
connection.post(path) do |req|
req.headers["Content-Type"] = "application/json"
req.body = JSON.dump(body)
end
end

def self.build_connection
Faraday.new(url: ChartMogul.api_base,
headers: { "User-Agent" => "chartmogul-ruby/#{ChartMogul::VERSION}" }) do |faraday|
Expand Down
68 changes: 68 additions & 0 deletions lib/chartmogul/concerns/external_id_operations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

module ChartMogul
module Concerns
# Shared class methods for resources that support CRUD by data_source_uuid + external_id.
# These resources use query-parameter-based lookups (not path-based).
module ExternalIdOperations
def self.included(base)
base.extend ClassMethods
end

module ClassMethods
# Retrieve a resource by data_source_uuid and external_id
def retrieve_by_external_id(data_source_uuid:, external_id:)
path = build_query_path(
resource_path.path,
data_source_uuid: data_source_uuid,
external_id: external_id
)
resp = handling_errors { connection.get(path) }
json = ChartMogul::Utils::JSONParser.parse(resp.body, immutable_keys: immutable_keys)
new_from_json(json)
end

# Update a resource by data_source_uuid and external_id
# @param handle_as_user_edit [Boolean] If true, the change is treated as a user edit
def update_by_external_id!(data_source_uuid:, external_id:, handle_as_user_edit: nil, **attributes)
path = build_query_path(
resource_path.path,
data_source_uuid: data_source_uuid,
external_id: external_id,
handle_as_user_edit: handle_as_user_edit
)
resp = handling_errors { json_patch(path, attributes) }
json = ChartMogul::Utils::JSONParser.parse(resp.body, immutable_keys: immutable_keys)
new_from_json(json)
end

# Delete a resource by data_source_uuid and external_id
# @param handle_as_user_edit [Boolean] If true, the change is treated as a user edit
def destroy_by_external_id!(data_source_uuid:, external_id:, handle_as_user_edit: nil)
path = build_query_path(
resource_path.path,
data_source_uuid: data_source_uuid,
external_id: external_id,
handle_as_user_edit: handle_as_user_edit
)
handling_errors { connection.delete(path) }
true
end

# Toggle disabled state of a resource by data_source_uuid and external_id
# @param handle_as_user_edit [Boolean] If true, the change is treated as a user edit
def toggle_disabled_by_external_id!(data_source_uuid:, external_id:, disabled:, handle_as_user_edit: nil)
path = build_query_path(
"#{resource_path.path}/disabled_state",
data_source_uuid: data_source_uuid,
external_id: external_id,
handle_as_user_edit: handle_as_user_edit
)
resp = handling_errors { json_patch(path, { disabled: disabled }) }
json = ChartMogul::Utils::JSONParser.parse(resp.body, immutable_keys: immutable_keys)
new_from_json(json)
end
end
end
end
end
24 changes: 24 additions & 0 deletions lib/chartmogul/concerns/toggle_disabled.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module ChartMogul
module Concerns
# Shared toggle_disabled! instance method for resources that support disabling.
# Expects the including class to have a `uuid` attribute.
module ToggleDisabled
# Toggle the disabled state of the resource
# @param disabled [Boolean] Whether to disable the resource
# @param handle_as_user_edit [Boolean] If true, the change is treated as a user edit
# @return [self] the updated resource
def toggle_disabled!(disabled:, handle_as_user_edit: nil)
path = "#{resource_path.path}/#{uuid}/disabled_state"
path = self.class.build_query_path(path, handle_as_user_edit: handle_as_user_edit)
resp = handling_errors do
self.class.json_patch(path, { disabled: disabled })
end
json = ChartMogul::Utils::JSONParser.parse(resp.body, immutable_keys: self.class.immutable_keys)
assign_all_attributes(json)
self
end
end
end
end
10 changes: 10 additions & 0 deletions lib/chartmogul/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ class Invoice < APIResource
writeable_attr :due_date, type: :time

include API::Actions::Retrieve
include API::Actions::Update
include API::Actions::Destroy
include Concerns::ToggleDisabled
include Concerns::ExternalIdOperations

# Update the status of an invoice by external_id
def self.update_status!(data_source_uuid:, invoice_external_id:, status:)
path = "/v1/data_sources/#{CGI.escape(data_source_uuid)}/invoices/#{CGI.escape(invoice_external_id)}/status"
handling_errors { json_patch(path, { status: status }) }
true
end

def serialize_line_items
line_items.map(&:serialize_for_write)
Expand Down
45 changes: 45 additions & 0 deletions lib/chartmogul/json_import.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module ChartMogul
# JsonImport resource for bulk importing data via JSON.
# Use this to efficiently upload large datasets to a data source.
class JsonImport < APIResource
set_resource_name 'JsonImport'
set_resource_path '/v1/data_sources/:data_source_uuid/json_imports'

readonly_attr :id
readonly_attr :external_id
readonly_attr :status
readonly_attr :created_at
readonly_attr :updated_at
readonly_attr :error_count
readonly_attr :processed_count

# Create a new JSON import batch for a data source
# @param data_source_uuid [String] The UUID of the data source
# @param data [Hash] The import data (customers, plans, invoices, etc.)
def self.create!(data_source_uuid:, **data)
path = "/v1/data_sources/#{data_source_uuid}/json_imports"
resp = handling_errors do
connection.post(path) do |req|
req.headers['Content-Type'] = 'application/json'
req.body = JSON.dump(data)
end
end
json = ChartMogul::Utils::JSONParser.parse(resp.body, immutable_keys:)
new_from_json(json)
end
Comment thread
wscourge marked this conversation as resolved.

# Retrieve the status of a JSON import
# @param data_source_uuid [String] The UUID of the data source
# @param id [String, Integer] The ID of the import
def self.retrieve(data_source_uuid:, id:)
path = "/v1/data_sources/#{data_source_uuid}/json_imports/#{id}"
resp = handling_errors do
connection.get(path)
end
json = ChartMogul::Utils::JSONParser.parse(resp.body, immutable_keys:)
new_from_json(json)
end
end
end
60 changes: 60 additions & 0 deletions lib/chartmogul/line_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

module ChartMogul
# Standalone LineItem resource for CRUD operations on existing line items.
# Note: For creating line items on invoices, use LineItems::Subscription or LineItems::OneTime.
class LineItem < APIResource
set_resource_name 'LineItem'
set_resource_path '/v1/line_items'

readonly_attr :uuid
readonly_attr :disabled
readonly_attr :disabled_at
readonly_attr :disabled_by
readonly_attr :invoice_uuid
readonly_attr :subscription_uuid
readonly_attr :edit_history_summary
readonly_attr :errors

writeable_attr :type
writeable_attr :subscription_external_id
writeable_attr :subscription_set_external_id
writeable_attr :plan_uuid
writeable_attr :service_period_start, type: :time
writeable_attr :service_period_end, type: :time
writeable_attr :amount_in_cents
writeable_attr :quantity
writeable_attr :discount_amount_in_cents
writeable_attr :discount_code
writeable_attr :discount_description
writeable_attr :tax_amount_in_cents
writeable_attr :transaction_fees_in_cents
writeable_attr :transaction_fees_currency
writeable_attr :external_id
writeable_attr :data_source_uuid
writeable_attr :prorated
writeable_attr :proration_type
writeable_attr :cancelled_at, type: :time
writeable_attr :description
writeable_attr :account_code
writeable_attr :event_order

include API::Actions::Retrieve
include API::Actions::Update
include API::Actions::Destroy
include Concerns::ToggleDisabled
include Concerns::ExternalIdOperations

# Create a line item for an invoice
# @param handle_as_user_edit [Boolean] If true, the change is treated as a user edit
def self.create!(invoice_uuid:, handle_as_user_edit: nil, **attributes)
path = build_query_path(
"/v1/import/invoices/#{invoice_uuid}/line_items",
handle_as_user_edit: handle_as_user_edit
)
resp = handling_errors { json_post(path, attributes) }
json = ChartMogul::Utils::JSONParser.parse(resp.body, immutable_keys:)
new_from_json(json)
end
end
end
Loading