Module: PaymentKit

Defined in:
lib/payment_kit.rb,
lib/payment_kit/client.rb,
lib/payment_kit/engine.rb,
lib/payment_kit/errors.rb,
lib/payment_kit/version.rb,
lib/payment_kit/webhook.rb,
lib/payment_kit/namespace.rb,
lib/payment_kit/configuration.rb,
lib/payment_kit/instrumentation.rb,
lib/payment_kit/resources/catalog.rb,
lib/payment_kit/resources/invoices.rb,
lib/payment_kit/resources/payments.rb,
lib/payment_kit/resources/customers.rb,
lib/payment_kit/notification_adapter.rb,
lib/payment_kit/resources/subscriptions.rb,
app/controllers/payment_kit/webhook_controller.rb

Overview

Ruby HTTP client and webhook event bus for the PaymentKit REST API.

HTTP API access goes through PaymentKit::Client. Inbound webhooks are verified and then fanned out via ActiveSupport::Notifications (+subscribe+ / instrument / all).

Defined Under Namespace

Modules: Instrumentation, Resources, Webhook Classes: APIConnectionError, APIError, AuthenticationError, CardError, Client, Configuration, ConflictError, Engine, Error, InvalidRequestError, Namespace, NotificationAdapter, PermissionError, RateLimitError, SignatureVerificationError, WebhookController

Constant Summary collapse

DEFAULT_EVENT_FILTER =

Identity filter: dispatches every event. Replace via event_filter=.

->(event) { event }
DEFAULT_EVENT_RETRIEVER =

Runs after signature verification and before dispatch. Return the event to continue, or nil to drop it. PaymentKit redelivers on retry, so hosts should deduplicate here on the event id — the same value PaymentKit sends in the X-Webhook-Event-Id header:

PaymentKit.event_retriever = lambda do |event|
key = "payment_kit:webhook:#{event["id"]}"
Sidekiq.redis { |r| r.set(key, "1", nx: true, ex: 3 * 24 * 60 * 60) } ? event : nil
end
->(event) { event }
VERSION =

Gem version.

"1.0.0"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.adapterObject

Wraps each subscriber so it receives a single event argument. Defaults to NotificationAdapter.



24
25
26
# File 'lib/payment_kit.rb', line 24

def adapter
  @adapter
end

.backendObject

Pub/sub backend. Defaults to ActiveSupport::Notifications; any object answering instrument(name, payload) and subscribe(pattern, callable) works.



28
29
30
# File 'lib/payment_kit.rb', line 28

def backend
  @backend
end

.error_handlerObject

Callable (exception, request) used by WebhookController to report a failed delivery and answer 200 instead of 500.



43
44
45
# File 'lib/payment_kit.rb', line 43

def error_handler
  @error_handler
end

.event_filterObject

Callable run before dispatch. Return the event to continue, nil to drop it.



35
36
37
# File 'lib/payment_kit.rb', line 35

def event_filter
  @event_filter
end

.event_retrieverObject

Callable run after verification, before dispatch — the deduplication hook. Return the event to continue, nil to drop a redelivery.



39
40
41
# File 'lib/payment_kit.rb', line 39

def event_retriever
  @event_retriever
end

.namespaceObject

Namespace prefixing instrumented event names. Defaults to Namespace.new("payment_kit.").



32
33
34
# File 'lib/payment_kit.rb', line 32

def namespace
  @namespace
end

.subscriber_error_handlerObject

Callable (exception, event) that isolates a failing subscriber, so one raising handler does not fail the whole delivery.



47
48
49
# File 'lib/payment_kit.rb', line 47

def subscriber_error_handler
  @subscriber_error_handler
end

Class Method Details

.all(callable = nil) ⇒ Object

Subscribe to every namespaced PaymentKit event.



113
114
115
# File 'lib/payment_kit.rb', line 113

def all(callable = nil, &)
  subscribe(nil, callable, &)
end

.configurationObject

Global configuration object, created on first use.



50
51
52
# File 'lib/payment_kit.rb', line 50

