sm_sms_campaign_webhook 0.1.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +243 -4
  4. data/app/controllers/sm_sms_campaign_webhook/application_controller.rb +22 -0
  5. data/app/controllers/sm_sms_campaign_webhook/webhook_controller.rb +25 -0
  6. data/app/exceptions/sm_sms_campaign_webhook/error.rb +6 -0
  7. data/app/exceptions/sm_sms_campaign_webhook/invalid_payload.rb +6 -0
  8. data/app/exceptions/sm_sms_campaign_webhook/invalid_payload_value.rb +6 -0
  9. data/app/exceptions/sm_sms_campaign_webhook/missing_config_error.rb +6 -0
  10. data/app/exceptions/sm_sms_campaign_webhook/payload_dispatch_error.rb +6 -0
  11. data/app/jobs/sm_sms_campaign_webhook/application_job.rb +7 -0
  12. data/app/jobs/sm_sms_campaign_webhook/dispatch_payload_job.rb +14 -0
  13. data/app/jobs/sm_sms_campaign_webhook/process_campaign_engagement_job.rb +14 -0
  14. data/app/models/sm_sms_campaign_webhook/campaign_engagement.rb +216 -0
  15. data/app/models/sm_sms_campaign_webhook/campaign_engagement/answer.rb +72 -0
  16. data/app/operations/sm_sms_campaign_webhook/campaign_engagement_operation.rb +36 -0
  17. data/app/operations/sm_sms_campaign_webhook/payload_operation.rb +26 -0
  18. data/app/processors/sm_sms_campaign_webhook/default_processor.rb +11 -0
  19. data/app/processors/sm_sms_campaign_webhook/processable.rb +21 -0
  20. data/config/routes.rb +7 -0
  21. data/lib/generators/sm_sms_campaign_webhook/install/USAGE +12 -0
  22. data/lib/generators/sm_sms_campaign_webhook/install/install_generator.rb +35 -0
  23. data/lib/generators/sm_sms_campaign_webhook/install/templates/README +25 -0
  24. data/lib/generators/sm_sms_campaign_webhook/install/templates/sm_sms_campaign_webhook.rb +9 -0
  25. data/lib/generators/sm_sms_campaign_webhook/install/templates/sms_payload_processor.rb.erb +13 -0
  26. data/lib/sm_sms_campaign_webhook.rb +25 -2
  27. data/lib/sm_sms_campaign_webhook/engine.rb +11 -0
  28. data/lib/sm_sms_campaign_webhook/version.rb +1 -1
  29. data/sm_sms_campaign_webhook.gemspec +27 -19
  30. metadata +66 -16
  31. data/.gitignore +0 -16
  32. data/.rspec +0 -3
  33. data/.travis.yml +0 -18
  34. data/Gemfile +0 -4
  35. data/Rakefile +0 -6
  36. data/bin/console +0 -14
  37. data/bin/setup +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfbcb58c24ec5e86d522e8bbd6523bdf0c348c562aa04ac33fc610860de7fbb2
4
- data.tar.gz: d23266b0bd2ba7b5f65e0ad24d4ca34eb35059b224217ad3f93f566785e1da04
3
+ metadata.gz: 9ee7507ed2364d7482af89dc6a0fda95b689ffe0c9e12f12b4a8cd8f1edf07d0
4
+ data.tar.gz: a0a7d6b50a54b6d518d5ef870727217b7c3cee1816ef1038cab61e6f5d506907
5
5
  SHA512:
