waffo-pancake 0.2.1 → 0.4.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.
@@ -3,14 +3,16 @@
3
3
  module Waffo
4
4
  module Pancake
5
5
  # Base class. `status` is the HTTP status (nil when the request never got an answer) and
6
- # `errors` the envelope's `errors` array, as Waffo sent it.
6
+ # `errors` the envelope's `errors` array, as Waffo sent it. `retry_after` is the seconds from
7
+ # the Retry-After header, nil when Waffo sent none.
7
8
  class Error < StandardError
8
- attr_reader :status, :errors
9
+ attr_reader :status, :errors, :retry_after
9
10
 
10
- def initialize(message = nil, status: nil, errors: [])
11
+ def initialize(message = nil, status: nil, errors: [], retry_after: nil)
11
12
  super(message)
12
13
  @status = status
13
14
  @errors = errors
15
+ @retry_after = retry_after
14
16
  end
15
17
  end
16
18
 
@@ -11,14 +11,28 @@ module Waffo
11
11
  # 3. config.waffo_pancake.* in config/application.rb or an environment file
12
12
  # 4. Waffo::Pancake.configure in config/initializers
13
13
  #
14
- # The logger defaults to Rails.logger, and `private_key` is filtered from logs.
14
+ # The logger defaults to Rails.logger, `private_key` is filtered from logs, and so is a
15
+ # redirect to a URL carrying a session token (`#token=`, authenticated checkout and the
16
+ # customer portal). Deprecated webhook fields are reported through `Railtie.deprecator`,
17
+ # registered as `Rails.application.deprecators[:waffo_pancake]`.
15
18
  class Railtie < ::Rails::Railtie
16
19
  CREDENTIAL_KEYS = %i[merchant_id private_key store_id environment webhook_public_key].freeze
17
20
 
21
+ TOKEN_FRAGMENT = "#token="
22
+
18
23
  config.waffo_pancake = ActiveSupport::OrderedOptions.new
19
24
 
25
+ def self.deprecator
26
+ @deprecator ||= ActiveSupport::Deprecation.new("1.0", "waffo-pancake")
27
+ end
28
+
20
29
  initializer "waffo_pancake.filter_parameters" do |app|
21
30
  app.config.filter_parameters |= [:private_key]
31
+ app.config.filter_redirect |= [TOKEN_FRAGMENT] if app.config.respond_to?(:filter_redirect)
32
+ end
33
+
34
+ initializer "waffo_pancake.deprecator" do |app|
35
+ app.deprecators[:waffo_pancake] = Railtie.deprecator if app.respond_to?(:deprecators)
22
36
  end
23
37
 
24
38
  initializer "waffo_pancake.configuration" do |app|
@@ -31,6 +45,7 @@ module Waffo
31
45
 
32
46
  app.config.waffo_pancake.each { |key, value| config.public_send(:"#{key}=", value) }
33
47
  config.logger ||= ::Rails.logger
48
+ config.deprecator = Railtie.deprecator unless app.config.waffo_pancake.key?(:deprecator)
34
49
  end
35
50
  end
