paddle 2.10 → 3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +284 -70
- data/lib/paddle/classic/client.rb +1 -1
- data/lib/paddle/client.rb +31 -15
- data/lib/paddle/collection.rb +47 -11
- data/lib/paddle/configuration.rb +66 -3
- data/lib/paddle/error_generator.rb +4 -5
- data/lib/paddle/models/adjustment.rb +6 -6
- data/lib/paddle/models/customer.rb +8 -2
- data/lib/paddle/models/explore_entity.rb +4 -0
- data/lib/paddle/models/explore_result.rb +50 -0
- data/lib/paddle/models/metric.rb +55 -0
- data/lib/paddle/models/notification.rb +6 -5
- data/lib/paddle/models/report.rb +1 -3
- data/lib/paddle/models/simulation.rb +2 -2
- data/lib/paddle/models/simulation_run.rb +2 -2
- data/lib/paddle/models/subscription.rb +12 -0
- data/lib/paddle/models/subscription_history.rb +4 -0
- data/lib/paddle/models/transaction.rb +9 -4
- data/lib/paddle/object.rb +91 -16
- data/lib/paddle/version.rb +1 -1
- data/lib/paddle/webhook.rb +65 -0
- data/lib/paddle.rb +27 -1
- metadata +20 -48
- data/.env.example +0 -3
- data/.rubocop.yml +0 -11
- data/Gemfile +0 -13
- data/Gemfile.lock +0 -110
- data/Rakefile +0 -12
- data/bin/console +0 -25
- data/bin/setup +0 -8
- data/mise.toml +0 -2
- data/paddle.gemspec +0 -32
data/lib/paddle/client.rb
CHANGED
|
@@ -2,43 +2,60 @@ require "faraday"
|
|
|
2
2
|
|
|
3
3
|
module Paddle
|
|
4
4
|
class Client
|
|
5
|
+
@connections = {}
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
|
|
5
8
|
class << self
|
|
9
|
+
# One connection is kept per base URL and set of connection options. The API key and
|
|
10
|
+
# version are sent with each request, so config changes take effect straight away
|
|
6
11
|
def connection
|
|
7
|
-
|
|
12
|
+
config = Paddle.config
|
|
13
|
+
key = [ config.url, config.connection_options.hash ]
|
|
14
|
+
|
|
15
|
+
@mutex.synchronize do
|
|
16
|
+
@connections[key] ||= create_connection(config)
|
|
17
|
+
end
|
|
8
18
|
end
|
|
9
19
|
|
|
10
20
|
def get_request(url, params: {}, headers: {})
|
|
11
|
-
|
|
21
|
+
# Paddle expects arrays as comma-separated lists, e.g. status=active,past_due
|
|
22
|
+
params = params.transform_values { |value| value.is_a?(Array) ? value.join(",") : value }
|
|
23
|
+
|
|
24
|
+
# skip_count is sent as a header rather than a query param
|
|
25
|
+
headers = headers.merge("Skip-Count" => "true") if params.delete(:skip_count)
|
|
26
|
+
|
|
27
|
+
handle_response(connection.get(url, params, request_headers(headers)))
|
|
12
28
|
end
|
|
13
29
|
|
|
14
30
|
def post_request(url, body: {}, headers: {})
|
|
15
|
-
handle_response(connection.post(url, body, headers))
|
|
31
|
+
handle_response(connection.post(url, body, request_headers(headers)))
|
|
16
32
|
end
|
|
17
33
|
|
|
18
34
|
def patch_request(url, body:, headers: {})
|
|
19
|
-
handle_response(connection.patch(url, body, headers))
|
|
35
|
+
handle_response(connection.patch(url, body, request_headers(headers)))
|
|
20
36
|
end
|
|
21
37
|
|
|
22
38
|
def delete_request(url, headers: {})
|
|
23
|
-
handle_response(connection.delete(url, headers))
|
|
39
|
+
handle_response(connection.delete(url, nil, request_headers(headers)))
|
|
24
40
|
end
|
|
25
41
|
|
|
26
42
|
private
|
|
27
43
|
|
|
28
|
-
def create_connection
|
|
29
|
-
Faraday.new(
|
|
30
|
-
conn.
|
|
31
|
-
conn.headers = default_headers
|
|
44
|
+
def create_connection(config)
|
|
45
|
+
Faraday.new(config.url, config.connection_options) do |conn|
|
|
46
|
+
conn.headers = { "User-Agent" => "paddle/v#{VERSION} (github.com/d34ndev/paddle)" }
|
|
32
47
|
conn.request :json
|
|
33
48
|
conn.response :json
|
|
34
49
|
end
|
|
35
50
|
end
|
|
36
51
|
|
|
37
|
-
def
|
|
52
|
+
def request_headers(headers)
|
|
53
|
+
config = Paddle.config
|
|
54
|
+
|
|
38
55
|
{
|
|
39
|
-
"
|
|
40
|
-
"Paddle-Version" =>
|
|
41
|
-
}
|
|
56
|
+
"Authorization" => "Bearer #{config.api_key}",
|
|
57
|
+
"Paddle-Version" => config.version.to_s
|
|
58
|
+
}.merge(headers)
|
|
42
59
|
end
|
|
43
60
|
|
|
44
61
|
def handle_response(response)
|
|
@@ -49,8 +66,7 @@ module Paddle
|
|
|
49
66
|
end
|
|
50
67
|
|
|
51
68
|
def error?(response)
|
|
52
|
-
|
|
53
|
-
response.body&.key?("error")
|
|
69
|
+
!response.success? || (response.body.is_a?(Hash) && response.body.key?("error"))
|
|
54
70
|
end
|
|
55
71
|
|
|
56
72
|
def raise_error(response)
|
data/lib/paddle/collection.rb
CHANGED
|
@@ -2,28 +2,64 @@ module Paddle
|
|
|
2
2
|
class Collection
|
|
3
3
|
include Enumerable
|
|
4
4
|
|
|
5
|
-
attr_reader :data, :total
|
|
5
|
+
attr_reader :data, :total, :per_page, :next_url
|
|
6
6
|
|
|
7
7
|
def self.from_response(response, type:)
|
|
8
8
|
body = response.body
|
|
9
9
|
|
|
10
|
-
data
|
|
10
|
+
data = body["data"].map { |attrs| type.new(attrs) }
|
|
11
|
+
pagination = body.dig("meta", "pagination")
|
|
11
12
|
|
|
12
|
-
if
|
|
13
|
-
|
|
13
|
+
if pagination
|
|
14
|
+
new(
|
|
15
|
+
data: data,
|
|
16
|
+
total: pagination["estimated_total"],
|
|
17
|
+
per_page: pagination["per_page"],
|
|
18
|
+
has_more: pagination["has_more"],
|
|
19
|
+
next_url: pagination["next"],
|
|
20
|
+
type: type,
|
|
21
|
+
skip_count: response.env.request_headers["Skip-Count"] == "true"
|
|
22
|
+
)
|
|
14
23
|
else
|
|
15
|
-
total
|
|
24
|
+
new(data: data, total: data.count)
|
|
16
25
|
end
|
|
17
|
-
|
|
18
|
-
new(
|
|
19
|
-
data: data,
|
|
20
|
-
total: total
|
|
21
|
-
)
|
|
22
26
|
end
|
|
23
27
|
|
|
24
|
-
def initialize(data:, total:)
|
|
28
|
+
def initialize(data:, total:, per_page: nil, has_more: false, next_url: nil, type: nil, skip_count: false)
|
|
25
29
|
@data = data
|
|
26
30
|
@total = total
|
|
31
|
+
@per_page = per_page
|
|
32
|
+
@has_more = has_more
|
|
33
|
+
@next_url = next_url
|
|
34
|
+
@type = type
|
|
35
|
+
@skip_count = skip_count
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def has_more?
|
|
39
|
+
@has_more == true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def skip_count?
|
|
43
|
+
@skip_count
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Paddle returns a next URL even on the last page, so has_more is checked first
|
|
47
|
+
def next_page
|
|
48
|
+
return unless has_more? && next_url && @type
|
|
49
|
+
|
|
50
|
+
# Only the path and query are used, so requests always go to the configured API host
|
|
51
|
+
response = Client.get_request(URI(next_url).request_uri.delete_prefix("/"), params: { skip_count: skip_count? })
|
|
52
|
+
Collection.from_response(response, type: @type)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def auto_paging_each(&block)
|
|
56
|
+
return enum_for(:auto_paging_each) unless block_given?
|
|
57
|
+
|
|
58
|
+
page = self
|
|
59
|
+
while page
|
|
60
|
+
page.each(&block)
|
|
61
|
+
page = page.next_page
|
|
62
|
+
end
|
|
27
63
|
end
|
|
28
64
|
|
|
29
65
|
def each(&block)
|
data/lib/paddle/configuration.rb
CHANGED
|
@@ -3,23 +3,67 @@
|
|
|
3
3
|
module Paddle
|
|
4
4
|
class Configuration
|
|
5
5
|
attr_reader :environment
|
|
6
|
+
attr_reader :api_key
|
|
6
7
|
|
|
7
8
|
attr_accessor :version
|
|
8
|
-
attr_accessor :api_key
|
|
9
9
|
attr_accessor :connection_options
|
|
10
10
|
|
|
11
11
|
def initialize
|
|
12
|
-
@environment
|
|
12
|
+
@environment = :production
|
|
13
|
+
@environment_set = false
|
|
13
14
|
@version ||= 1
|
|
14
15
|
@connection_options = {}
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
# When the environment isn't set, it's detected from the API key
|
|
17
19
|
def environment=(env)
|
|
18
|
-
|
|
20
|
+
if env.nil?
|
|
21
|
+
@environment_set = false
|
|
22
|
+
@environment = key_environment || :production
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
env = env.to_sym
|
|
19
27
|
unless [ :development, :sandbox, :production ].include?(env)
|
|
20
28
|
raise ArgumentError, "#{env.inspect} is not a valid environment"
|
|
21
29
|
end
|
|
30
|
+
|
|
31
|
+
check_key_matches!(env)
|
|
22
32
|
@environment = env
|
|
33
|
+
@environment_set = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def api_key=(key)
|
|
37
|
+
@api_key = key
|
|
38
|
+
|
|
39
|
+
if @environment_set
|
|
40
|
+
check_key_matches!(@environment)
|
|
41
|
+
else
|
|
42
|
+
@environment = key_environment || :production
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
MERGEABLE = [ :api_key, :environment, :version, :connection_options ].freeze
|
|
47
|
+
|
|
48
|
+
# Returns a new Configuration with the given options applied over this one. When a new
|
|
49
|
+
# API key is given without an environment, the environment is detected from the key,
|
|
50
|
+
# or kept from this config for older keys without a prefix
|
|
51
|
+
def merge(**options)
|
|
52
|
+
unknown = options.keys - MERGEABLE
|
|
53
|
+
raise ArgumentError, "Unknown config options: #{unknown.join(", ")}" if unknown.any?
|
|
54
|
+
|
|
55
|
+
config = Configuration.new
|
|
56
|
+
config.version = options.fetch(:version, version)
|
|
57
|
+
config.connection_options = options.fetch(:connection_options, connection_options)
|
|
58
|
+
config.api_key = options.fetch(:api_key, api_key)
|
|
59
|
+
|
|
60
|
+
if options.key?(:environment)
|
|
61
|
+
config.environment = options[:environment]
|
|
62
|
+
elsif !options.key?(:api_key) || config.key_environment.nil?
|
|
63
|
+
config.environment = environment
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
config
|
|
23
67
|
end
|
|
24
68
|
|
|
25
69
|
def url
|
|
@@ -30,5 +74,24 @@ module Paddle
|
|
|
30
74
|
"https://sandbox-api.paddle.com"
|
|
31
75
|
end
|
|
32
76
|
end
|
|
77
|
+
|
|
78
|
+
protected
|
|
79
|
+
|
|
80
|
+
# API keys created since May 2025 start with pdl_live_ or pdl_sdbx_. Older keys have no prefix
|
|
81
|
+
def key_environment
|
|
82
|
+
case @api_key
|
|
83
|
+
when /\Apdl_live_/ then :production
|
|
84
|
+
when /\Apdl_sdbx_/ then :sandbox
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def check_key_matches!(env)
|
|
91
|
+
key_env = key_environment
|
|
92
|
+
return if key_env.nil? || (key_env == :production) == (env == :production)
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, "The API key is for the #{key_env} environment, but environment is set to #{env.inspect}"
|
|
95
|
+
end
|
|
33
96
|
end
|
|
34
97
|
end
|
|
@@ -8,7 +8,8 @@ module Paddle
|
|
|
8
8
|
attr_reader :request_id
|
|
9
9
|
|
|
10
10
|
def initialize(response_body, http_status_code)
|
|
11
|
-
|
|
11
|
+
# Non-JSON responses (e.g. an HTML page from a gateway) have no Paddle error details
|
|
12
|
+
@response_body = response_body.is_a?(Hash) ? response_body : {}
|
|
12
13
|
@http_status_code = http_status_code
|
|
13
14
|
set_paddle_error_values
|
|
14
15
|
super(build_message)
|
|
@@ -25,9 +26,7 @@ module Paddle
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def error_message
|
|
28
|
-
@paddle_error_message || @
|
|
29
|
-
rescue NoMethodError
|
|
30
|
-
"An unknown error occurred."
|
|
29
|
+
@paddle_error_message || @paddle_error_code || "An unknown error occurred."
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def build_message
|
|
@@ -121,7 +120,7 @@ module Paddle
|
|
|
121
120
|
private
|
|
122
121
|
|
|
123
122
|
def error_message
|
|
124
|
-
"
|
|
123
|
+
"The Paddle API is temporarily unavailable. Try again later."
|
|
125
124
|
end
|
|
126
125
|
end
|
|
127
126
|
|
|
@@ -6,17 +6,17 @@ module Paddle
|
|
|
6
6
|
Collection.from_response(response, type: Adjustment)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# items can be omitted when type is "full"
|
|
10
|
+
def create(transaction_id:, action:, reason:, items: nil, **params)
|
|
11
|
+
attrs = { transaction_id: transaction_id, action: action, reason: reason }
|
|
12
|
+
attrs[:items] = items if items
|
|
11
13
|
response = Client.post_request("adjustments", body: attrs.merge(params))
|
|
12
14
|
Adjustment.new(response.body["data"])
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def credit_note(id:, disposition: "attachment")
|
|
16
|
-
response = Client.get_request("adjustments/#{id}/credit-note
|
|
17
|
-
|
|
18
|
-
response.body["data"]["url"]
|
|
19
|
-
end
|
|
18
|
+
response = Client.get_request("adjustments/#{id}/credit-note", params: { disposition: disposition })
|
|
19
|
+
response.body["data"]["url"]
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -22,9 +22,15 @@ module Paddle
|
|
|
22
22
|
Customer.new(response.body["data"])
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def credit_balances(id:, **params)
|
|
26
|
+
response = Client.get_request("customers/#{id}/credit-balances", params: params)
|
|
27
|
+
Collection.from_response(response, type: CreditBalance)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns only the first credit balance. Customers have a balance per currency,
|
|
31
|
+
# so use credit_balances to get all of them.
|
|
25
32
|
def credit(id:)
|
|
26
|
-
|
|
27
|
-
CreditBalance.new(response.body["data"][0])
|
|
33
|
+
credit_balances(id: id).first
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
def auth_token(id:)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Paddle
|
|
2
|
+
# The result of an Explore query. Pages are made up of series (one per combination of dimension
|
|
3
|
+
# values), and the next page is requested by POSTing the same query to the next URL
|
|
4
|
+
class ExploreResult < Object
|
|
5
|
+
def self.from_response(response, query:)
|
|
6
|
+
new(response.body["data"], query: query, pagination: response.body.dig("meta", "pagination"))
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(attributes, query: {}, pagination: nil)
|
|
10
|
+
super(attributes)
|
|
11
|
+
@query = query
|
|
12
|
+
@pagination = pagination || {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def per_page
|
|
16
|
+
@pagination["per_page"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def total
|
|
20
|
+
@pagination["estimated_total"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def next_url
|
|
24
|
+
@pagination["next"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def has_more?
|
|
28
|
+
@pagination["has_more"] == true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def next_page
|
|
32
|
+
return unless has_more? && next_url
|
|
33
|
+
|
|
34
|
+
# Only the path and query are used, so requests always go to the configured API host
|
|
35
|
+
response = Client.post_request(URI(next_url).request_uri.delete_prefix("/"), body: @query)
|
|
36
|
+
ExploreResult.from_response(response, query: @query)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Yields each series across all pages
|
|
40
|
+
def auto_paging_each(&block)
|
|
41
|
+
return enum_for(:auto_paging_each) unless block_given?
|
|
42
|
+
|
|
43
|
+
page = self
|
|
44
|
+
while page
|
|
45
|
+
page.series.each(&block)
|
|
46
|
+
page = page.next_page
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Paddle
|
|
2
|
+
class Metric < Object
|
|
3
|
+
class << self
|
|
4
|
+
# from and to are dates, e.g. "2025-09-01" or a Date. Returns daily data with a timeseries
|
|
5
|
+
def monthly_recurring_revenue(from:, to:)
|
|
6
|
+
get("monthly-recurring-revenue", from: from, to: to)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def monthly_recurring_revenue_change(from:, to:)
|
|
10
|
+
get("monthly-recurring-revenue-change", from: from, to: to)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def active_subscribers(from:, to:)
|
|
14
|
+
get("active-subscribers", from: from, to: to)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def revenue(from:, to:)
|
|
18
|
+
get("revenue", from: from, to: to)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def refunds(from:, to:)
|
|
22
|
+
get("refunds", from: from, to: to)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def chargebacks(from:, to:)
|
|
26
|
+
get("chargebacks", from: from, to: to)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def checkout_conversion(from:, to:)
|
|
30
|
+
get("checkout-conversion", from: from, to: to)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Lists the entities that can be queried with explore, with their dimensions and measures
|
|
34
|
+
def explore_entities(**params)
|
|
35
|
+
response = Client.get_request("metrics/explore/entities", params: params)
|
|
36
|
+
Collection.from_response(response, type: ExploreEntity)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Runs an Explore query. from is inclusive and to is exclusive.
|
|
40
|
+
# measures is an array of hashes, e.g. [ { field: "gross_revenue", agg: "sum" } ]
|
|
41
|
+
def explore(entity:, from:, to:, measures:, **params)
|
|
42
|
+
query = { entity: entity, from: from, to: to, measures: measures }.merge(params)
|
|
43
|
+
response = Client.post_request("metrics/explore", body: query)
|
|
44
|
+
ExploreResult.from_response(response, query: query)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def get(metric, from:, to:)
|
|
50
|
+
response = Client.get_request("metrics/#{metric}", params: { from: from, to: to })
|
|
51
|
+
Metric.new(response.body["data"])
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -11,11 +11,12 @@ module Paddle
|
|
|
11
11
|
Notification.new(response.body["data"])
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
# Only delivered or failed notifications with an origin of "event" can be replayed.
|
|
15
|
+
# Returns a Notification with the notification_id of the new notification.
|
|
16
|
+
def replay(id:)
|
|
17
|
+
response = Client.post_request("notifications/#{id}/replay")
|
|
18
|
+
Notification.new(response.body["data"])
|
|
19
|
+
end
|
|
19
20
|
|
|
20
21
|
def logs(id:, **params)
|
|
21
22
|
response = Client.get_request("notifications/#{id}/logs", params: params)
|
data/lib/paddle/models/report.rb
CHANGED
|
@@ -22,8 +22,8 @@ module Paddle
|
|
|
22
22
|
Simulation.new(response.body["data"])
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def runs(id
|
|
26
|
-
response = Client.get_request("simulations/#{id}/runs")
|
|
25
|
+
def runs(id:, **params)
|
|
26
|
+
response = Client.get_request("simulations/#{id}/runs", params: params)
|
|
27
27
|
Collection.from_response(response, type: SimulationRun)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -11,8 +11,8 @@ module Paddle
|
|
|
11
11
|
SimulationRun.new(response.body["data"])
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def events(simulation_id:, id
|
|
15
|
-
response = Client.get_request("simulations/#{simulation_id}/runs/#{id}/events")
|
|
14
|
+
def events(simulation_id:, id:, **params)
|
|
15
|
+
response = Client.get_request("simulations/#{simulation_id}/runs/#{id}/events", params: params)
|
|
16
16
|
Collection.from_response(response, type: SimulationRunEvent)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -12,6 +12,11 @@ module Paddle
|
|
|
12
12
|
Subscription.new(response.body["data"])
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def history(id:, **params)
|
|
16
|
+
response = Client.get_request("subscriptions/#{id}/history", params: params)
|
|
17
|
+
Collection.from_response(response, type: SubscriptionHistory)
|
|
18
|
+
end
|
|
19
|
+
|
|
15
20
|
def get_transaction(id:)
|
|
16
21
|
response = Client.get_request("subscriptions/#{id}/update-payment-method-transaction")
|
|
17
22
|
Transaction.new(response.body["data"])
|
|
@@ -33,6 +38,13 @@ module Paddle
|
|
|
33
38
|
Subscription.new(response.body["data"])
|
|
34
39
|
end
|
|
35
40
|
|
|
41
|
+
# Previews a one-time charge without billing it. Takes the same params as charge
|
|
42
|
+
def charge_preview(id:, items:, effective_from:, **params)
|
|
43
|
+
attrs = { items: items, effective_from: effective_from }
|
|
44
|
+
response = Client.post_request("subscriptions/#{id}/charge/preview", body: attrs.merge(params))
|
|
45
|
+
Subscription.new(response.body["data"])
|
|
46
|
+
end
|
|
47
|
+
|
|
36
48
|
def pause(id:, **params)
|
|
37
49
|
response = Client.post_request("subscriptions/#{id}/pause", body: params)
|
|
38
50
|
Subscription.new(response.body["data"])
|
|
@@ -23,11 +23,16 @@ module Paddle
|
|
|
23
23
|
Transaction.new(response.body["data"])
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Revises customer, business and address details on a billed or completed transaction.
|
|
27
|
+
# A transaction can only be revised once
|
|
28
|
+
def revise(id:, **params)
|
|
29
|
+
response = Client.post_request("transactions/#{id}/revise", body: params)
|
|
30
|
+
Transaction.new(response.body["data"])
|
|
31
|
+
end
|
|
32
|
+
|
|
26
33
|
def invoice(id:, disposition: "attachment")
|
|
27
|
-
response = Client.get_request("transactions/#{id}/invoice
|
|
28
|
-
|
|
29
|
-
response.body["data"]["url"]
|
|
30
|
-
end
|
|
34
|
+
response = Client.get_request("transactions/#{id}/invoice", params: { disposition: disposition })
|
|
35
|
+
response.body["data"]["url"]
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def preview(items:, **params)
|
data/lib/paddle/object.rb
CHANGED
|
@@ -1,23 +1,66 @@
|
|
|
1
|
-
require "ostruct"
|
|
2
|
-
|
|
3
1
|
module Paddle
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
# A lightweight wrapper around API response data. Attributes can be read with dot notation
|
|
3
|
+
# (object.id) or like a hash (object[:id] or object["id"]). Nested hashes are wrapped too.
|
|
4
|
+
class Object
|
|
5
|
+
def initialize(attributes = {})
|
|
6
|
+
@attributes = {}
|
|
7
|
+
(attributes || {}).each { |key, val| self[key] = val }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def [](key)
|
|
11
|
+
@attributes[key.to_sym]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def []=(key, val)
|
|
15
|
+
@attributes[key.to_sym] = wrap(val)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def key?(key)
|
|
19
|
+
@attributes.key?(key.to_sym)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dig(key, *rest)
|
|
23
|
+
val = self[key]
|
|
24
|
+
rest.empty? || val.nil? ? val : val.dig(*rest)
|
|
17
25
|
end
|
|
18
26
|
|
|
27
|
+
def each_pair(&block)
|
|
28
|
+
return enum_for(:each_pair) unless block_given?
|
|
29
|
+
|
|
30
|
+
@attributes.each_pair(&block)
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the attributes as a hash, with nested objects converted to hashes too
|
|
35
|
+
def to_h
|
|
36
|
+
@attributes.transform_values { |val| unwrap(val) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def as_json(*)
|
|
40
|
+
to_h
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_json(*args)
|
|
44
|
+
to_h.to_json(*args)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ==(other)
|
|
48
|
+
other.is_a?(Paddle::Object) && to_h == other.to_h
|
|
49
|
+
end
|
|
50
|
+
alias eql? ==
|
|
51
|
+
|
|
52
|
+
def hash
|
|
53
|
+
to_h.hash
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def inspect
|
|
57
|
+
attrs = @attributes.map { |key, val| "#{key}=#{val.inspect}" }.join(", ")
|
|
58
|
+
"#<#{self.class.name}#{" " unless attrs.empty?}#{attrs}>"
|
|
59
|
+
end
|
|
60
|
+
alias to_s inspect
|
|
61
|
+
|
|
19
62
|
def update(**params)
|
|
20
|
-
|
|
63
|
+
raise NoMethodError, "undefined method 'update' for #{klass}" unless klass.respond_to? :update
|
|
21
64
|
|
|
22
65
|
primary_attributes = klass.method(:update).parameters.select { |(type, name)| type == :keyreq }.map(&:last) # Identified by whatever is a required named parameter.
|
|
23
66
|
|
|
@@ -36,5 +79,37 @@ module Paddle
|
|
|
36
79
|
def klass
|
|
37
80
|
self.class
|
|
38
81
|
end
|
|
82
|
+
|
|
83
|
+
# Unknown attributes return nil, like a hash
|
|
84
|
+
def method_missing(name, *args)
|
|
85
|
+
if name.end_with?("=") && args.size == 1
|
|
86
|
+
self[name.to_s.chomp("=")] = args.first
|
|
87
|
+
elsif args.empty? && !name.end_with?("=", "?", "!")
|
|
88
|
+
self[name]
|
|
89
|
+
else
|
|
90
|
+
super
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def respond_to_missing?(name, include_private = false)
|
|
95
|
+
key?(name.to_s.chomp("=")) || super
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def wrap(val)
|
|
99
|
+
case val
|
|
100
|
+
when Paddle::Object then val
|
|
101
|
+
when Hash then Paddle::Object.new(val)
|
|
102
|
+
when Array then val.map { |v| wrap(v) }
|
|
103
|
+
else val
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def unwrap(val)
|
|
108
|
+
case val
|
|
109
|
+
when Paddle::Object then val.to_h
|
|
110
|
+
when Array then val.map { |v| unwrap(v) }
|
|
111
|
+
else val
|
|
112
|
+
end
|
|
113
|
+
end
|
|
39
114
|
end
|
|
40
115
|
end
|
data/lib/paddle/version.rb
CHANGED