payment_kit 1.0.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 +7 -0
- data/CHANGELOG.md +102 -0
- data/LICENSE.txt +21 -0
- data/README.md +1008 -0
- data/Rakefile +41 -0
- data/app/controllers/payment_kit/webhook_controller.rb +45 -0
- data/config/routes.rb +5 -0
- data/lib/payment_kit/client.rb +449 -0
- data/lib/payment_kit/configuration.rb +117 -0
- data/lib/payment_kit/engine.rb +10 -0
- data/lib/payment_kit/errors.rb +103 -0
- data/lib/payment_kit/instrumentation.rb +109 -0
- data/lib/payment_kit/namespace.rb +27 -0
- data/lib/payment_kit/notification_adapter.rb +21 -0
- data/lib/payment_kit/resources/catalog.rb +36 -0
- data/lib/payment_kit/resources/customers.rb +62 -0
- data/lib/payment_kit/resources/invoices.rb +64 -0
- data/lib/payment_kit/resources/payments.rb +71 -0
- data/lib/payment_kit/resources/subscriptions.rb +129 -0
- data/lib/payment_kit/version.rb +6 -0
- data/lib/payment_kit/webhook.rb +64 -0
- data/lib/payment_kit.rb +204 -0
- metadata +90 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module PaymentKit
|
|
6
|
+
# Base error for all PaymentKit failures.
|
|
7
|
+
#
|
|
8
|
+
# PaymentKit returns RFC 7807 problem documents. Standard members (+title+,
|
|
9
|
+
# +detail+, +status+, +instance+, +request_id+) plus any extension members are
|
|
10
|
+
# kept in +problem+ so callers can branch on them without re-parsing the body.
|
|
11
|
+
#
|
|
12
|
+
# Transient failures carry a top-level +error_code+ and +retryable: true+
|
|
13
|
+
# (for example +invoice_locked+ on HTTP 409).
|
|
14
|
+
class Error < StandardError
|
|
15
|
+
# HTTP status of the failing response, or +nil+ for client-side failures.
|
|
16
|
+
attr_reader :status
|
|
17
|
+
|
|
18
|
+
# Raw response body as received.
|
|
19
|
+
attr_reader :body
|
|
20
|
+
|
|
21
|
+
# PaymentKit request id, from the problem document or the +Request-Id+ header.
|
|
22
|
+
attr_reader :request_id
|
|
23
|
+
|
|
24
|
+
# Machine-readable code such as +invoice_locked+, when PaymentKit sends one.
|
|
25
|
+
attr_reader :error_code
|
|
26
|
+
|
|
27
|
+
# Parsed RFC 7807 problem document as a Hash; empty when the body was not JSON.
|
|
28
|
+
attr_reader :problem
|
|
29
|
+
|
|
30
|
+
# Any attribute not passed explicitly is read from the RFC 7807 +body+.
|
|
31
|
+
def initialize(message = nil, status: nil, body: nil, request_id: nil,
|
|
32
|
+
error_code: nil, retryable: nil)
|
|
33
|
+
@status = status
|
|
34
|
+
@body = body
|
|
35
|
+
@problem = parse_problem(body)
|
|
36
|
+
@request_id = request_id || @problem["request_id"]
|
|
37
|
+
@error_code = error_code || @problem["error_code"]
|
|
38
|
+
@retryable = retryable.nil? ? @problem["retryable"] : retryable
|
|
39
|
+
super(message)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Whether PaymentKit marked this failure as safe to retry unchanged.
|
|
43
|
+
def retryable?
|
|
44
|
+
@retryable == true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Reads an RFC 7807 extension member, e.g. +error["invoice_id"]+.
|
|
48
|
+
def [](key)
|
|
49
|
+
@problem[key.to_s]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Convenience reader for the +invoice_id+ extension member, when present.
|
|
53
|
+
def invoice_id
|
|
54
|
+
self["invoice_id"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Convenience reader for the +subscription_id+ extension member, when present.
|
|
58
|
+
def subscription_id
|
|
59
|
+
self["subscription_id"]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def parse_problem(raw)
|
|
65
|
+
return {} if raw.nil? || raw.to_s.empty?
|
|
66
|
+
|
|
67
|
+
parsed = JSON.parse(raw.to_s)
|
|
68
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
69
|
+
rescue JSON::ParserError
|
|
70
|
+
{}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Raised when credentials are missing/invalid or the API returns 401.
|
|
75
|
+
class AuthenticationError < Error; end
|
|
76
|
+
|
|
77
|
+
# Raised on HTTP 403: the credential is valid but is not allowed to touch this
|
|
78
|
+
# resource. Subclasses AuthenticationError so existing rescues still match.
|
|
79
|
+
class PermissionError < AuthenticationError; end
|
|
80
|
+
|
|
81
|
+
# Raised when an inbound webhook signature is missing or does not verify.
|
|
82
|
+
# Subclasses AuthenticationError so existing rescues still match.
|
|
83
|
+
class SignatureVerificationError < AuthenticationError; end
|
|
84
|
+
|
|
85
|
+
# Raised for client-side invalid parameters or API 400/404/422 responses.
|
|
86
|
+
class InvalidRequestError < Error; end
|
|
87
|
+
|
|
88
|
+
# Raised when a request conflicts with the current resource state (HTTP 409).
|
|
89
|
+
class ConflictError < Error; end
|
|
90
|
+
|
|
91
|
+
# Raised when a payment attempt is declined (HTTP 402). The invoice or intent
|
|
92
|
+
# is left in a retry-safe state, so this is a decline rather than a bug.
|
|
93
|
+
class CardError < Error; end
|
|
94
|
+
|
|
95
|
+
# Raised when the API rate-limits the client (HTTP 429).
|
|
96
|
+
class RateLimitError < Error; end
|
|
97
|
+
|
|
98
|
+
# Raised for unexpected API responses and other HTTP error statuses.
|
|
99
|
+
class APIError < Error; end
|
|
100
|
+
|
|
101
|
+
# Raised when the HTTP connection fails (timeouts, refused, reset, etc.).
|
|
102
|
+
class APIConnectionError < Error; end
|
|
103
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module PaymentKit
|
|
6
|
+
# Optional hooks around outbound HTTP requests.
|
|
7
|
+
#
|
|
8
|
+
# Deliberately separate from the webhook event bus: instrumenting API calls
|
|
9
|
+
# through +ActiveSupport::Notifications+ under the +payment_kit.+ namespace
|
|
10
|
+
# would deliver them to +PaymentKit.all+ webhook subscribers.
|
|
11
|
+
#
|
|
12
|
+
# PaymentKit::Instrumentation.subscribe(:request_end) do |event|
|
|
13
|
+
# StatsD.timing("payment_kit.request", event.duration * 1000,
|
|
14
|
+
# tags: ["path:#{event.path}", "status:#{event.status}"])
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Subscriber exceptions are never allowed to break the API call.
|
|
18
|
+
module Instrumentation
|
|
19
|
+
# Emitted before the request leaves the process.
|
|
20
|
+
class RequestBeginEvent
|
|
21
|
+
# HTTP verb as a Symbol, e.g. +:post+.
|
|
22
|
+
attr_reader :method
|
|
23
|
+
|
|
24
|
+
# Request path, relative to the account base URL.
|
|
25
|
+
attr_reader :path
|
|
26
|
+
|
|
27
|
+
def initialize(method:, path:) # :nodoc:
|
|
28
|
+
@method = method
|
|
29
|
+
@path = path
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Emitted once a request has finished, including after retries.
|
|
34
|
+
class RequestEvent
|
|
35
|
+
# HTTP verb as a Symbol, e.g. +:post+.
|
|
36
|
+
attr_reader :method
|
|
37
|
+
|
|
38
|
+
# Request path, relative to the account base URL.
|
|
39
|
+
attr_reader :path
|
|
40
|
+
|
|
41
|
+
# HTTP status of the final attempt, or +nil+ if no response arrived.
|
|
42
|
+
attr_reader :status
|
|
43
|
+
|
|
44
|
+
# Wall-clock seconds for the whole call, retries included.
|
|
45
|
+
attr_reader :duration
|
|
46
|
+
|
|
47
|
+
# Number of retries performed; +0+ when the first attempt settled it.
|
|
48
|
+
attr_reader :num_retries
|
|
49
|
+
|
|
50
|
+
# PaymentKit request id from the response headers, when present.
|
|
51
|
+
attr_reader :request_id
|
|
52
|
+
|
|
53
|
+
def initialize(method:, path:, status:, duration:, num_retries:, request_id: nil) # :nodoc:
|
|
54
|
+
@method = method
|
|
55
|
+
@path = path
|
|
56
|
+
@status = status
|
|
57
|
+
@duration = duration
|
|
58
|
+
@num_retries = num_retries
|
|
59
|
+
@request_id = request_id
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Topics that may be subscribed to.
|
|
64
|
+
TOPICS = %i[request_begin request_end].freeze
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
# Registered blocks, keyed by topic and then by subscriber name.
|
|
68
|
+
def subscribers
|
|
69
|
+
@subscribers ||= Hash.new { |hash, key| hash[key] = {} }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Registers +block+ for +topic+, which must be +:request_begin+ or
|
|
73
|
+
# +:request_end+. Returns the subscriber name, for use with #unsubscribe.
|
|
74
|
+
def subscribe(topic, name = SecureRandom.uuid, &block)
|
|
75
|
+
raise ArgumentError, "unknown topic: #{topic}" unless TOPICS.include?(topic)
|
|
76
|
+
raise ArgumentError, "subscriber block required" if block.nil?
|
|
77
|
+
|
|
78
|
+
subscribers[topic][name] = block
|
|
79
|
+
name
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Removes the subscriber registered under +name+ for +topic+.
|
|
83
|
+
def unsubscribe(topic, name)
|
|
84
|
+
subscribers[topic].delete(name)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Whether +topic+ has any subscribers. The client checks this before
|
|
88
|
+
# building event objects, so an unsubscribed topic costs nothing.
|
|
89
|
+
def subscribers?(topic)
|
|
90
|
+
!subscribers[topic].empty?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Notifies subscribers. A raising subscriber must never break the API call,
|
|
94
|
+
# so failures are swallowed after being reported to +$stderr+.
|
|
95
|
+
def notify(topic, event)
|
|
96
|
+
subscribers[topic].each_value do |subscriber|
|
|
97
|
+
subscriber.call(event)
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
warn("[PaymentKit::Instrumentation] #{topic} subscriber raised: #{e.message}")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Drops every subscriber. Intended for test suites.
|
|
104
|
+
def reset!
|
|
105
|
+
@subscribers = nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaymentKit
|
|
4
|
+
# Prefixes notification names and builds subscription regexes.
|
|
5
|
+
#
|
|
6
|
+
# Events instrument as +payment_kit.<type>+.
|
|
7
|
+
class Namespace
|
|
8
|
+
# +prefix+ is prepended to every event type, and should end with a dot.
|
|
9
|
+
def initialize(prefix)
|
|
10
|
+
@prefix = prefix.to_s
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# The configured prefix, e.g. <tt>"payment_kit."</tt>.
|
|
14
|
+
attr_reader :prefix
|
|
15
|
+
|
|
16
|
+
# Returns the fully namespaced notification name for an event type.
|
|
17
|
+
def call(name)
|
|
18
|
+
"#{@prefix}#{name}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# +nil+/empty name matches the entire namespace (used by +PaymentKit.all+).
|
|
22
|
+
def to_regexp(name = nil)
|
|
23
|
+
pattern = name.nil? || name.to_s.empty? ? @prefix : "#{@prefix}#{name}"
|
|
24
|
+
/\A#{Regexp.escape(pattern)}/
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaymentKit
|
|
4
|
+
# Adapts ActiveSupport::Notifications' multi-argument callback to +#call(event)+.
|
|
5
|
+
class NotificationAdapter
|
|
6
|
+
# Wraps +subscriber+ so the notification backend can invoke it.
|
|
7
|
+
def self.call(subscriber)
|
|
8
|
+
new(subscriber)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(subscriber) # :nodoc:
|
|
12
|
+
@subscriber = subscriber
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Receives the backend's callback arguments and forwards only the payload.
|
|
16
|
+
def call(*args)
|
|
17
|
+
event = args.last
|
|
18
|
+
@subscriber.call(event)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaymentKit
|
|
4
|
+
module Resources
|
|
5
|
+
# Products and prices.
|
|
6
|
+
module Catalog
|
|
7
|
+
# GET /products/ — auto-paginated Array of products.
|
|
8
|
+
def list_products(params = {}) = list("/products/", params)
|
|
9
|
+
|
|
10
|
+
# GET /products/{id}
|
|
11
|
+
def retrieve_product(id) = get("/products/#{id}")
|
|
12
|
+
|
|
13
|
+
# POST /products/ — takes +name+, +description+, +is_active+,
|
|
14
|
+
# +default_price_id+, +metadata+, +custom_fields+.
|
|
15
|
+
def create_product(params = nil, **) = post("/products/", params, **)
|
|
16
|
+
|
|
17
|
+
# PATCH /products/{id} — only the fields you send are changed.
|
|
18
|
+
def update_product(id, params = nil, **) = patch("/products/#{id}", params, **)
|
|
19
|
+
|
|
20
|
+
# GET /products/{id}/prices — auto-paginated Array of the product's prices.
|
|
21
|
+
def list_product_prices(product_id, params = {})
|
|
22
|
+
list("/products/#{product_id}/prices", params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# GET /prices/ — auto-paginated Array of prices.
|
|
26
|
+
def list_prices(params = {}) = list("/prices/", params)
|
|
27
|
+
|
|
28
|
+
# GET /prices/{id}
|
|
29
|
+
def retrieve_price(id, params = {}) = get("/prices/#{id}", params)
|
|
30
|
+
|
|
31
|
+
# POST /prices/ — takes +product_id+, +currency+, +unit_amount_atom+,
|
|
32
|
+
# +pricing_type+, +billing_scheme+, +recurring_interval+ and friends.
|
|
33
|
+
def create_price(params = nil, **) = post("/prices/", params, **)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaymentKit
|
|
4
|
+
# Endpoint methods, grouped by resource and mixed into PaymentKit::Client.
|
|
5
|
+
#
|
|
6
|
+
# Each module assumes the private request helpers the client provides, so the
|
|
7
|
+
# modules are not useful on their own.
|
|
8
|
+
module Resources
|
|
9
|
+
# Customers, credit balance and credit notes.
|
|
10
|
+
#
|
|
11
|
+
# Write methods accept the body positionally or as keywords, and take an
|
|
12
|
+
# optional +idempotency_key:+:
|
|
13
|
+
#
|
|
14
|
+
# client.create_customer(email: "a@b.com")
|
|
15
|
+
# client.create_customer({ email: "a@b.com" }, idempotency_key: "signup-42")
|
|
16
|
+
module Customers
|
|
17
|
+
# POST /customers/ — takes +email+, +first_name+, +last_name+,
|
|
18
|
+
# +business_name+, +address+, +currency+, +metadata+ and friends.
|
|
19
|
+
def create_customer(params = nil, **) = post("/customers/", params, **)
|
|
20
|
+
|
|
21
|
+
# GET /customers/{id}
|
|
22
|
+
def retrieve_customer(id) = get("/customers/#{id}")
|
|
23
|
+
|
|
24
|
+
# PUT /customers/{id} — only the fields you send are changed.
|
|
25
|
+
def update_customer(id, params = nil, **) = put("/customers/#{id}", params, **)
|
|
26
|
+
|
|
27
|
+
# GET /customers/ — auto-paginated Array of customers.
|
|
28
|
+
def list_customers(params = {}) = list("/customers/", params)
|
|
29
|
+
|
|
30
|
+
# Sets the customer's credit balance to an ABSOLUTE target for the given
|
|
31
|
+
# currency — PaymentKit issues or voids credit to reach it. This is not a
|
|
32
|
+
# delta; use #create_credit_note to add credit incrementally.
|
|
33
|
+
#
|
|
34
|
+
# client.set_credit_balance("cus_1", amount_atom: 5000, currency: "USD")
|
|
35
|
+
def set_credit_balance(customer_id, params = nil, **)
|
|
36
|
+
patch("/customers/#{customer_id}/credit-balance", params, **)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# *Deprecated.* Use #set_credit_balance. The name implied delta semantics,
|
|
40
|
+
# but the endpoint sets an absolute target balance.
|
|
41
|
+
def create_balance_transaction(customer_id, params = nil, **)
|
|
42
|
+
warn("[PaymentKit] create_balance_transaction is deprecated; use set_credit_balance. " \
|
|
43
|
+
"The endpoint sets an absolute target balance, not a delta.")
|
|
44
|
+
set_credit_balance(customer_id, params, **)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Appends a credit note (an additive adjustment) to the customer balance.
|
|
48
|
+
# Always pass a stable +idempotency_key+: a double submit issues two notes.
|
|
49
|
+
#
|
|
50
|
+
# +reason+ is required and must be one of +proration_excess+,
|
|
51
|
+
# +manual_adjustment+, +auto_apply+ or +debit_settlement+.
|
|
52
|
+
def create_credit_note(customer_id, params = nil, **)
|
|
53
|
+
post("/customers/#{customer_id}/credit-notes", params, **)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# GET /customers/{id}/credit-notes — newest first; filter with +currency+.
|
|
57
|
+
def list_credit_notes(customer_id, params = {})
|
|
58
|
+
list("/customers/#{customer_id}/credit-notes", params)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaymentKit
|
|
4
|
+
module Resources
|
|
5
|
+
# Invoices and floating (pending) invoice items.
|
|
6
|
+
module Invoices
|
|
7
|
+
# POST /invoices/ — creates a one-off invoice in +draft+. Each entry in
|
|
8
|
+
# +items+ takes an +amount+, a +unit_amount+ with +quantity+, or a
|
|
9
|
+
# catalog +price_id+.
|
|
10
|
+
def create_invoice(params = nil, **) = post("/invoices/", params, **)
|
|
11
|
+
|
|
12
|
+
# Supports +expand+, e.g. <tt>retrieve_invoice("in_1", expand: "custom_fields")</tt>.
|
|
13
|
+
def retrieve_invoice(id, params = {}) = get("/invoices/#{id}", params)
|
|
14
|
+
|
|
15
|
+
# GET /invoices/ — auto-paginated Array of invoices.
|
|
16
|
+
def list_invoices(params = {}) = list("/invoices/", params)
|
|
17
|
+
|
|
18
|
+
# Returns <tt>{ "pdf_url" => ..., "status" => "available" | "generating" }</tt>;
|
|
19
|
+
# poll again while +status+ is +generating+.
|
|
20
|
+
def retrieve_invoice_pdf(id) = get("/invoices/#{id}/pdf")
|
|
21
|
+
|
|
22
|
+
# Attempts collection on an open invoice (finalizing it first if draft).
|
|
23
|
+
# A decline raises PaymentKit::CardError and leaves the invoice payable.
|
|
24
|
+
def pay_invoice(id, params = nil, **) = post("/invoices/#{id}/collect", params, **)
|
|
25
|
+
|
|
26
|
+
# Cancels the invoice so no payment is expected.
|
|
27
|
+
def void_invoice(id, **) = post("/invoices/#{id}/void", {}, **)
|
|
28
|
+
|
|
29
|
+
# Moves a draft invoice to +open+ and locks its amounts.
|
|
30
|
+
def finalize_invoice(id, **) = post("/invoices/#{id}/finalize", {}, **)
|
|
31
|
+
|
|
32
|
+
# Only permitted from +OPEN+ or +PAST_DUE+; other states return 422.
|
|
33
|
+
def mark_invoice_uncollectible(id, **)
|
|
34
|
+
post("/invoices/#{id}/mark-uncollectible", {}, **)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Sweeps floating items into standalone invoices (one per currency),
|
|
38
|
+
# finalizes them and attempts collection. Always pass a stable
|
|
39
|
+
# +idempotency_key+ or duplicate requests create duplicate invoices.
|
|
40
|
+
def bill_pending_items(params = nil, **)
|
|
41
|
+
post("/invoices/bill-pending-items", params, **)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# --- Invoice items ------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
# POST /invoice-items/ — creates a floating (pending) item, collected at
|
|
47
|
+
# the next renewal or on demand via #bill_pending_items. The subscription
|
|
48
|
+
# must be +active+ or +trialing+ and belong to the same customer.
|
|
49
|
+
def create_invoice_item(params = nil, **) = post("/invoice-items/", params, **)
|
|
50
|
+
|
|
51
|
+
# GET /invoice-items/{id}
|
|
52
|
+
def retrieve_invoice_item(id) = get("/invoice-items/#{id}")
|
|
53
|
+
|
|
54
|
+
# GET /invoice-items/ — auto-paginated; filter with +subscription_id+ and
|
|
55
|
+
# <tt>status: "floating"</tt> to list pending items.
|
|
56
|
+
def list_invoice_items(params = {}) = list("/invoice-items/", params)
|
|
57
|
+
|
|
58
|
+
# PATCH /invoice-items/{id}
|
|
59
|
+
def update_invoice_item(id, params = nil, **)
|
|
60
|
+
patch("/invoice-items/#{id}", params, **)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaymentKit
|
|
4
|
+
module Resources
|
|
5
|
+
# Payment intents, payment methods, refunds and checkout sessions.
|
|
6
|
+
module Payments
|
|
7
|
+
# --- Payment intents ----------------------------------------------------
|
|
8
|
+
|
|
9
|
+
# POST /payments/intents/ — takes +customer_id+, +amount_atom+,
|
|
10
|
+
# +currency+, +payment_method_id+, +processor_id+ and +metadata+.
|
|
11
|
+
def create_payment_intent(params = nil, **)
|
|
12
|
+
post("/payments/intents/", params, **)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Supports <tt>expand: "checkout_attempt"</tt>.
|
|
16
|
+
def retrieve_payment_intent(id, params = {}) = get("/payments/intents/#{id}", params)
|
|
17
|
+
|
|
18
|
+
# GET /payments/intents/ — auto-paginated Array of payment intents.
|
|
19
|
+
def list_payment_intents(params = {}) = list("/payments/intents/", params)
|
|
20
|
+
|
|
21
|
+
# Auto-paginated Array of refunds recorded against a payment intent.
|
|
22
|
+
def list_refunds_by_intent(payment_intent_id, params = {})
|
|
23
|
+
list("/payments/refunds/by_intent/#{payment_intent_id}", params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Auto-paginated Array of processor attempts for a payment intent.
|
|
27
|
+
def list_attempts_by_intent(payment_intent_id, params = {})
|
|
28
|
+
list("/payments/processor_attempts/by_intent/#{payment_intent_id}", params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# --- Payment methods ----------------------------------------------------
|
|
32
|
+
|
|
33
|
+
# POST /payments/payment_methods/
|
|
34
|
+
def create_payment_method(params = nil, **)
|
|
35
|
+
post("/payments/payment_methods/", params, **)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# GET /payments/payment_methods/{id}
|
|
39
|
+
def retrieve_payment_method(id) = get("/payments/payment_methods/#{id}")
|
|
40
|
+
|
|
41
|
+
# PUT /payments/payment_methods/{id}
|
|
42
|
+
def update_payment_method(id, params = nil, **)
|
|
43
|
+
put("/payments/payment_methods/#{id}", params, **)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Documented way to take a card out of use: flips +is_active+ to false.
|
|
47
|
+
# Prefer this over #detach_payment_method.
|
|
48
|
+
def deactivate_payment_method(id) = update_payment_method(id, is_active: false)
|
|
49
|
+
|
|
50
|
+
# Removes the payment method outright.
|
|
51
|
+
#
|
|
52
|
+
# NOTE: PaymentKit documents deletion only on the customer-portal surface
|
|
53
|
+
# (<tt>DELETE /billing-portal/token/{token}/payment-methods/{id}</tt>). This
|
|
54
|
+
# account-scoped delete is kept for backwards compatibility; if your account
|
|
55
|
+
# returns 404/405, use #deactivate_payment_method instead.
|
|
56
|
+
def detach_payment_method(id) = delete("/payments/payment_methods/#{id}")
|
|
57
|
+
|
|
58
|
+
# --- Checkout sessions --------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# Returns a session carrying the +secure_token+ used to initialise
|
|
61
|
+
# PaymentKit.js or redirect to hosted checkout. +line_items+,
|
|
62
|
+
# +success_url+ and +return_url+ are required.
|
|
63
|
+
def create_checkout_session(params = nil, **)
|
|
64
|
+
post("/checkout-sessions", params, **)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# GET /checkout-sessions/{id}
|
|
68
|
+
def retrieve_checkout_session(id) = get("/checkout-sessions/#{id}")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaymentKit
|
|
4
|
+
module Resources
|
|
5
|
+
# Subscription lifecycle, scheduling and change requests.
|
|
6
|
+
module Subscriptions
|
|
7
|
+
# POST /subscriptions — +customer_id+, +currency+, +billing_interval+,
|
|
8
|
+
# +billing_interval_count+, +period_start+ and +collection_method+ are
|
|
9
|
+
# required; +items+ is an Array of +price_id+/+quantity+ pairs.
|
|
10
|
+
def create_subscription(params = nil, **) = post("/subscriptions", params, **)
|
|
11
|
+
|
|
12
|
+
# GET /subscriptions/{id}
|
|
13
|
+
def retrieve_subscription(id) = get("/subscriptions/#{id}")
|
|
14
|
+
|
|
15
|
+
# GET /subscriptions — auto-paginated Array of subscriptions.
|
|
16
|
+
def list_subscriptions(params = {}) = list("/subscriptions", params)
|
|
17
|
+
|
|
18
|
+
# PATCH /subscriptions/{id} — subscription-level fields such as
|
|
19
|
+
# +cancel_at_period_end+ and +metadata+. Line items are not accepted here;
|
|
20
|
+
# use #update_subscription_items.
|
|
21
|
+
def update_subscription(id, params = nil, **)
|
|
22
|
+
patch("/subscriptions/#{id}", params, **)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Add/update/remove items within the current billing interval. Charge-first:
|
|
26
|
+
# payment is collected before the change applies, so pass a stable
|
|
27
|
+
# +idempotency_key+. +proration_behavior+ is required by the API.
|
|
28
|
+
def update_subscription_items(id, params = nil, **)
|
|
29
|
+
patch("/subscriptions/#{id}/items", params, **)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Immediate, irreversible cancellation. Accepts +refund_option+
|
|
33
|
+
# (+none+, +full+, +prorated+, +cancel_unpaid+) and +is_preview+.
|
|
34
|
+
def cancel_subscription(id, params = nil, **)
|
|
35
|
+
post("/subscriptions/#{id}/cancel", params, **)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Cancel on a specific date instead of immediately.
|
|
39
|
+
def schedule_cancellation(id, params = nil, **)
|
|
40
|
+
post("/subscriptions/#{id}/schedule-cancellation", params, **)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Clears a cancellation previously scheduled by #schedule_cancellation.
|
|
44
|
+
def cancel_scheduled_cancellation(id) = delete("/subscriptions/#{id}/scheduled-cancellation")
|
|
45
|
+
|
|
46
|
+
# +pause_behavior+ (+pause_immediately+/+pause_at_end+) is required.
|
|
47
|
+
def pause_subscription(id, params = nil, **)
|
|
48
|
+
post("/subscriptions/#{id}/pause", params, **)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Removes a pending +pause_at_end+ before it activates.
|
|
52
|
+
def cancel_scheduled_pause(id) = delete("/subscriptions/#{id}/scheduled-pause")
|
|
53
|
+
|
|
54
|
+
# Resumes a paused subscription. Whether billing re-anchors or catches up
|
|
55
|
+
# is governed by the account's +resume_billing_behavior+ setting.
|
|
56
|
+
def resume_subscription(id, params = nil, **)
|
|
57
|
+
post("/subscriptions/#{id}/resume", params, **)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Moves the next invoice date. Accepts +next_billing_date+,
|
|
61
|
+
# +create_proration+ and +is_preview+.
|
|
62
|
+
def reschedule_billing(id, params = nil, **)
|
|
63
|
+
post("/subscriptions/#{id}/reschedule-billing", params, **)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Forces a renewal cycle immediately.
|
|
67
|
+
def renew_subscription(id, **) = post("/subscriptions/#{id}/renew", {}, **)
|
|
68
|
+
|
|
69
|
+
# *Deprecated.* PaymentKit documents +change-plan+ as the legacy
|
|
70
|
+
# single-call endpoint, planned for deprecation. Prefer the change-request
|
|
71
|
+
# workflow (#create_change_request, #add_change_request_changes,
|
|
72
|
+
# #preview_change_request, #apply_change_request) or the one-step
|
|
73
|
+
# #apply_subscription_changes.
|
|
74
|
+
def change_plan(subscription_id, params = nil, **)
|
|
75
|
+
post("/subscriptions/#{subscription_id}/change-plan", params, **)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Cancels a scheduled (period-end) plan change before it executes.
|
|
79
|
+
def cancel_pending_change(id) = delete("/subscriptions/#{id}/pending-change")
|
|
80
|
+
|
|
81
|
+
# --- Change requests ----------------------------------------------------
|
|
82
|
+
|
|
83
|
+
# Opens a draft change request. Accepts +reason+ and +expires_in_hours+.
|
|
84
|
+
#
|
|
85
|
+
# Only one active (draft/ready) change request may exist per subscription;
|
|
86
|
+
# creating a second raises ConflictError (HTTP 409).
|
|
87
|
+
def create_change_request(subscription_id, params = nil, **)
|
|
88
|
+
post("/subscriptions/#{subscription_id}/change-requests", params, **)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# GET /subscriptions/{id}/change-requests/{request_id}
|
|
92
|
+
def retrieve_change_request(subscription_id, request_id)
|
|
93
|
+
get("/subscriptions/#{subscription_id}/change-requests/#{request_id}")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns +null+ when the subscription has no active change request.
|
|
97
|
+
def active_change_request(subscription_id)
|
|
98
|
+
get("/subscriptions/#{subscription_id}/change-requests/active")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Appends +item_changes+, +coupon_changes+, +balance_changes+ and
|
|
102
|
+
# +trial_behavior+ to a draft. Callable repeatedly; each call appends.
|
|
103
|
+
# Adding changes to a +ready+ request reverts it to +draft+.
|
|
104
|
+
def add_change_request_changes(subscription_id, request_id, params = nil, **)
|
|
105
|
+
patch("/subscriptions/#{subscription_id}/change-requests/#{request_id}", params, **)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Computes proration and moves the request to +ready+.
|
|
109
|
+
def preview_change_request(subscription_id, request_id)
|
|
110
|
+
post("/subscriptions/#{subscription_id}/change-requests/#{request_id}/preview", {})
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Charge-first execution: pass a stable +idempotency_key+.
|
|
114
|
+
def apply_change_request(subscription_id, request_id, **)
|
|
115
|
+
post("/subscriptions/#{subscription_id}/change-requests/#{request_id}/apply", {}, **)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Discards a change request without applying it.
|
|
119
|
+
def cancel_change_request(subscription_id, request_id)
|
|
120
|
+
delete("/subscriptions/#{subscription_id}/change-requests/#{request_id}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# One-step shortcut: create, add changes, preview and apply in a single call.
|
|
124
|
+
def apply_subscription_changes(subscription_id, params = nil, **)
|
|
125
|
+
post("/subscriptions/#{subscription_id}/change-requests/apply", params, **)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|