36
51
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Waffo
6
+ module Pancake
7
+ # Sends an explicit JSON null in an update, which clears the field. A plain nil is dropped,
8
+ # so a variable that happens to be nil can never erase data at Waffo.
9
+ NULL = Object.new
10
+ class << NULL
11
+ def to_json(*) = "null"
12
+ def inspect = "Waffo::Pancake::NULL"
13
+ end
14
+ NULL.freeze
15
+
16
+ # The request logic shared by Client, CustomerSession and StoreCheckout. An includer defines
17
+ # `auth_headers(path, json)` and `connection`.
18
+ #
19
+ # Waffo refuses a request that carries two authentication methods, so an includer sends the
20
+ # headers of one method only.
21
+ module Requests
22
+ IDEMPOTENCY_KEY = /\A[A-Za-z0-9_-]{1,256}\z/
23
+
24
+ # Any action endpoint. Returns the envelope's `data`, plus a "warnings" key when Waffo sent
25
+ # any. Raises Rejected or Unavailable on `errors` or a non-2xx status.
26
+ #
27
+ # Top-level keys are sent in camelCase and nil values are dropped; use NULL to clear a field.
28
+ # `idempotency_key:` is sent as X-Idempotency-Key. Waffo keeps the answer for 24 hours.
29
+ def action(path, params = {}, **named)
30
+ params = params.merge(named)
31
+ key = params.delete(:idempotency_key)
32
+ response = post(path, camelize(params), idempotency_headers(key))
33
+ succeeded = !present?(response.envelope["errors"]) && (200..299).cover?(response.status)
34
+ connection.fail_with(path, response) unless succeeded
35
+
36
+ with_warnings(response.envelope)
37
+ end
38
+
39
+ # Returns `data`. Raises when the response carries `errors` and no data, or on a non-2xx
40
+ # status. Partial data comes back as is, because the fields Waffo did answer are still valid.
41
+ def graphql(query, variables = {})
42
+ path = "/v1/graphql"
43
+ response = post(path, { "query" => query, "variables" => variables })
44
+ envelope = response.envelope
45
+ failed = (present?(envelope["errors"]) && envelope["data"].nil?) || !(200..299).cover?(response.status)
46
+ connection.fail_with(path, response) if failed
47
+
48
+ envelope["data"]
49
+ end
50
+
51
+ def self.environment!(value)
52
+ value = value.to_s.strip.downcase
53
+ return value if Configuration::ENVIRONMENTS.include?(value)
54
+
55
+ raise ConfigurationError, "environment must be test or prod, got #{value.inspect}"
56
+ end
57
+
58
+ private
59
+ def post(path, params, extra_headers = {})
60
+ json = JSON.generate(params)
61
+ headers = { "Content-Type" => "application/json" }.merge(auth_headers(path, json)).merge(extra_headers)
62
+ connection.post(path, headers, json)
63
+ end
64
+
65
+ def idempotency_headers(key)
66
+ return {} if key.nil?
67
+ return { "X-Idempotency-Key" => key } if key.is_a?(String) && key.match?(IDEMPOTENCY_KEY)
68
+
69
+ raise ArgumentError, "idempotency_key must be 1 to 256 characters from A-Z a-z 0-9 _ -"
70
+ end
71
+
72
+ def with_warnings(envelope)
73
+ data = envelope["data"] || {}
74
+ warnings = envelope["warnings"]
75
+ data.is_a?(Hash) && present?(warnings) ? data.merge("warnings" => warnings) : data
76
+ end
77
+
78
+ def camelize(params)
79
+ params.each_with_object({}) do |(key, value), out|
80
+ next if value.nil?
81
+
82
+ out[key.to_s.gsub(/_([a-z\d])/) { Regexp.last_match(1).upcase }] = value.equal?(NULL) ? nil : value
83
+ end
84
+ end
85
+
86
+ def blank?(value) = value.nil? || value.to_s.strip.empty?
87
+
88
+ def present?(value) = !(value.nil? || (value.respond_to?(:empty?) && value.empty?))
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "requests"
4
+ require_relative "connection"
5
+
6
+ module Waffo
7
+ module Pancake
8
+ # Anonymous storefront access, authenticated with only the store's public slug. It holds no
9
+ # secret, so it suits code that cannot keep an API key.
10
+ #
11
+ # storefront = Waffo::Pancake::StoreCheckout.new(store_slug: "my-store", environment: "test")
12
+ # storefront.create_checkout_session(product_id: "PROD_...", currency: "USD")
13
+ class StoreCheckout
14
+ include Requests
15
+
16
+ attr_reader :store_slug, :environment
17
+
18
+ def initialize(store_slug:, config: Pancake.configuration, environment: config.environment,
19
+ base_url: config.base_url, timeout: config.timeout, logger: config.logger,
20
+ instrumenter: config.instrumenter, transport: nil)
21
+ raise ConfigurationError, "Missing store_slug" if store_slug.to_s.strip.empty?
22
+
23
+ @store_slug = store_slug
24
+ @environment = Requests.environment!(environment)
25
+ @connection = Connection.new(base_url: base_url, timeout: timeout, logger: logger, instrumenter: instrumenter,
26
+ transport: transport)
27
+ end
28
+
29
+ def create_checkout_session(product_id:, currency:, **params)
30
+ action("/v1/actions/checkout/create-session", product_id: product_id, currency: currency, **params)
31
+ end
32
+
33
+ private
34
+ attr_reader :connection
35
+
36
+ def auth_headers(_path, _json)
37
+ { "X-Store-Slug" => @store_slug, "X-Environment" => @environment }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -46,7 +46,8 @@ module Waffo
46
46
  { "CONTENT_TYPE" => "application/json", Webhook::SIGNATURE_HEADER => waffo_webhook_signature(body, **options) }