def configuration
  @configuration ||= Configuration.new
end

.configure(&block) ⇒ Object

Two forms, chosen by block arity:

PaymentKit.configure { |config| config.secret_key = "st_..." }  # settings
PaymentKit.configure { subscribe("invoice.paid") { |e| ... } }  # subscriber DSL

The arity-0 form runs against the module itself, so subscriptions can be registered without repeating the PaymentKit. receiver.

Raises:

  • (ArgumentError)


61
62
63
64
65
66
# File 'lib/payment_kit.rb', line 61

def configure(&block)
  raise ArgumentError, "PaymentKit.configure requires a block" if block.nil?

  block.arity.zero? ? instance_eval(&block) : yield(configuration)
  configuration
end

.instrument(event) ⇒ Object

Run event_filter, then notify subscribers under payment_kit.<type>. Returns the (possibly filtered) event, or nil when the filter drops it.



119
120
121
122
123
124
125
126
127
128
# File 'lib/payment_kit.rb', line 119

def instrument(event)
  filtered = event_filter.call(event)
  return if filtered.nil?

  type = event_type(filtered)
  raise InvalidRequestError, "PaymentKit event is missing a type" if type.empty?

  backend.instrument(namespace.call(type), filtered)
  filtered
end

.listening?(name) ⇒ Boolean

Whether any subscriber is currently listening for name.

Returns:

  • (Boolean)


144
145
146
147
148
149
# File 'lib/payment_kit.rb', line 144

def listening?(name)
  notifier = backend.notifier
  return false unless notifier.respond_to?(:listening?)

  notifier.listening?(namespace.call(name))
end

.process_webhook(payload, signature, secrets: nil) ⇒ Object

Verify webhook signature(s), run the retriever, instrument the event.

Returns the dispatched event, or nil when event_retriever dropped it (for example a duplicate delivery).



134
135
136
137
138
139
140
141
# File 'lib/payment_kit.rb', line 134

def process_webhook(payload, signature, secrets: nil)
  event = Webhook.construct_event(payload, signature, secrets || signing_secrets)
  event = event_retriever.call(event)
  return if event.nil?

  instrument(event)
  event
end

.reset_configuration!Object

Restores configuration and the event-bus hooks to their defaults. Intended for test suites.



70
71
72
73
74
75
76
77
# File 'lib/payment_kit.rb', line 70

def reset_configuration!
  @configuration = Configuration.new
  self.event_filter = DEFAULT_EVENT_FILTER
  self.event_retriever = DEFAULT_EVENT_RETRIEVER
  self.error_handler = nil
  self.subscriber_error_handler = nil
  configuration
end

.signing_secretObject

First configured signing secret.



82
83
84
# File 'lib/payment_kit.rb', line 82

def signing_secret
  configuration.signing_secret
end

.signing_secret=(value) ⇒ Object

Assigns a single signing secret, replacing any already configured.



87
88
89
# File 'lib/payment_kit.rb', line 87

def signing_secret=(value)
  configuration.signing_secret = value
end

.signing_secretsObject

All configured signing secrets, as an Array. During a roll-secret grace period both the old and new secret are live and are tried in order.



93
94
95
# File 'lib/payment_kit.rb', line 93

def signing_secrets
  Array(configuration.signing_secrets).compact
end

.signing_secrets=(value) ⇒ Object

Assigns the full list of signing secrets.



98
99
100
# File 'lib/payment_kit.rb', line 98

def signing_secrets=(value)
  configuration.signing_secrets = value.nil? ? nil : Array(value)
end

.subscribe(name, callable = nil, &block) ⇒ Object

Subscribe to an event type or type prefix (e.g. "invoice.paid", "invoice.").

Raises:

  • (ArgumentError)


105
106
107
108
109
110
# File 'lib/payment_kit.rb', line 105

def subscribe(name, callable = nil, &block)
  handler = callable || block
  raise ArgumentError, "subscriber callable or block required" if handler.nil?

  backend.subscribe(namespace.to_regexp(name), adapter.call(isolate(handler)))
end