6
- metadata.gz: ae0eb7921ee8ef9b295427c8333627c3938281977a97cc08ca10eefd6363c11699014aa3cbd987b05ced256760a758e909970708d1d9d6ea2481e83320d0778a
7
- data.tar.gz: 380100e28e9a8a4646fc61c132afe8824088947e378bfee1ef08785e48dc983d71fa39330d4d93b941cdf9cc2a9cfbd6fd4e4b4e84aa32de2d32dd8926aaaf3d
6
+ metadata.gz: cb4b43bfc4733b09458fea2a6c008f33a3a271918ae16cecd66571b6f97bc221f67748c8f5b9b7ef6ce811232ac96b5ce44433da6cb64d91083979b625f4a8f2
7
+ data.tar.gz: b078c3040867520148ad9e129a6845100c82d277a4e68b60beb8b51495876c2915251c13f208a0e3326f14dbe64c46e4355077a95719d4e5abcefe58569f55b9
@@ -6,6 +6,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.0.0] - 2019-07-26
10
+ ### Added
11
+ - Mountable Rails engine as API
12
+ - POST /api/webhook resource requiring JSON payload for asynchronous dispatching and processing
13
+ - Require inbound POST requests be authorization requests with an auth token
14
+ - Support for Rails 5.2.x, 6.0.x
15
+ - Data models for campaign engagement event payloads
16
+ - Helper method to get specific campaign engagement answer
17
+ - Payload operation library to dispatch and process deserialized JSON with supported event data modeling
18
+ - Processable behavior definition for app implementors
19
+ - Default processor mixing in processable behavior with noisy errors
20
+ - ActiveJob library for asynchronous handling of payload dispatching and processing
21
+ - Configuration support for required app implementation values
22
+
23
+ ### Changed
24
+ - Require Ruby >= 2.5
25
+ - CI to test against Ruby 2.5.5, 2.6.3
26
+ - CI to test against Rails 5.2.x, 6.0.x
27
+
28
+ ## [0.1.1] - 2019-07-16
29
+ ### Changed
30
+ - Changelog URI to correct value in gemspec
31
+
9
32
  ## [0.1.0] - 2019-07-16
10
33
  ### Added