47
47
  end
48
48
 
49
- # A webhook envelope in Waffo's shape. `mode` and `store_id` default to the configuration.
49
+ # A webhook envelope in Waffo's shape, as a plain Hash to JSON.generate and sign.
50
+ # `mode` and `store_id` default to the configuration.
50
51
  def waffo_webhook_event(event_type, data: {}, id: SecureRandom.uuid, mode: nil, store_id: nil)
51
52
  config = Waffo::Pancake.configuration
52
53
  {
@@ -70,17 +71,31 @@ module Waffo
70
71
  Waffo::Pancake.client = previous
71
72
  end
72
73
 
73
- # Stands in for Waffo::Pancake::Client. Answers subscription_order from `orders`, moves
74
- # an order's status on cancel / reactivate the way Waffo does, and records every call
75
- # in `calls` as [method, arguments]. Set `fail_with` to an error to make calls raise it.
74
+ # Stands in for Waffo::Pancake::Client. Answers subscription_order from `orders` and
75
+ # webhook_deliveries from `deliveries`, moves an order's status on cancel / reactivate the
76
+ # way Waffo does, and records every call in `calls` as [method, arguments]. Set
77
+ # `fail_with` to an error to make calls raise it.
76
78
  class FakeClient
77
79
  attr_reader :orders, :calls
78
- attr_accessor :fail_with, :checkout_url
80
+ attr_accessor :fail_with, :checkout_url, :portal_url, :deliveries
79
81
 
80
82
  def initialize(orders = {})
81
83
  @orders = orders.transform_keys(&:to_s)
82
84
  @calls = []
83
85
  @checkout_url = "https://pancake.waffo.ai/store/test/checkout/cs_test"
86
+ @portal_url = "https://pancake.waffo.ai/consumer/portal/store/STO_test"
87
+ @deliveries = []
88
+ end
89
+
90
+ def create_customer_portal_link(**params)
91
+ record(:create_customer_portal_link, params)
92
+ { "portalUrl" => "#{portal_url}#token=test_token", "expiresAt" => (Time.now + 900).utc.iso8601 }
93
+ end
94
+
95
+ def webhook_deliveries(**params)
96
+ record(:webhook_deliveries, params)
97
+ status = params[:status]&.to_s
98
+ status ? deliveries.select { |delivery| delivery["status"] == status } : deliveries.dup
84
99
  end
85
100
 
86
101
  def subscription_order(order_id)
@@ -100,13 +115,13 @@ module Waffo
100
115
  "expiresAt" => (Time.now + 2700).utc.iso8601 }
101
116
  end
102
117
 
103
- def cancel_subscription(order_id)
104
- record(:cancel_subscription, order_id)
118
+ def cancel_subscription(order_id, **params)
119
+ record(:cancel_subscription, order_id, *optional(params))
105
120
  change_status(order_id, "canceling")
106
121
  end
107
122
 
108
- def reactivate_subscription(order_id)
109
- record(:reactivate_subscription, order_id)
123
+ def reactivate_subscription(order_id, **params)
124
+ record(:reactivate_subscription, order_id, *optional(params))
110
125
  change_status(order_id, "active")
111
126
  end
112
127
 
@@ -115,19 +130,41 @@ module Waffo
115
130
  {}
116
131
  end
117
132
 
118
- def action(path, params)
119
- record(:action, path, params)
133
+ def action(path, params = {}, **named)
134
+ record(:action, path, params.merge(named))
120
135
  {}
121
136
  end
122
137
 
138
+ # Recorded, then answers as this same fake, so a test reads every call from one list.
139
+ def customer(token, **options)
140
+ record(:customer, token, *optional(options))
141
+ self
142
+ end
143
+
123
144
  def called?(name) = calls.any? { |call| call.first == name }
124
145
 
146
+ # Every other API method records its call and answers empty, so a new endpoint cannot be
147
+ # missing from the fake. Lists answer [], counts 0, reads nil, writes {}.
148
+ answers = API::Queries.empty_answers.merge(webhook_deliveries_count: 0)
149
+ api_methods = [API::CustomerActions, API::StoreActions, API::ProductActions, API::PromotionActions,
150
+ API::Queries].flat_map(&:public_instance_methods)
151
+ client_only = %i[issue_session_token create_authenticated_plan_change webhook_deliveries_count
152
+ create_onetime_order create_subscription_order]
153
+ (api_methods + client_only).uniq.reject { |name| method_defined?(name) }.each do |name|
154
+ define_method(name) do |*arguments, **params|
155
+ record(name, *arguments, *optional(params))
156
+ answers.fetch(name, {})
157
+ end
158
+ end
159
+
125
160
  private
