waffo-pancake 0.1.0 → 0.2.1
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 +26 -0
- data/README.md +107 -2
- data/lib/generators/waffo_pancake/install/install_generator.rb +69 -0
- data/lib/generators/waffo_pancake/install/templates/create_waffo_webhook_events.rb.tt +16 -0
- data/lib/generators/waffo_pancake/install/templates/initializer.rb.tt +17 -0
- data/lib/generators/waffo_pancake/install/templates/webhook_event.rb.tt +17 -0
- data/lib/generators/waffo_pancake/install/templates/webhook_job.rb.tt +25 -0
- data/lib/generators/waffo_pancake/install/templates/webhooks_controller.rb.tt +16 -0
- data/lib/waffo/pancake/client.rb +23 -8
- data/lib/waffo/pancake/configuration.rb +126 -0
- data/lib/waffo/pancake/railtie.rb +38 -0
- data/lib/waffo/pancake/test_helpers.rb +138 -0
- data/lib/waffo/pancake/version.rb +1 -1
- data/lib/waffo/pancake/webhook_controller.rb +76 -0
- data/lib/waffo/pancake.rb +5 -0
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b498f6ef1ac1fbdec66122b1a9d4758ebd6ea917840563c8f296c9e020c03bdd
|
|
4
|
+
data.tar.gz: c34ede893f8f1a84c2d6f7e71968dddfed09ce1a20e43140156805cf2bf93e39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bee472c21659562c0a68275d615eb5af3f21d95ec1937f2aed7fab7a5570b1f968e6b27f3ff4bbd7fa82598eef3e30407b35afbf8503759cca44490aa9e3bb85
|
|
7
|
+
data.tar.gz: f96b92d6942e7a94e298153a02c1f9a5b480afb1c399ba420da5044964bbe7f946ca6d02f09c54086d256f2881485ff250c166536223d7493122fc01b8577926
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- `Client#subscription_order` also returns the order's current product as `productId`, read
|
|
6
|
+
from `subscriptionProduct { id }`, so a plan change to another product can be detected.
|
|
7
|
+
|
|
8
|
+
## 0.2.0
|
|
9
|
+
|
|
10
|
+
- `Waffo::Pancake.configure` / `.configuration`: process-wide settings that start from the
|
|
11
|
+
`WAFFO_*` environment variables, a shared `Waffo::Pancake.client`, and
|
|
12
|
+
`Waffo::Pancake.verify_webhook` using the configured environment, key and tolerances.
|
|
13
|
+
`Client.new` arguments now default to the configuration (still the environment variables when
|
|
14
|
+
nothing is configured).
|
|
15
|
+
- Rails integration, loaded only inside a Rails app:
|
|
16
|
+
- A Railtie that reads `credentials.waffo` and `config.waffo_pancake`, logs to `Rails.logger`
|
|
17
|
+
and filters `private_key` from logs.
|
|
18
|
+
- `Waffo::Pancake::WebhookController`, a controller concern that verifies the signature
|
|
19
|
+
before the action, keeps the payload out of params and the request log, and acknowledges
|
|
20
|
+
deliveries for another environment or store without running the action.
|
|
21
|
+
- `bin/rails generate waffo_pancake:install`: initializer, route, webhook controller, and
|
|
22
|
+
(unless `--skip-events-table`) a `waffo_webhook_events` table, model and job.
|
|
23
|
+
- Instrumentation: `request.waffo_pancake` for each API request and
|
|
24
|
+
`verify_webhook.waffo_pancake` for each webhook check, through ActiveSupport::Notifications
|
|
25
|
+
when it is loaded or any `instrumenter` you configure.
|
|
26
|
+
- `Waffo::Pancake::TestHelpers`: signed webhook headers for a throwaway key, event builders, and
|
|
27
|
+
`FakeClient` to stand in for the API.
|
|
28
|
+
|
|
3
29
|
## 0.1.0
|
|
4
30
|
|
|
5
31
|
- `Waffo::Pancake::Client`: signed merchant API requests (RSA-SHA256, `X-Merchant-Id` /
|
data/README.md
CHANGED
|
@@ -14,6 +14,22 @@ gem "waffo-pancake"
|
|
|
14
14
|
|
|
15
15
|
## Configure
|
|
16
16
|
|
|
17
|
+
Settings come from `Waffo::Pancake.configure`, which starts from the `WAFFO_*` environment
|
|
18
|
+
variables (`WAFFO_MERCHANT_ID`, `WAFFO_PRIVATE_KEY`, `WAFFO_STORE_ID`, `WAFFO_ENVIRONMENT`):
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
Waffo::Pancake.configure do |config|
|
|
22
|
+
config.merchant_id = "MER_..."
|
|
23
|
+
config.private_key = File.read("waffo.pem")
|
|
24
|
+
config.store_id = "STO_..."
|
|
25
|
+
config.environment = "prod" # which webhook key to trust; nil tries prod, then test
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
Waffo::Pancake.client.cancel_subscription("ORD_...") # a client built from the configuration
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or build a client yourself:
|
|
32
|
+
|
|
17
33
|
Create an API key in the Dashboard (**API & Development**). A key is bound to test or prod
|
|
18
34
|
when it is created, so the key decides the environment.
|
|
19
35
|
|
|
@@ -115,6 +131,94 @@ both), or pass `public_key:`. Timestamps may be up to 45 minutes old and 1 minut
|
|
|
115
131
|
|
|
116
132
|
`Waffo::Pancake::Webhook::EVENT_TYPES` lists the event types.
|
|
117
133
|
|
|
134
|
+
## Rails
|
|
135
|
+
|
|
136
|
+
Inside a Rails app the gem configures itself. Settings are read in this order, the last one
|
|
137
|
+
winning: `WAFFO_*` environment variables, `credentials.waffo`, `config.waffo_pancake`, then
|
|
138
|
+
`Waffo::Pancake.configure` in an initializer. The logger is `Rails.logger`, and `private_key`
|
|
139
|
+
is added to `filter_parameters`.
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
# bin/rails credentials:edit
|
|
143
|
+
waffo:
|
|
144
|
+
merchant_id: MER_...
|
|
145
|
+
private_key: "-----BEGIN PRIVATE KEY-----\n..."
|
|
146
|
+
store_id: STO_...
|
|
147
|
+
environment: prod
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
bin/rails generate waffo_pancake:install # add --skip-events-table to leave out the table
|
|
152
|
+
bin/rails db:migrate
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The generator adds `config/initializers/waffo_pancake.rb`, a `POST /webhooks/waffo` route, and
|
|
156
|
+
`WaffoWebhooksController`. Unless `--skip-events-table` is given it also adds a
|
|
157
|
+
`waffo_webhook_events` table (one row per delivery id, so a retry is not handled twice), its
|
|
158
|
+
model and a `WaffoWebhookJob` to fill in.
|
|
159
|
+
|
|
160
|
+
The controller uses the `Waffo::Pancake::WebhookController` concern, which you can also include
|
|
161
|
+
in your own controller:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
class WaffoWebhooksController < ActionController::Base
|
|
165
|
+
include Waffo::Pancake::WebhookController
|
|
166
|
+
|
|
167
|
+
def create
|
|
168
|
+
HandleWaffoEventJob.perform_later(waffo_event) # verified, parsed delivery
|
|
169
|
+
head :ok
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Before the action the concern:
|
|
175
|
+
|
|
176
|
+
- verifies `X-Waffo-Signature` with the configured key, and answers 401 if it fails;
|
|
177
|
+
- answers 413 for a body over 256 KB;
|
|
178
|
+
- answers 200 without running the action when the delivery is for another `environment` or `store_id` (when those are configured), since an error status only makes Waffo retry it;
|
|
179
|
+
- keeps the payload out of `params` and the request log.
|
|
180
|
+
|
|
181
|
+
### Instrumentation
|
|
182
|
+
|
|
183
|
+
With ActiveSupport loaded (or any object answering `instrument(name, payload) { }` set as
|
|
184
|
+
`config.instrumenter`), the gem reports:
|
|
185
|
+
|
|
186
|
+
| Event | Payload |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `request.waffo_pancake` | `method`, `path`, `status` (nil when there was no answer), `exception` when raised |
|
|
189
|
+
| `verify_webhook.waffo_pancake` | `environment`, `event_id`, `event_type`, `mode`, `exception` when refused |
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
ActiveSupport::Notifications.subscribe("request.waffo_pancake") do |event|
|
|
193
|
+
Rails.logger.info("Waffo #{event.payload[:path]} #{event.payload[:status]} #{event.duration.round}ms")
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Testing
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
# spec/rails_helper.rb (or test/test_helper.rb with ActiveSupport::TestCase)
|
|
201
|
+
require "waffo/pancake/test_helpers"
|
|
202
|
+
RSpec.configure { |config| config.include Waffo::Pancake::TestHelpers }
|
|
203
|
+
|
|
204
|
+
it "accepts a signed delivery" do
|
|
205
|
+
body = waffo_webhook_event("subscription.activated", data: { "orderId" => "ORD_1" }).to_json
|
|
206
|
+
with_waffo_webhook_key { post "/webhooks/waffo", params: body, headers: waffo_webhook_headers(body) }
|
|
207
|
+
expect(response).to have_http_status(:ok)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "cancels at Waffo" do
|
|
211
|
+
with_fake_waffo_client(Waffo::Pancake::TestHelpers::FakeClient.new("ORD_1" => { "status" => "active" })) do |waffo|
|
|
212
|
+
post "/billing/cancel"
|
|
213
|
+
expect(waffo.calls).to include([:cancel_subscription, "ORD_1"])
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`FakeClient` answers `subscription_order` from the orders it was given, moves an order to
|
|
219
|
+
`canceling` / `active` on cancel / reactivate, records every call in `calls`, and raises
|
|
220
|
+
`fail_with` when set.
|
|
221
|
+
|
|
118
222
|
## Errors
|
|
119
223
|
|
|
120
224
|
| Class | When |
|
|
@@ -142,10 +246,11 @@ Test cards: `4576 7500 0000 0110` succeeds and `4576 7500 0000 0220` is declined
|
|
|
142
246
|
|
|
143
247
|
```sh
|
|
144
248
|
bundle install
|
|
145
|
-
bundle exec rake test
|
|
249
|
+
bundle exec rake test # test:core (no Rails loaded) and test:rails
|
|
146
250
|
```
|
|
147
251
|
|
|
148
|
-
Releases are published to RubyGems by the `Release` workflow when a `v*` tag is pushed
|
|
252
|
+
Releases are published to RubyGems by the `Release` workflow when a `v*` tag is pushed (see
|
|
253
|
+
[RELEASING.md](RELEASING.md)). It uses
|
|
149
254
|
[trusted publishing](https://guides.rubygems.org/trusted-publishing/), so no API key is stored.
|
|
150
255
|
|
|
151
256
|
## License
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module WaffoPancake
|
|
7
|
+
module Generators
|
|
8
|
+
# bin/rails generate waffo_pancake:install [--skip-events-table]
|
|
9
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
10
|
+
include ::Rails::Generators::Migration
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc "Adds a Waffo Pancake initializer and a verified webhook endpoint, with a table that " \
|
|
15
|
+
"records each delivery once and a job that handles it."
|
|
16
|
+
|
|
17
|
+
class_option :events_table, type: :boolean, default: true,
|
|
18
|
+
desc: "Record deliveries in waffo_webhook_events and handle them in a job"
|
|
19
|
+
|
|
20
|
+
def self.next_migration_number(dirname)
|
|
21
|
+
require "rails/generators/active_record"
|
|
22
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_initializer
|
|
26
|
+
template "initializer.rb", "config/initializers/waffo_pancake.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_controller
|
|
30
|
+
template "webhooks_controller.rb", "app/controllers/waffo_webhooks_controller.rb"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_route
|
|
34
|
+
route 'post "webhooks/waffo", to: "waffo_webhooks#create", as: :waffo_webhook'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_events_table
|
|
38
|
+
return unless events_table?
|
|
39
|
+
|
|
40
|
+
migration_template "create_waffo_webhook_events.rb", "db/migrate/create_waffo_webhook_events.rb"
|
|
41
|
+
template "webhook_event.rb", "app/models/waffo_webhook_event.rb"
|
|
42
|
+
template "webhook_job.rb", "app/jobs/waffo_webhook_job.rb"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def show_next_steps
|
|
46
|
+
say <<~TEXT
|
|
47
|
+
|
|
48
|
+
Waffo Pancake is installed. Next:
|
|
49
|
+
1. Add credentials (bin/rails credentials:edit) or WAFFO_* environment variables:
|
|
50
|
+
waffo:
|
|
51
|
+
merchant_id: MER_...
|
|
52
|
+
private_key: "-----BEGIN PRIVATE KEY-----\\n..."
|
|
53
|
+
store_id: STO_...
|
|
54
|
+
environment: test
|
|
55
|
+
#{events_table? ? "2. bin/rails db:migrate" : "2. Deduplicate deliveries on waffo_event[\"id\"] in WaffoWebhooksController"}
|
|
56
|
+
3. In the Waffo Dashboard, add a webhook pointing at https://<your host>/webhooks/waffo
|
|
57
|
+
and subscribe to the events you handle.
|
|
58
|
+
TEXT
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
def events_table? = options[:events_table]
|
|
63
|
+
|
|
64
|
+
def migration_version
|
|
65
|
+
"[#{::ActiveRecord::Migration.current_version}]" if defined?(::ActiveRecord::Migration)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateWaffoWebhookEvents < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :waffo_webhook_events do |t|
|
|
4
|
+
t.string :delivery_id, null: false
|
|
5
|
+
t.string :event_type, null: false
|
|
6
|
+
t.string :mode, null: false
|
|
7
|
+
t.string :order_id
|
|
8
|
+
t.json :payload, null: false
|
|
9
|
+
t.datetime :processed_at
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :waffo_webhook_events, :delivery_id, unique: true
|
|
14
|
+
add_index :waffo_webhook_events, :order_id
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Waffo Pancake (the waffo-pancake gem). Settings are read in this order, the last one winning:
|
|
2
|
+
# WAFFO_* environment variables, credentials.waffo, config.waffo_pancake, then this block.
|
|
3
|
+
#
|
|
4
|
+
# # bin/rails credentials:edit
|
|
5
|
+
# waffo:
|
|
6
|
+
# merchant_id: MER_...
|
|
7
|
+
# private_key: "-----BEGIN PRIVATE KEY-----\n..." # PEM, or bare base64
|
|
8
|
+
# store_id: STO_...
|
|
9
|
+
# environment: test # or prod
|
|
10
|
+
#
|
|
11
|
+
# Waffo::Pancake.client is a client built from these settings; webhook deliveries are verified
|
|
12
|
+
# by Waffo::Pancake::WebhookController. Requests are reported as `request.waffo_pancake` and
|
|
13
|
+
# webhook checks as `verify_webhook.waffo_pancake` ActiveSupport::Notifications events.
|
|
14
|
+
Waffo::Pancake.configure do |config|
|
|
15
|
+
# config.environment = Rails.env.production? ? "prod" : "test"
|
|
16
|
+
# config.timeout = 15
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# One verified Waffo Pancake delivery. The unique delivery_id makes a redelivery a no-op.
|
|
2
|
+
class WaffoWebhookEvent < ApplicationRecord
|
|
3
|
+
# Returns [event, created?]; two deliveries racing on the unique index share the one row.
|
|
4
|
+
def self.record(event)
|
|
5
|
+
attributes = {
|
|
6
|
+
event_type: event["eventType"].to_s,
|
|
7
|
+
mode: event["mode"].to_s,
|
|
8
|
+
order_id: event.dig("data", "orderId"),
|
|
9
|
+
payload: event
|
|
10
|
+
}
|
|
11
|
+
[ create!(delivery_id: event["id"].to_s, **attributes), true ]
|
|
12
|
+
rescue ActiveRecord::RecordNotUnique
|
|
13
|
+
[ find_by!(delivery_id: event["id"].to_s), false ]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def data = payload.fetch("data", {})
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Handles one Waffo Pancake delivery (WaffoWebhooksController records it first). Event types:
|
|
2
|
+
# https://docs.waffo.ai — Waffo::Pancake::Webhook::EVENT_TYPES lists them.
|
|
3
|
+
class WaffoWebhookJob < ApplicationJob
|
|
4
|
+
retry_on Waffo::Pancake::Unavailable, wait: :polynomially_longer, attempts: 8
|
|
5
|
+
|
|
6
|
+
def perform(event)
|
|
7
|
+
return if event.processed_at
|
|
8
|
+
|
|
9
|
+
case event.event_type
|
|
10
|
+
when "subscription.activated", "subscription.renewed", "subscription.recovered", "subscription.uncanceled"
|
|
11
|
+
# Grant access until event.data["currentPeriodEnd"]. To read the order as Waffo has it
|
|
12
|
+
# now, regardless of delivery order: Waffo::Pancake.client.subscription_order(event.order_id)
|
|
13
|
+
when "subscription.canceling"
|
|
14
|
+
# Access continues until event.data["currentPeriodEnd"]; no renewal follows.
|
|
15
|
+
when "subscription.canceled"
|
|
16
|
+
# Revoke access.
|
|
17
|
+
when "subscription.past_due"
|
|
18
|
+
# The renewal failed; the payment channel retries once, usually the next day.
|
|
19
|
+
when "refund.succeeded"
|
|
20
|
+
# Compare event.data["refundedAmount"] with event.data["originalChargedAmount"].
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
event.update!(processed_at: Time.current)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Receives Waffo Pancake webhooks. The included concern has already verified the signature,
|
|
2
|
+
# the environment and the store before `create` runs; `waffo_event` is the parsed delivery.
|
|
3
|
+
class WaffoWebhooksController < ActionController::Base
|
|
4
|
+
include Waffo::Pancake::WebhookController
|
|
5
|
+
|
|
6
|
+
def create
|
|
7
|
+
<% if events_table? -%>
|
|
8
|
+
event, created = WaffoWebhookEvent.record(waffo_event)
|
|
9
|
+
WaffoWebhookJob.perform_later(event) if created
|
|
10
|
+
<% else -%>
|
|
11
|
+
# A retry redelivers the same waffo_event["id"]; handle each id once.
|
|
12
|
+
Rails.logger.info("Waffo #{waffo_event["eventType"]} #{waffo_event["id"]}")
|
|
13
|
+
<% end -%>
|
|
14
|
+
head :ok
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/waffo/pancake/client.rb
CHANGED
|
@@ -30,6 +30,7 @@ module Waffo
|
|
|
30
30
|
subscriptionOrder(id: $id) {
|
|
31
31
|
id storeId status testMode buyerEmail merchantProvidedBuyerIdentity orderMerchantExternalId
|
|
32
32
|
billingPeriod currentPeriodStart currentPeriodEnd canceledAt
|
|
33
|
+
subscriptionProduct { id }
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
GRAPHQL
|
|
@@ -44,11 +45,14 @@ module Waffo
|
|
|
44
45
|
Base64.strict_encode64(private_key.sign(OpenSSL::Digest::SHA256.new, canonical))
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
# Every argument defaults to Waffo::Pancake.configuration (itself defaulting to the
|
|
49
|
+
# WAFFO_* environment variables). `transport` replaces the HTTP call: it takes the URI,
|
|
50
|
+
# the headers and the JSON body and answers `[status, body]` (status nil when there was
|
|
51
|
+
# no answer). `logger` gets one info line per request and `instrumenter` one
|
|
52
|
+
# `request.waffo_pancake` event; neither ever sees the body or the key.
|
|
53
|
+
def initialize(config: Pancake.configuration, merchant_id: config.merchant_id, private_key: config.private_key,
|
|
54
|
+
base_url: config.base_url, timeout: config.timeout, logger: config.logger,
|
|
55
|
+
instrumenter: config.instrumenter, transport: nil)
|
|
52
56
|
raise ConfigurationError, "Missing merchant_id (WAFFO_MERCHANT_ID)" if blank?(merchant_id)
|
|
53
57
|
raise ConfigurationError, "Missing private_key (WAFFO_PRIVATE_KEY)" if blank?(private_key)
|
|
54
58
|
|
|
@@ -57,6 +61,7 @@ module Waffo
|
|
|
57
61
|
@base_url = base_url.to_s.chomp("/")
|
|
58
62
|
@timeout = timeout
|
|
59
63
|
@logger = logger
|
|
64
|
+
@instrumenter = instrumenter
|
|
60
65
|
@transport = transport || method(:http_post)
|
|
61
66
|
end
|
|
62
67
|
|
|
@@ -105,9 +110,11 @@ module Waffo
|
|
|
105
110
|
action("/v1/actions/onetime-order/cancel-order", { order_id: order_id })
|
|
106
111
|
end
|
|
107
112
|
|
|
108
|
-
# Nil when Waffo knows no such order.
|
|
113
|
+
# Nil when Waffo knows no such order. `productId` is the order's current product, so a plan
|
|
114
|
+
# change to another product can be told apart.
|
|
109
115
|
def subscription_order(order_id)
|
|
110
|
-
graphql(SUBSCRIPTION_ORDER_QUERY, id: order_id)&.dig("subscriptionOrder")
|
|
116
|
+
order = graphql(SUBSCRIPTION_ORDER_QUERY, id: order_id)&.dig("subscriptionOrder")
|
|
117
|
+
order && order.merge("productId" => order.dig("subscriptionProduct", "id"))
|
|
111
118
|
end
|
|
112
119
|
|
|
113
120
|
# --- Products ----------------------------------------------------------------------
|
|
@@ -174,7 +181,9 @@ module Waffo
|
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
177
|
-
status, body =
|
|
184
|
+
status, body = instrument("request.waffo_pancake", method: "POST", path: path) do |payload|
|
|
185
|
+
call_transport(URI("#{@base_url}#{path}"), headers, json).tap { |answer| payload[:status] = answer.first }
|
|
186
|
+
end
|
|
178
187
|
elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).round(2)
|
|
179
188
|
@logger&.info("[Waffo] POST #{path} status=#{status.inspect} in #{elapsed}s")
|
|
180
189
|
|
|
@@ -186,6 +195,12 @@ module Waffo
|
|
|
186
195
|
[status, envelope]
|
|
187
196
|
end
|
|
188
197
|
|
|
198
|
+
def instrument(name, payload, &block)
|
|
199
|
+
return yield(payload) unless @instrumenter
|
|
200
|
+
|
|
201
|
+
@instrumenter.instrument(name, payload, &block)
|
|
202
|
+
end
|
|
203
|
+
|
|
189
204
|
def call_transport(uri, headers, json)
|
|
190
205
|
@transport.call(uri, headers, json)
|
|
191
206
|
rescue *NETWORK_ERRORS => e
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Waffo
|
|
4
|
+
module Pancake
|
|
5
|
+
# Process-wide settings, read by `Waffo::Pancake.client`, `Waffo::Pancake.verify_webhook`
|
|
6
|
+
# and the Rails integration. Every value starts from its environment variable; the Railtie
|
|
7
|
+
# then applies `credentials.waffo`, and `Waffo::Pancake.configure` has the last word.
|
|
8
|
+
class Configuration
|
|
9
|
+
ENVIRONMENTS = %w[test prod].freeze
|
|
10
|
+
|
|
11
|
+
# MER_..., the API key's private key (PEM, PEM with literal \n, or bare base64), STO_...
|
|
12
|
+
attr_accessor :merchant_id, :private_key, :store_id
|
|
13
|
+
attr_accessor :base_url, :timeout, :logger
|
|
14
|
+
# A key used instead of the built-in platform keys; nil uses those.
|
|
15
|
+
attr_accessor :webhook_public_key
|
|
16
|
+
# Seconds a webhook timestamp may lie in the past (0 skips the check) or in the future.
|
|
17
|
+
attr_accessor :webhook_tolerance, :webhook_future_tolerance
|
|
18
|
+
# Anything answering `instrument(name, payload) { |payload| ... }`. Defaults to
|
|
19
|
+
# ActiveSupport::Notifications when it is loaded; nil turns instrumentation off.
|
|
20
|
+
attr_writer :instrumenter
|
|
21
|
+
|
|
22
|
+
# "test" or "prod": which platform key verifies webhooks, and which deliveries the Rails
|
|
23
|
+
# webhook concern accepts. nil tries prod, then test, and accepts both.
|
|
24
|
+
attr_reader :environment
|
|
25
|
+
|
|
26
|
+
def initialize
|
|
27
|
+
@merchant_id = ENV["WAFFO_MERCHANT_ID"]
|
|
28
|
+
@private_key = ENV["WAFFO_PRIVATE_KEY"]
|
|
29
|
+
@store_id = ENV["WAFFO_STORE_ID"]
|
|
30
|
+
begin
|
|
31
|
+
self.environment = ENV["WAFFO_ENVIRONMENT"]
|
|
32
|
+
rescue ConfigurationError => e
|
|
33
|
+
raise ConfigurationError, "WAFFO_ENVIRONMENT: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
@base_url = ENV["WAFFO_API_BASE_URL"] || Client::DEFAULT_BASE_URL
|
|
36
|
+
@timeout = Client::DEFAULT_TIMEOUT
|
|
37
|
+
@logger = nil
|
|
38
|
+
@webhook_public_key = nil
|
|
39
|
+
@webhook_tolerance = Webhook::DEFAULT_TOLERANCE
|
|
40
|
+
@webhook_future_tolerance = Webhook::DEFAULT_FUTURE_TOLERANCE
|
|
41
|
+
@instrumenter = :default
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def environment=(value)
|
|
45
|
+
value = value.to_s.strip.downcase
|
|
46
|
+
value = nil if value.empty?
|
|
47
|
+
unless value.nil? || ENVIRONMENTS.include?(value)
|
|
48
|
+
raise ConfigurationError, "environment must be test or prod, got #{value.inspect}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@environment = value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def instrumenter
|
|
55
|
+
return @instrumenter unless @instrumenter == :default
|
|
56
|
+
|
|
57
|
+
defined?(::ActiveSupport::Notifications) ? ::ActiveSupport::Notifications : nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# What a client needs; a store id is only checked by the webhook concern.
|
|
61
|
+
def configured?
|
|
62
|
+
!blank?(merchant_id) && !blank?(private_key)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
def blank?(value) = value.nil? || value.to_s.strip.empty?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@mutex = Mutex.new
|
|
70
|
+
|
|
71
|
+
class << self
|
|
72
|
+
def configuration
|
|
73
|
+
@configuration || @mutex.synchronize { @configuration ||= Configuration.new }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Yields the configuration; the shared client is rebuilt on its next use.
|
|
77
|
+
def configure
|
|
78
|
+
yield configuration
|
|
79
|
+
reset_client!
|
|
80
|
+
configuration
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A client built from the configuration, shared by the process. Raises
|
|
84
|
+
# ConfigurationError while the merchant id or the key is missing.
|
|
85
|
+
def client
|
|
86
|
+
return @client if @client
|
|
87
|
+
|
|
88
|
+
config = configuration
|
|
89
|
+
@mutex.synchronize { @client ||= Client.new(config: config) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Replaces the shared client, for example with Waffo::Pancake::TestHelpers::FakeClient.
|
|
93
|
+
attr_writer :client
|
|
94
|
+
|
|
95
|
+
def reset_client!
|
|
96
|
+
@mutex.synchronize { @client = nil }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Back to the environment variables, with no shared client.
|
|
100
|
+
def reset!
|
|
101
|
+
@mutex.synchronize do
|
|
102
|
+
@configuration = nil
|
|
103
|
+
@client = nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Webhook.verify with the configured environment, key and tolerances, reported as a
|
|
108
|
+
# `verify_webhook.waffo_pancake` event (with `:exception` when it is refused).
|
|
109
|
+
def verify_webhook(payload, signature_header, now: Time.now)
|
|
110
|
+
config = configuration
|
|
111
|
+
verify = lambda do |event_payload|
|
|
112
|
+
event = Webhook.verify(payload, signature_header, environment: config.environment,
|
|
113
|
+
public_key: config.webhook_public_key,
|
|
114
|
+
tolerance: config.webhook_tolerance,
|
|
115
|
+
future_tolerance: config.webhook_future_tolerance, now: now)
|
|
116
|
+
event_payload.merge!(event_id: event["id"], event_type: event["eventType"], mode: event["mode"]) if event.is_a?(Hash)
|
|
117
|
+
event
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
instrumenter = config.instrumenter
|
|
121
|
+
details = { environment: config.environment }
|
|
122
|
+
instrumenter ? instrumenter.instrument("verify_webhook.waffo_pancake", details, &verify) : verify.call(details)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module Waffo
|
|
6
|
+
module Pancake
|
|
7
|
+
# Loaded only inside a Rails app. Settings are applied in this order, the last one winning:
|
|
8
|
+
#
|
|
9
|
+
# 1. WAFFO_* environment variables
|
|
10
|
+
# 2. credentials.waffo (merchant_id, private_key, store_id, environment, webhook_public_key)
|
|
11
|
+
# 3. config.waffo_pancake.* in config/application.rb or an environment file
|
|
12
|
+
# 4. Waffo::Pancake.configure in config/initializers
|
|
13
|
+
#
|
|
14
|
+
# The logger defaults to Rails.logger, and `private_key` is filtered from logs.
|
|
15
|
+
class Railtie < ::Rails::Railtie
|
|
16
|
+
CREDENTIAL_KEYS = %i[merchant_id private_key store_id environment webhook_public_key].freeze
|
|
17
|
+
|
|
18
|
+
config.waffo_pancake = ActiveSupport::OrderedOptions.new
|
|
19
|
+
|
|
20
|
+
initializer "waffo_pancake.filter_parameters" do |app|
|
|
21
|
+
app.config.filter_parameters |= [:private_key]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
initializer "waffo_pancake.configuration" do |app|
|
|
25
|
+
Waffo::Pancake.configure do |config|
|
|
26
|
+
credentials = app.credentials[:waffo] if app.respond_to?(:credentials)
|
|
27
|
+
CREDENTIAL_KEYS.each do |key|
|
|
28
|
+
value = credentials&.[](key)
|
|
29
|
+
config.public_send(:"#{key}=", value) unless value.nil? || value.to_s.strip.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
app.config.waffo_pancake.each { |key, value| config.public_send(:"#{key}=", value) }
|
|
33
|
+
config.logger ||= ::Rails.logger
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
require "waffo/pancake"
|
|
6
|
+
|
|
7
|
+
module Waffo
|
|
8
|
+
module Pancake
|
|
9
|
+
# Helpers for an app's own tests (Minitest or RSpec):
|
|
10
|
+
#
|
|
11
|
+
# # test/test_helper.rb # spec/rails_helper.rb
|
|
12
|
+
# require "waffo/pancake/test_helpers" require "waffo/pancake/test_helpers"
|
|
13
|
+
# class ActiveSupport::TestCase RSpec.configure do |config|
|
|
14
|
+
# include Waffo::Pancake::TestHelpers config.include Waffo::Pancake::TestHelpers
|
|
15
|
+
# end end
|
|
16
|
+
#
|
|
17
|
+
# `with_waffo_webhook_key` makes the configuration trust a throwaway key, and
|
|
18
|
+
# `waffo_webhook_headers` signs a body with it, so a request test can post a delivery the
|
|
19
|
+
# real verification accepts. `with_fake_waffo_client` swaps the shared client for a
|
|
20
|
+
# FakeClient, which records calls instead of making them.
|
|
21
|
+
module TestHelpers
|
|
22
|
+
# Generated once per process: RSA key generation is slow.
|
|
23
|
+
def self.key
|
|
24
|
+
@key ||= OpenSSL::PKey::RSA.generate(2048)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Webhook verification trusts TestHelpers.key (and only it) inside the block.
|
|
28
|
+
def with_waffo_webhook_key(key = TestHelpers.key)
|
|
29
|
+
config = Waffo::Pancake.configuration
|
|
30
|
+
previous = config.webhook_public_key
|
|
31
|
+
config.webhook_public_key = key.public_key
|
|
32
|
+
yield
|
|
33
|
+
ensure
|
|
34
|
+
config.webhook_public_key = previous
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# `t=<ms>,v1=<signature>` over the body, as Waffo sends it.
|
|
38
|
+
def waffo_webhook_signature(body, at: Time.now, key: TestHelpers.key)
|
|
39
|
+
timestamp = (at.to_f * 1000).to_i
|
|
40
|
+
signature = Base64.strict_encode64(key.sign(OpenSSL::Digest::SHA256.new, "#{timestamp}.#{body}"))
|
|
41
|
+
"t=#{timestamp},v1=#{signature}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Headers for posting `body` (a String) to a webhook endpoint.
|
|
45
|
+
def waffo_webhook_headers(body, **options)
|
|
46
|
+
{ "CONTENT_TYPE" => "application/json", Webhook::SIGNATURE_HEADER => waffo_webhook_signature(body, **options) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A webhook envelope in Waffo's shape. `mode` and `store_id` default to the configuration.
|
|
50
|
+
def waffo_webhook_event(event_type, data: {}, id: SecureRandom.uuid, mode: nil, store_id: nil)
|
|
51
|
+
config = Waffo::Pancake.configuration
|
|
52
|
+
{
|
|
53
|
+
"id" => id,
|
|
54
|
+
"timestamp" => Time.now.utc.iso8601,
|
|
55
|
+
"eventType" => event_type,
|
|
56
|
+
"eventId" => "evt_#{SecureRandom.hex(8)}",
|
|
57
|
+
"storeId" => store_id || config.store_id || "STO_test",
|
|
58
|
+
"storeName" => "Test store",
|
|
59
|
+
"mode" => mode || config.environment || "test",
|
|
60
|
+
"data" => data
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Waffo::Pancake.client is `client` inside the block. Yields the client.
|
|
65
|
+
def with_fake_waffo_client(client = FakeClient.new)
|
|
66
|
+
previous = Waffo::Pancake.instance_variable_get(:@client)
|
|
67
|
+
Waffo::Pancake.client = client
|
|
68
|
+
yield client
|
|
69
|
+
ensure
|
|
70
|
+
Waffo::Pancake.client = previous
|
|
71
|
+
end
|
|
72
|
+
|
|
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.
|
|
76
|
+
class FakeClient
|
|
77
|
+
attr_reader :orders, :calls
|
|
78
|
+
attr_accessor :fail_with, :checkout_url
|
|
79
|
+
|
|
80
|
+
def initialize(orders = {})
|
|
81
|
+
@orders = orders.transform_keys(&:to_s)
|
|
82
|
+
@calls = []
|
|
83
|
+
@checkout_url = "https://pancake.waffo.ai/store/test/checkout/cs_test"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def subscription_order(order_id)
|
|
87
|
+
record(:subscription_order, order_id)
|
|
88
|
+
order = @orders[order_id.to_s]
|
|
89
|
+
order && order.dup
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def create_checkout_session(**params)
|
|
93
|
+
record(:create_checkout_session, params)
|
|
94
|
+
{ "sessionId" => "cs_test", "checkoutUrl" => checkout_url, "expiresAt" => (Time.now + 2700).utc.iso8601 }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def create_authenticated_checkout(**params)
|
|
98
|
+
record(:create_authenticated_checkout, params)
|
|
99
|
+
{ "sessionId" => "cs_test", "checkoutUrl" => "#{checkout_url}#token=test_token", "token" => "test_token",
|
|
100
|
+
"expiresAt" => (Time.now + 2700).utc.iso8601 }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def cancel_subscription(order_id)
|
|
104
|
+
record(:cancel_subscription, order_id)
|
|
105
|
+
change_status(order_id, "canceling")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def reactivate_subscription(order_id)
|
|
109
|
+
record(:reactivate_subscription, order_id)
|
|
110
|
+
change_status(order_id, "active")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def graphql(query, variables = {})
|
|
114
|
+
record(:graphql, query, variables)
|
|
115
|
+
{}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def action(path, params)
|
|
119
|
+
record(:action, path, params)
|
|
120
|
+
{}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def called?(name) = calls.any? { |call| call.first == name }
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
def record(name, *arguments)
|
|
127
|
+
calls << [name, *arguments]
|
|
128
|
+
raise fail_with if fail_with
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def change_status(order_id, status)
|
|
132
|
+
@orders[order_id.to_s]&.merge!("status" => status)
|
|
133
|
+
{ "orderId" => order_id, "status" => status }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Waffo
|
|
6
|
+
module Pancake
|
|
7
|
+
# Include in the controller that receives Waffo's webhooks:
|
|
8
|
+
#
|
|
9
|
+
# class WaffoWebhooksController < ActionController::Base
|
|
10
|
+
# include Waffo::Pancake::WebhookController
|
|
11
|
+
#
|
|
12
|
+
# def create
|
|
13
|
+
# HandleWaffoEventJob.perform_later(waffo_event) # already verified
|
|
14
|
+
# head :ok
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Before the action it reads the raw body, verifies X-Waffo-Signature with
|
|
19
|
+
# Waffo::Pancake.verify_webhook and exposes the event as `waffo_event`. A bad signature is
|
|
20
|
+
# answered 401 and an oversized body 413, without reaching the action. A delivery for
|
|
21
|
+
# another environment or store (when `environment` / `store_id` are configured) is
|
|
22
|
+
# acknowledged with 200 and skipped, since an error status only makes Waffo retry it.
|
|
23
|
+
#
|
|
24
|
+
# Deduplicate on `waffo_event["id"]`: a retry redelivers the same id.
|
|
25
|
+
module WebhookController
|
|
26
|
+
extend ActiveSupport::Concern
|
|
27
|
+
|
|
28
|
+
MAX_BODY_BYTES = 256 * 1024
|
|
29
|
+
|
|
30
|
+
included do
|
|
31
|
+
skip_forgery_protection if respond_to?(:skip_forgery_protection)
|
|
32
|
+
wrap_parameters false if respond_to?(:wrap_parameters)
|
|
33
|
+
before_action :verify_waffo_webhook!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The body is trusted only once its signature checks out. Left to Rails it would be
|
|
37
|
+
# parsed into params first and written whole into the request log.
|
|
38
|
+
def process_action(...)
|
|
39
|
+
request.delete_header("action_dispatch.request.parameters")
|
|
40
|
+
request.request_parameters = {}
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
attr_reader :waffo_event
|
|
46
|
+
|
|
47
|
+
def verify_waffo_webhook!
|
|
48
|
+
body = request.body.read(MAX_BODY_BYTES + 1).to_s
|
|
49
|
+
request.body.rewind if request.body.respond_to?(:rewind)
|
|
50
|
+
return head(413) if body.bytesize > MAX_BODY_BYTES
|
|
51
|
+
|
|
52
|
+
@waffo_event = Waffo::Pancake.verify_webhook(body, request.headers[Webhook::SIGNATURE_HEADER])
|
|
53
|
+
return head(:bad_request) unless @waffo_event.is_a?(Hash) && @waffo_event["id"]
|
|
54
|
+
return if waffo_event_for_this_account?
|
|
55
|
+
|
|
56
|
+
waffo_webhook_log(:info, "skipped event=#{@waffo_event["id"]} mode=#{@waffo_event["mode"]} " \
|
|
57
|
+
"store=#{@waffo_event["storeId"]}: not this environment or store")
|
|
58
|
+
head :ok
|
|
59
|
+
rescue Waffo::Pancake::InvalidSignature => e
|
|
60
|
+
waffo_webhook_log(:warn, "refused: #{e.message}")
|
|
61
|
+
head :unauthorized
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def waffo_event_for_this_account?
|
|
65
|
+
config = Waffo::Pancake.configuration
|
|
66
|
+
return false if config.environment && waffo_event["mode"] != config.environment
|
|
67
|
+
|
|
68
|
+
config.store_id.to_s.strip.empty? || waffo_event["storeId"] == config.store_id
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def waffo_webhook_log(level, message)
|
|
72
|
+
(Waffo::Pancake.configuration.logger || logger)&.public_send(level, "[Waffo] webhook #{message}")
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/waffo/pancake.rb
CHANGED
|
@@ -5,9 +5,14 @@ require_relative "pancake/errors"
|
|
|
5
5
|
require_relative "pancake/keys"
|
|
6
6
|
require_relative "pancake/client"
|
|
7
7
|
require_relative "pancake/webhook"
|
|
8
|
+
require_relative "pancake/configuration"
|
|
9
|
+
require_relative "pancake/railtie" if defined?(Rails::Railtie)
|
|
8
10
|
|
|
9
11
|
module Waffo
|
|
10
12
|
# Ruby client for the Waffo Pancake merchant API (https://docs.waffo.ai).
|
|
11
13
|
module Pancake
|
|
14
|
+
# Need ActiveSupport / Action Pack; loaded on first use.
|
|
15
|
+
autoload :WebhookController, "waffo/pancake/webhook_controller"
|
|
16
|
+
autoload :TestHelpers, "waffo/pancake/test_helpers"
|
|
12
17
|
end
|
|
13
18
|
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.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Good First Issue
|
|
@@ -34,12 +34,22 @@ files:
|
|
|
34
34
|
- CHANGELOG.md
|
|
35
35
|
- LICENSE
|
|
36
36
|
- README.md
|
|
37
|
+
- lib/generators/waffo_pancake/install/install_generator.rb
|
|
38
|
+
- lib/generators/waffo_pancake/install/templates/create_waffo_webhook_events.rb.tt
|
|
39
|
+
- lib/generators/waffo_pancake/install/templates/initializer.rb.tt
|
|
40
|
+
- lib/generators/waffo_pancake/install/templates/webhook_event.rb.tt
|
|
41
|
+
- lib/generators/waffo_pancake/install/templates/webhook_job.rb.tt
|
|
42
|
+
- lib/generators/waffo_pancake/install/templates/webhooks_controller.rb.tt
|
|
37
43
|
- lib/waffo/pancake.rb
|
|
38
44
|
- lib/waffo/pancake/client.rb
|
|
45
|
+
- lib/waffo/pancake/configuration.rb
|
|
39
46
|
- lib/waffo/pancake/errors.rb
|
|
40
47
|
- lib/waffo/pancake/keys.rb
|
|
48
|
+
- lib/waffo/pancake/railtie.rb
|
|
49
|
+
- lib/waffo/pancake/test_helpers.rb
|
|
41
50
|
- lib/waffo/pancake/version.rb
|
|
42
51
|
- lib/waffo/pancake/webhook.rb
|
|
52
|
+
- lib/waffo/pancake/webhook_controller.rb
|
|
43
53
|
homepage: https://github.com/goodfirstissueorg/waffo-pancake-ruby
|
|
44
54
|
licenses:
|
|
45
55
|
- MIT
|
|
@@ -63,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
63
73
|
- !ruby/object:Gem::Version
|
|
64
74
|
version: '0'
|
|
65
75
|
requirements: []
|
|
66
|
-
rubygems_version:
|
|
76
|
+
rubygems_version: 3.6.9
|
|
67
77
|
specification_version: 4
|
|
68
78
|
summary: Ruby client for the Waffo Pancake merchant API
|
|
69
79
|
test_files: []
|