11
34
  - NOOP gem configured for development
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
- # SmSmsCampaignWebhook: Middleware providing webhook for Southern Made SMS Campaign Engagement
1
+ # SmSmsCampaignWebhook
2
+
3
+ ![Southern Made - Galaxy Logo](https://raw.github.com/SouthernMade/sm_sms_campaign_webhook/develop/logo_galaxymark.png) by [Southern Made](https://www.southernmade.com/)
2
4
 
3
5
  [![Gem Version](https://badge.fury.io/rb/sm_sms_campaign_webhook.svg)](https://badge.fury.io/rb/sm_sms_campaign_webhook)
4
6
  [![Travis Build Status](https://travis-ci.org/SouthernMade/sm_sms_campaign_webhook.svg?branch=develop)](https://travis-ci.org/SouthernMade/sm_sms_campaign_webhook)
@@ -15,12 +17,36 @@ This gem will help app implementors using [Rails](https://rubyonrails.org) setup
15
17
 
16
18
  Work closely with your Southern Made project manager to gather details about what needs to be tracked, what fields to verify, and which scenarios are expected to be supported!
17
19
 
20
+ ## Table of Contents
21
+
22
+ - [Installation](#installation)
23
+ - [Configuration](#configuration)
24
+ - [Auto Generate Config](#auto-generate-config)
25
+ - [Webhook Auth Token](#webhook-auth-token)
26
+ - [ActiveJob](#activejob)
27
+ - [Mount the Webhook Engine](#mount-the-webhook-engine)
28
+ - [Webhook Initializer](#webhook-initializer)
29
+ - [Payload Processor](#payload-processor)
30
+ - [Usage](#usage)
31
+ - [Campaign Engagement](#campaign-engagement)
32
+ - [Processor Expections](#processor-expections)
33
+ - [Campaign Engagement Data Model](#campaign-engagement-data-model)
34
+ - [Campaign Engagement Answer Data Model](#campaign-engagement-answer-data-model)
35
+ - [Development](#development)
36
+ - [Versioning](#versioning)
37
+ - [Testing](#testing)
38
+ - [Documentation](#documentation)
39
+ - [Contributing](#contributing)
40
+ - [License](#license)
41
+
18
42
  ## Installation
19
43
 
44
+ This gem is tested with Rails 5.2.x, 6.0.x versions.
45
+
20
46
  Add this line to your application's Gemfile:
21
47
 
22
48
  ```ruby
23
- gem 'sm_sms_campaign_webhook'
49
+ gem 'sm_sms_campaign_webhook', '~> 1.0'
24
50
  ```
25
51
 
26
52
  And then execute:
@@ -31,9 +57,223 @@ Or install it yourself as:
31
57
 
32
58
  $ gem install sm_sms_campaign_webhook
33
59
 
60
+ ## Configuration
61
+
62
+ These are the steps to configure your app to be ready to capture SMS campaign service payloads.
63
+
64
+ ### Auto Generate Config
65
+
66
+ You can setup most app configuration by running the generator:
67
+
68
+ ```
69
+ $ bundle exec rails generate sm_sms_campaign_webhook:install
70
+ ```
71
+
72
+ Some things will still require manual configuration and will be identified after generation:
73
+
74
+ - [Webhook Auth Token](#webhook-auth-token)
75
+ - [ActiveJob](#activejob)
76
+
77
+ After that, be sure to read the [Usage](#usage) section for payload processor details!
78
+
79
+ If you prefer to setup everything by hand, be sure to check out:
80
+
81
+ - [Webhook Initializer](#webhook-initializer)
82
+ - [Payload Processor](#payload-processor)
83
+
84
+ ### Webhook Auth Token
85
+
86
+ The `SM_SMS_CAMPAIGN_WEBHOOK_AUTH_TOKEN` value is required to be an `ENV` value to avoid leaking production values. It will be used to authorize payload requests from the SMS campaign service.
87
+
88
+ Set this value using the rails secret generator:
89
+
90
+ ```
91
+ $ bundle exec rails secret
92
+ ```
93
+
94
+ And copy the result to your `.env` or applicable config file:
95
+
96
+ ```
97
+ SM_SMS_CAMPAIGN_WEBHOOK_AUTH_TOKEN="******"
98
+ ```
99
+
100
+ ### ActiveJob
101
+
102
+ Payloads will be dispatched and processed asynchronously using [ActiveJob](https://edgeguides.rubyonrails.org/active_job_basics.html). Southern Made prefers that the app be configured with [Sidekiq](https://github.com/mperham/sidekiq) as the queue adapter.
103
+
104
+ You can set the adapter in `config/application.rb` with:
105
+
106
+ ```ruby
107
+ class Application < Rails::Application
108
+ config.active_job.queue_adapter = :sidekiq
109
+ end
110
+ ```
111
+
112
+ Update your Procfile or appropriate config to launch worker processes:
113
+
114
+ ```
115
+ worker: RAILS_MAX_THREADS=${SIDEKIQ_CONCURRENCY:-5} bundle exec sidekiq --config config/sidekiq.yml
116
+ ```
117
+
118
+ More detailed instructions about using Sidekiq can be found in the [Sidekiq Wiki](https://github.com/mperham/sidekiq/wiki).
119
+
120
+ ### Mount the Webhook Engine
121
+
122
+ If you opted to [auto generate the config](#auto-generate-config), this can be skipped.
123
+
124
+ Add the following to `config/routes.rb` in your app to mount the webhook:
125
+
126
+ ```ruby
127
+ mount SmSmsCampaignWebhook::Engine => "/sms_campaign"
128
+ ```
129
+
130
+ This sets the app up to receive POST requests from the SMS campaign service:
131
+
132
+ POST /sms_campaign/api/webhook
133
+
134
+ Be sure to replace `/sms_campaign` with whatever mount point you choose. Once you share the webhook URI with your project manager, avoid changing it; they will configure it with the correspending SMS campaign!
135
+
136
+ ### Webhook Initializer
137
+
138
+ If you opted to [auto generate the config](#auto-generate-config), this can be skipped.
139
+
140
+ App implementors must configure some library options. Here are all supported configuration options identifying their default values for `config/initializers/sm_sms_campaign_webhook.rb`:
141
+
142
+ ```ruby
143
+ require "sm_sms_campaign_webhook"
144
+
145
+ SmSmsCampaignWebhook.config do |config|
146
+ # SMS campaign payload processor implementing SmSmsCampaignWebhook::Processable behavior.
147
+ # default: SmSmsCampaignWebhook::DefaultProcessor (raises errors for processing)
148
+ # config.processor = SmsPayloadProcessor
149
+ end
150
+ ```
151
+
152
+ ### Payload Processor
153
+
154
+ If you opted to [auto generate the config](#auto-generate-config), this can be skipped. However, you will still need to implement the processor methods!
155
+
156
+ The default payload processor will raise errors while processing. You are required to provide a working payload processor to properly handle the data received from the SMS campaign service.
157
+
158
+ To create a processor, create a custom class mixing in `SmSmsCampaignWebhook::Processable` behavior. For example, we can create a custom processor named `SmsPayloadProcessor`:
159
+
160
+ ```ruby
161
+ class SmsPayloadProcessor
162
+ include SmSmsCampaignWebhook::Processable
163
+
164
+ # Implement required methods for Processable behavior.
165
+
166
+ # @param campaign_engagement [SmSmsCampaignWebhook::CampaignEngagement]
167
+ # def self.process_campaign_engagement(campaign_engagement)
168
+ # # NOOP - I need to be implemented.
169
+ # end
170
+ end
171
+ ```
172
+
173
+ This class will continue raising errors until the required methods are implemented. Please see the [Processable mixin](https://github.com/SouthernMade/sm_sms_campaign_webhook/blob/develop/app/processors/sm_sms_campaign_webhook/processable.rb) for expected method definitions.
174
+
175
+ Finally the configuration needs to be updated to use the custom processor. Add this within the config block in `config/initializers/sm_sms_campaign_webhook.rb`:
176
+
177
+ ```ruby
178
+ SmSmsCampaignWebhook.config do |config|
179
+ #...
180
+ config.processor = SmsPayloadProcessor
181
+ #...
182
+ end
183
+ ```
184
+
34
185
  ## Usage
35
186
 
36
- Right now, nothing happens! Soon, some useful details will emerge about how to ingest the SMS campaign payloads.
187
+ The main goal is to ingest the data contained in payloads received from the SMS campaign service. Your app knows best what to do with the data, so your primary focus is implementing the required methods of a payload processor.
188
+
189
+ Assuming that you completed configuration with the [auto generate installer](#auto-generate-config) or [manually created a processor](#payload-processor), this section will expand what to do with it.
190
+
191
+ ### Campaign Engagement
192
+
193
+ This payload represents a user's phone interaction with the SMS campaign. This includes:
194
+
195
+ - First contact with a SMS campaign by keyword
196
+ - Responding to subsequent SMS campaign messages
197
+ - Continued engagement for multi-entry SMS campaigns
198
+
199
+ Payloads will POST to the webhook every time a phone interacts with the campaign, so the processor behavior should expect to see repeats of inbound payloads from a phone!
200
+
201
+ It is important that you work closely with your Southern Made project manager to determine which scenarios are relevant for your app. They will be able to tell you:
202
+
203
+ - Fields + value types that will be in answers
204
+ - Required fields to complete registration/entry
205
+ - How to interpret voting style numeric answers
206
+
207
+ #### Processor Expections
208
+
209
+ You must behavior for this method to ingest campaign engagement data in your paylod processor:
210
+
211
+ ```ruby
212
+ def self.process_campaign_engagement(campaign_engagement)
213
+ # ...
214
+ end
215
+ ```
216
+
217
+ It will receive an instance of the [SmSmsCampaignWebhook::CampaignEngagement](https://github.com/SouthernMade/sm_sms_campaign_webhook/blob/develop/app/models/sm_sms_campaign_webhook/campaign_engagement.rb) data model. This method will need to handle scenarios such as:
218
+
219
+ - Registering/creating a user account
220
+ - Logging registration/entries
221
+ - Interpreting + logging vote responses
222
+
223
+ Check with your Southern Made project manager for expectations.
224
+
225
+ #### Campaign Engagement Data Model
226
+
227
+ The payload will be modeled with the [SmSmsCampaignWebhook::CampaignEngagement](https://github.com/SouthernMade/sm_sms_campaign_webhook/blob/develop/app/models/sm_sms_campaign_webhook/campaign_engagement.rb) class. It provides basic methods to extract values out of the payload. The data model coerces values to the appropriate types in Ruby.
228
+
229
+ Some example message passing to an instance:
230
+
231
+ ```ruby
232
+ campaign_engagement.event_uuid # UUID - unique payload event
233
+ campaign_engagement.campaign_keyword # String - SMS campaign entry point
234
+ campaign_engagement.phone_id # Integer - represents specific phone
235
+ campaign_engagement.phone_number # String - phone number interacting with SMS campaign
236
+
237
+ # This represents a specific phone engaging with a specific campaign.
238
+ # The value will differ for each entry in multi-entry campaigns.
239
+ # For standard campaigns we will only see one value.
240
+ campaign_engagement.phone_campaign_state_id # Integer
241
+
242
+ # These values help determine if and when answers were received
243
+ # for all campaign messages.
244
+ campaign_engagement.phone_campaign_state_completed? # TrueClass,FalseClass
245
+ campaign_engagement.phone_campaign_state_completed_at # DateTime
246
+ ```
247
+
248
+ It also provides a useful helper methods related to campaign engagement answers. For example:
249
+
250
+ ```ruby
251
+ # Are any campaign engagement answers in the payload?
252
+ campaign_engagement.phone_campaign_state_answers? # TrueClass,FalseClass
253
+
254
+ # This tries to find an answer for the requested field.
255
+ # If a match is found it returns instance of
256
+ # SmSmsCampaignWebhook::CampaignEngagement::Answer data model.
257
+ # If a match is not found it return nil (NilClass).
258
+ campaign_enagement.answer_for(field: "email") # Returned type answer specific
259
+ ```
260
+
261
+ #### Campaign Engagement Answer Data Model
262
+
263
+ The [SmSmsCampaignWebhook::CampaignEngagement::Answer](https://github.com/SouthernMade/sm_sms_campaign_webhook/blob/develop/app/models/sm_sms_campaign_webhook/campaign_engagement/answer.rb) class models the answer data contained in the campaign engagement payload. It consists of:
264
+
265
+ - field (`String`)
266
+ - value (varies)
267
+ - collected_at (`DateTime`)
268
+
269
+ The value data types could be one of the following:
270
+
271
+ - string (`String`)
272
+ - email (`String`)
273
+ - date (`Date`)
274
+ - number (`Integer`)
275
+ - boolean (`TrueClass`, `FalseClass`)
276
+ - us_state (`String`)
37
277
 
38
278
  ## Development
39
279
 
@@ -43,7 +283,6 @@ This gem uses [git-flow](https://github.com/nvie/gitflow) to manage deployments.
43
283
 
44
284
  Gem versioning follows [Semantic Versioning](https://semver.org).
45
285
 
46
-
47
286
  ### Testing
48
287
 
49
288
  This project uses Rspec for testing. Specs must be green for any PR to be accepted!
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_controller/metal/http_authentication"
4
+ require "active_support/security_utils"
5
+
6
+ module SmSmsCampaignWebhook
7
+ # General webhook controller configuration.
8
+ class ApplicationController < ActionController::API
9
+ include ActionController::HttpAuthentication::Token::ControllerMethods
10
+
11
+ before_action :authenticate
12
+
13
+ protected
14
+
15
+ # Verify auth token is present and matches the configured value.
16
+ def authenticate
17
+ authenticate_or_request_with_http_token do |token, options|
18
+ ActiveSupport::SecurityUtils.secure_compare(token, SmSmsCampaignWebhook.auth_token)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_dependency "sm_sms_campaign_webhook/application_controller"
5
+
6
+ module SmSmsCampaignWebhook
7
+ # API webhook for POST requests from SMS campaign service.
8
+ class WebhookController < ApplicationController
9
+ # POST /api/webhook
10
+ # @see DispatchPayloadJob#perform
11
+ def create
12
+ # Deserialize the payload.
13
+ payload = JSON.parse(request.body.read)
14
+ logger.debug "#{self.class} - Payload: #{payload.inspect}"
15
+
16
+ # Dispatch the payload to the appropriate payload processor.
17
+ DispatchPayloadJob.perform_later(payload)
18
+
19
+ head :no_content
20
+ rescue JSON::ParserError => e
21
+ logger.warn "#{self.class} - Bad Request: #{e.class} - #{e}"
22
+ head :bad_request
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmSmsCampaignWebhook
4
+ # General base error type for custom errors.
5
+ class Error < StandardError; end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmSmsCampaignWebhook
4
+ # Error type for invalid payload schema received by the SMS webhook.
5
+ class InvalidPayload < Error; end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmSmsCampaignWebhook
4
+ # Error type for invalid value in the payload received by the SMS webhook.
5
+ class InvalidPayloadValue < Error; end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmSmsCampaignWebhook
4
+ # Error type for missing config values for this library.
5
+ class MissingConfigError < Error; end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmSmsCampaignWebhook
4
+ # Error type for improperly dispatch SMS campaign payloads.
5
+ class PayloadDispatchError < Error; end
6
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmSmsCampaignWebhook
4
+ # General async job configuration.
5
+ class ApplicationJob < ActiveJob::Base
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "sm_sms_campaign_webhook/application_job"
4
+
5
+ module SmSmsCampaignWebhook
6
+ # Handles SMS campaign payload dispatch to processor async.
7
+ class DispatchPayloadJob < ApplicationJob
8
+ # @param payload [Hash] Deserialized payload from SMS campaign service
9
+ # @see PayloadOperation.dispatch
10
+ def perform(payload)
11
+ PayloadOperation.dispatch(payload: payload)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_dependency "sm_sms_campaign_webhook/application_job"
4
+
5
+ module SmSmsCampaignWebhook
6
+ # Handles campaign engagement payload processing async.
7
+ class ProcessCampaignEngagementJob < ApplicationJob
8
+ # @param payload [Hash] Campaign engagement event payload
9
+ # @see CampaignEngagementOperation.process
10
+ def perform(payload)
11
+ CampaignEngagementOperation.process(payload: payload)
12
+ end
13
+ end
14
+ end