126
161
  def record(name, *arguments)
127
162
  calls << [name, *arguments]
128
163
  raise fail_with if fail_with
129
164
  end
130
165
 
166
+ def optional(params) = params.empty? ? [] : [params]
167
+
131
168
  def change_status(order_id, status)
132
169
  @orders[order_id.to_s]&.merge!("status" => status)
133
170
  { "orderId" => order_id, "status" => status }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Waffo
4
4
  module Pancake
5
- VERSION = "0.2.1"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Waffo
6
+ module Pancake
7
+ module Webhook
8
+ PAYMENT_EVENT_TYPES = %w[order.completed subscription.payment_succeeded].freeze
9
+ REFUND_EVENT_TYPES = %w[refund.succeeded refund.failed].freeze
10
+
11
+ # The six `data` fields whose name meant a different thing per event family, with the field
12
+ # that replaces each on payment, refund and subscription events. Waffo keeps sending them
13
+ # until the next major API version, not before DEPRECATED_FIELDS_REMOVAL.
14
+ DEPRECATED_FIELDS = {
15
+ "amount" => { payment: "chargedAmount", refund: "refundedAmount", subscription: "planPrice.total" },
16
+ "total" => { payment: "listPrice.total", refund: "originalPayment.total", subscription: "planPrice.total" },
17
+ "subtotal" => { payment: "listPrice.subtotal", refund: "originalPayment.subtotal",
18
+ subscription: "planPrice.subtotal" },
19
+ "taxAmount" => { payment: "listPrice.taxAmount", refund: "originalPayment.taxAmount",
20
+ subscription: "planPrice.taxAmount" },
21
+ "taxRate" => { payment: "listPrice.taxRate", refund: "originalPayment.taxRate",
22
+ subscription: "planPrice.taxRate" },
23
+ "taxName" => { payment: "listPrice.taxName", refund: "originalPayment.taxName",
24
+ subscription: "planPrice.taxName" }
25
+ }.freeze
26
+ DEPRECATED_FIELDS_REMOVAL = "2027-09-20"
27
+
28
+ module_function
29
+
30
+ # :payment (one charge), :refund, :subscription (a subscription's status), or nil.
31
+ def family(event_type)
32
+ event_type = event_type.to_s
33
+ return :payment if PAYMENT_EVENT_TYPES.include?(event_type)
34
+ return :refund if REFUND_EVENT_TYPES.include?(event_type)
35
+
36
+ :subscription if event_type.start_with?("subscription.")
37
+ end
38
+
39
+ # A delivery as a Hash, with readers for the envelope. `data` is an EventData, which warns
40
+ # through Waffo::Pancake::Deprecation when a deprecated field is read.
41
+ #
42
+ # event = Waffo::Pancake::Webhook::Event.wrap(stored_payload) # Hash or JSON String
43
+ # event.data.charged_amount # => "9.00"
44
+ class Event < Hash
45
+ # `payload` is a Hash (string or symbol keys) or its JSON. Use it on a delivery you stored
46
+ # or read back with Client#webhook_deliveries; it does not check a signature.
47
+ def self.wrap(payload)
48
+ return payload if payload.is_a?(Event)
49
+
50
+ payload = JSON.parse(payload) if payload.is_a?(String)
51
+ raise ArgumentError, "webhook event must be a Hash, got #{payload.class}" unless payload.respond_to?(:to_hash)
52
+
53
+ event = new.replace(payload.to_hash.transform_keys(&:to_s))
54
+ event["data"] = EventData.new(event["data"], event["eventType"]) if event["data"].respond_to?(:to_hash)
55
+ event
56
+ end
57
+
58
+ # The delivery id: the same on every retry, so deduplicate on it.
59
+ def id = self["id"]
60
+ def event_type = self["eventType"]
61
+ def event_id = self["eventId"]
62
+ def store_id = self["storeId"]
63
+ def mode = self["mode"]
64
+ def test? = mode == "test"
65
+ def timestamp = self["timestamp"]
66
+ def data = self["data"] || EventData.new({}, event_type)
67
+ def family = Webhook.family(event_type)
68
+ def payment? = family == :payment
69
+ def refund? = family == :refund
70
+ def subscription? = family == :subscription
71
+ end
72
+
73
+ # The `data` of a delivery. Reading `amount`, `total`, `subtotal`, `taxAmount`, `taxRate` or
74
+ # `taxName` with [], fetch or dig still answers the value, and reports the field to read
75
+ # instead. Iterating, serializing and `key?` report nothing.
76
+ class EventData < Hash
77
+ attr_reader :event_type
78
+
79
+ def initialize(fields = {}, event_type = nil)
80
+ super()
81
+ replace(fields.to_hash.transform_keys(&:to_s))
82
+ @event_type = event_type
83
+ end
84
+
85
+ def [](key)
86
+ deprecated_read(key)
87
+ super
88
+ end
89
+
90
+ def fetch(key, *default, &block)
91
+ deprecated_read(key)
92
+ super
93
+ end
94
+
95
+ def dig(key, *keys)
96
+ deprecated_read(key)
97
+ super
98
+ end
99
+
100
+ def order_id = self["orderId"]
101
+ def currency = self["currency"]
102
+
103
+ # What this charge actually took, after prorated credits, trials and zero-amount card
104
+ # checks. Payment events only; nil when the payment channel reported no amount.
105
+ def charged_amount = self["chargedAmount"]
106
+
107
+ # What this refund returned. Refund events only; nil when the channel reported no amount.
108
+ def refunded_amount = self["refundedAmount"]
109
+
110
+ # What the refunded payment actually collected: the most a refund can reach. Refund
111
+ # events only; nil when the channel reported no amount for that payment.
112
+ def original_charged_amount = self["originalChargedAmount"]
113
+
114
+ # Breakdowns (total, subtotal, taxAmount, taxRate, taxName), each on one event family.
115
+ def list_price = self["listPrice"]
116
+ def original_payment = self["originalPayment"]
117
+ def plan_price = self["planPrice"]
118
+
119
+ # The breakdown this event's figures refer to, whatever its family.
120
+ def price = list_price || original_payment || plan_price
121
+
122
+ # The payment channel's billing period: 1 for the first, N for the Nth renewal. A failed
123
+ # charge uses a period too; 0 is authorized but not charged yet. On refunds, the period of
124
+ # the refunded charge. Nil on one-time orders and on older subscriptions. Not unique per
125
+ # event: deduplicate on the event id.
126
+ def period_number = self["periodNumber"]
127
+ def first_period? = period_number == 1
128
+ def renewal? = period_number.is_a?(Integer) && period_number > 1
129
+
130
+ private
131
+ def deprecated_read(key)
132
+ replacements = DEPRECATED_FIELDS[key.to_s]
133
+ return unless replacements
134
+
135
+ Deprecation.warn(deprecation_message(key.to_s, replacements), caller_locations(2))
136
+ end
137
+
138
+ def deprecation_message(key, replacements)
139
+ family = Webhook.family(event_type)
140
+ instead = if family
141
+ reader(replacements.fetch(family))
142
+ else
143
+ replacements.map { |name, field| "#{reader(field)} on #{name} events" }.join(", ")
144
+ end
145
+ "Waffo webhook data[#{key.inspect}] is deprecated#{" on #{event_type}" if family}; read #{instead} " \
146
+ "instead. Waffo removes it in its next major API version, not before #{DEPRECATED_FIELDS_REMOVAL}."
147
+ end
148
+
149
+ def reader(field)
150
+ parts = field.split(".")
151
+ parts.one? ? "data[#{field.inspect}]" : "data.dig(#{parts.map(&:inspect).join(", ")})"
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -10,7 +10,8 @@ module Waffo
10
10
  #
11
11
  # event = Waffo::Pancake::Webhook.verify(request.raw_post, request.headers["X-Waffo-Signature"],
12
12
  # environment: "prod")
13
- # event["eventType"] # => "subscription.activated"
13
+ # event["eventType"] # => "subscription.activated"
14
+ # event.data.period_number # => 1
14
15
  #
15
16
  # The header is `t=<unix ms>,v1=<base64>`: an RSA-SHA256 signature over "<t>.<raw body>".
16
17
  # Pass the body exactly as received; a re-serialized JSON will not verify.
@@ -24,7 +25,7 @@ module Waffo
24
25
 
25
26
  ENVIRONMENTS = %w[test prod].freeze
26
27
 
27
- # Waffo's platform keys, as embedded in @waffo/pancake-ts 0.25.0.
28
+ # Waffo's platform keys, as embedded in @waffo/pancake-ts 0.26.0.
28
29
  PUBLIC_KEYS = {
29
30
  "test" => <<~PEM,
30
31
  -----BEGIN PUBLIC KEY-----
@@ -60,7 +61,7 @@ module Waffo
60
61
 
61
62
  module_function
62
63
 
63
- # Returns the parsed event. Raises InvalidSignature.
64
+ # Returns the parsed event, a Webhook::Event (a Hash). Raises InvalidSignature.
64
65
  #
65
66
  # environment: "test" or "prod" uses that environment's key; nil tries prod, then test.
66
67
  # public_key: a key to use instead (skips the lookup and `environment`).
@@ -74,7 +75,8 @@ module Waffo
74
75
  keys = public_key ? [Keys.public_key(public_key)] : keys_for(environment)
75
76
  raise InvalidSignature, "Invalid webhook signature" unless keys.any? { |key| valid?(key, signature, input) }
76
77
 
77
- JSON.parse(payload.to_s)
78
+ event = JSON.parse(payload.to_s)
79
+ event.is_a?(Hash) ? Event.wrap(event) : event
78
80
  rescue JSON::ParserError
79
81
  raise InvalidSignature, "Webhook body is not JSON"
80
82
  end
data/lib/waffo/pancake.rb CHANGED
@@ -4,7 +4,10 @@ require_relative "pancake/version"
4
4
  require_relative "pancake/errors"
5
5
  require_relative "pancake/keys"
6
6
  require_relative "pancake/client"
7
+ require_relative "pancake/store_checkout"
8
+ require_relative "pancake/deprecation"
7
9
  require_relative "pancake/webhook"
10
+ require_relative "pancake/webhook/event"
8
11
  require_relative "pancake/configuration"
9
12
  require_relative "pancake/railtie" if defined?(Rails::Railtie)
10
13
 
@@ -13,6 +16,7 @@ module Waffo
13
16
  module Pancake
14
17
  # Need ActiveSupport / Action Pack; loaded on first use.
15
18
  autoload :WebhookController, "waffo/pancake/webhook_controller"
19
+ autoload :CustomerPortalController, "waffo/pancake/customer_portal_controller"
16
20
  autoload :TestHelpers, "waffo/pancake/test_helpers"
17
21
  end
18
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waffo-pancake
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Good First Issue
@@ -41,14 +41,26 @@ files:
41
41
  - lib/generators/waffo_pancake/install/templates/webhook_job.rb.tt
42
42
  - lib/generators/waffo_pancake/install/templates/webhooks_controller.rb.tt
43
43
  - lib/waffo/pancake.rb
44
+ - lib/waffo/pancake/api/customer_actions.rb
45
+ - lib/waffo/pancake/api/product_actions.rb
46
+ - lib/waffo/pancake/api/promotion_actions.rb
47
+ - lib/waffo/pancake/api/queries.rb
48
+ - lib/waffo/pancake/api/store_actions.rb
44
49
  - lib/waffo/pancake/client.rb
45
50
  - lib/waffo/pancake/configuration.rb
51
+ - lib/waffo/pancake/connection.rb
52
+ - lib/waffo/pancake/customer_portal_controller.rb
53
+ - lib/waffo/pancake/customer_session.rb
54
+ - lib/waffo/pancake/deprecation.rb
46
55
  - lib/waffo/pancake/errors.rb
47
56
  - lib/waffo/pancake/keys.rb
48
57
  - lib/waffo/pancake/railtie.rb
58
+ - lib/waffo/pancake/requests.rb
59
+ - lib/waffo/pancake/store_checkout.rb
49
60
  - lib/waffo/pancake/test_helpers.rb
50
61
  - lib/waffo/pancake/version.rb
51
62
  - lib/waffo/pancake/webhook.rb
63
+ - lib/waffo/pancake/webhook/event.rb
52
64
  - lib/waffo/pancake/webhook_controller.rb
53
65
  homepage: https://github.com/goodfirstissueorg/waffo-pancake-ruby
54
66
  licenses: