sm_sms_campaign_webhook 0.1.1 → 2.1.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 +56 -0
  3. data/README.md +340 -7
  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/answer.rb +76 -0
  15. data/app/models/sm_sms_campaign_webhook/campaign_engagement.rb +216 -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/engine.rb +11 -0
  27. data/lib/sm_sms_campaign_webhook/version.rb +1 -1
  28. data/lib/sm_sms_campaign_webhook.rb +25 -2
  29. data/sm_sms_campaign_webhook.gemspec +35 -21
  30. metadata +87 -22
  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: fdb779a5ea0f6c655227aebf4243bf8a3403a5cebe5c87de25e0a17aeb959019
4
+ data.tar.gz: d91f616682fbf09b20f4f553eaae9ed786a8799ec9e20c4952cd7c4c93610944
5
5
  SHA512:
6
- metadata.gz: ae0eb7921ee8ef9b295427c8333627c3938281977a97cc08ca10eefd6363c11699014aa3cbd987b05ced256760a758e909970708d1d9d6ea2481e83320d0778a
7
- data.tar.gz: 380100e28e9a8a4646fc61c132afe8824088947e378bfee1ef08785e48dc983d71fa39330d4d93b941cdf9cc2a9cfbd6fd4e4b4e84aa32de2d32dd8926aaaf3d
6
+ metadata.gz: 95bbdbbe988221f5604738a398d7a10a457a702839e791f59821367770de8cd241357635443a312743d77d9c25c1c6e4870a2a18ea8f8c765e7c878badc2b73b
7
+ data.tar.gz: 19029ca02d3afa4ff29872876cb80ba31ca0abf9b25d59b9f645c9d18a2a89c6daa97be24db210beb80ac4227e9ddc5697781e735c12c298d4a7394953b27539
data/CHANGELOG.md CHANGED
@@ -6,6 +6,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.1.0] - 2022-01-07
10
+ ### Added
11
+ - Ruby 3.1.x support
12
+ - Rails 7.0.x support
13
+
14
+ ### Changed
15
+ - Require Ruby >= 2.7.5
16
+ - CI to test against Ruby 3.1.x
17
+ - CI to test against Rails 7.0.x
18
+
19
+ ### Removed
20
+ - Ruby 2.6.x support
21
+
22
+ ## [2.0.1] - 2020-12-31
23
+ ### Removed
24
+ - Ruby 2.5.x support
25
+
26
+ ## [2.0.0] - 2020-12-31
27
+ ### Added
28
+ - Campaign engagement payload example to README
29
+ - Support for Rails 6.1.x
30
+
31
+ ### Changed
32
+ - Required version of rake for development
33
+ - Standardize style/format of gem code with [StandardRB](https://github.com/testdouble/standard)
34
+ - CI to test against Ruby 2.7.x
35
+ - Required Ruby version to permit 3.0.x
36
+ - CI to test against Ruby 3.0.x
37
+ - CI to test against Rails 6.1.x
38
+
39
+ ### Removed
40
+ - Rails 5.2.x support
41
+
42
+ ## [1.0.0] - 2019-07-26
43
+ ### Added
44
+ - Mountable Rails engine as API
45
+ - POST /api/webhook resource requiring JSON payload for asynchronous dispatching and processing
46
+ - Require inbound POST requests be authorization requests with an auth token
47
+ - Support for Rails 5.2.x, 6.0.x
48
+ - Data models for campaign engagement event payloads
49
+ - Helper method to get specific campaign engagement answer
50
+ - Payload operation library to dispatch and process deserialized JSON with supported event data modeling
51
+ - Processable behavior definition for app implementors
52
+ - Default processor mixing in processable behavior with noisy errors
53
+ - ActiveJob library for asynchronous handling of payload dispatching and processing
54
+ - Configuration support for required app implementation values
55
+
56
+ ### Changed
57
+ - Require Ruby >= 2.5
58
+ - CI to test against Ruby 2.5.5, 2.6.3
59
+ - CI to test against Rails 5.2.x, 6.0.x
60
+
61
+ ## [0.1.1] - 2019-07-16
62
+ ### Changed
63
+ - Changelog URI to correct value in gemspec
64
+
9
65
  ## [0.1.0] - 2019-07-16
10
66
  ### Added
11
67
  - NOOP gem configured for development
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
- # SmSmsCampaignWebhook: Middleware providing webhook for Southern Made SMS Campaign Engagement
1
+ # SmSmsCampaignWebhook
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/sm_sms_campaign_webhook.svg)](https://badge.fury.io/rb/sm_sms_campaign_webhook)
4
- [![Travis Build Status](https://travis-ci.org/SouthernMade/sm_sms_campaign_webhook.svg?branch=develop)](https://travis-ci.org/SouthernMade/sm_sms_campaign_webhook)
3
+ [![Southern Made - Galaxy Logo](https://raw.github.com/SouthernMade/sm_sms_campaign_webhook/develop/logo_galaxymark.png)](https://www.southernmade.com/) by [Southern Made](https://www.southernmade.com/)
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/sm_sms_campaign_webhook.svg)](https://rubygems.org/gems/sm_sms_campaign_webhook)
6
+ [![CI](https://github.com/SouthernMade/sm_sms_campaign_webhook/actions/workflows/ci.yml/badge.svg)](https://github.com/SouthernMade/sm_sms_campaign_webhook/actions/workflows/ci.yml)
5
7
  [![Code Climate Maintainability](https://api.codeclimate.com/v1/badges/2298f12a7d6f31688c9c/maintainability)](https://codeclimate.com/github/SouthernMade/sm_sms_campaign_webhook/maintainability)
6
8
  [![Code Climate Test Coverage](https://api.codeclimate.com/v1/badges/2298f12a7d6f31688c9c/test_coverage)](https://codeclimate.com/github/SouthernMade/sm_sms_campaign_webhook/test_coverage)
7
9
 
@@ -15,12 +17,37 @@ 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
+ - [Campaign Engagement Payload Example](#campaign-engagement-payload-example)
36
+ - [Development](#development)
37
+ - [Versioning](#versioning)
38
+ - [Testing](#testing)
39
+ - [Documentation](#documentation)
40
+ - [Contributing](#contributing)
41
+ - [License](#license)
42
+
18
43
  ## Installation
19
44
 
45
+ This gem is tested with Rails 6.0.x, 6.1.x, 7.0.x versions.
46
+
20
47
  Add this line to your application's Gemfile:
21
48
 
22
49
  ```ruby
23
- gem 'sm_sms_campaign_webhook'
50
+ gem "sm_sms_campaign_webhook", "~> 2.1"
24
51
  ```
25
52
 
26
53
  And then execute:
@@ -31,26 +58,332 @@ Or install it yourself as:
31
58
 
32
59
  $ gem install sm_sms_campaign_webhook
33
60
 
61
+ ## Configuration
62
+
63
+ These are the steps to configure your app to be ready to capture SMS campaign service payloads.
64
+
65
+ ### Auto Generate Config
66
+
67
+ You can setup most app configuration by running the generator:
68
+
69
+ ```
70
+ $ bundle exec rails generate sm_sms_campaign_webhook:install
71
+ ```
72
+
73
+ Some things will still require manual configuration and will be identified after generation:
74
+
75
+ - [Webhook Auth Token](#webhook-auth-token)
76
+ - [ActiveJob](#activejob)
77
+
78
+ After that, be sure to read the [Usage](#usage) section for payload processor details!
79
+
80
+ If you prefer to setup everything by hand, be sure to check out:
81
+
82
+ - [Webhook Initializer](#webhook-initializer)
83
+ - [Payload Processor](#payload-processor)
84
+
85
+ ### Webhook Auth Token
86
+
87
+ 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.
88
+
89
+ Set this value using the rails secret generator:
90
+
91
+ ```
92
+ $ bundle exec rails secret
93
+ ```
94
+
95
+ And copy the result to your `.env` or applicable config file:
96
+
97
+ ```
98
+ SM_SMS_CAMPAIGN_WEBHOOK_AUTH_TOKEN="******"
99
+ ```
100
+
101
+ ### ActiveJob
102
+
103
+ 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.
104
+
105
+ If you have already chosen another queue adapter then feel free to ignore this step!
106
+
107
+ You can set the adapter in `config/application.rb` with:
108
+
109
+ ```ruby
110
+ class Application < Rails::Application
111
+ config.active_job.queue_adapter = :sidekiq
112
+ end
113
+ ```
114
+
115
+ Add `config/sidekiq.yml` config with:
116
+
117
+ ```yaml
118
+ ---
119
+ :concurrency: <%= ENV.fetch("SIDEKIQ_CONCURRENCY") { 5 }.to_i %>
120
+ :timeout: <%= ENV.fetch("SIDEKIQ_TIMEOUT") { 25 }.to_i %>
121
+ :queues:
122
+ - default
123
+ - mailers
124
+ ```
125
+
126
+ Add `config/initializers/sidekiq.rb` with:
127
+
128
+ ```ruby
129
+ # @note Sidekiq server + client must both be configured for Redis.
130
+ # @see https://github.com/mperham/sidekiq/wiki/Using-Redis
131
+
132
+ Sidekiq.configure_server do |config|
133
+ config.redis = {
134
+ url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" },
135
+ network_timeout: ENV.fetch("REDIS_NETWORK_TIMEOUT") { 5 }.to_i
136
+ }
137
+ end
138
+
139
+ Sidekiq.configure_client do |config|
140
+ config.redis = {
141
+ url: ENV.fetch("REDIS_URL") { "redis://localhost:6379/0" },
142
+ network_timeout: ENV.fetch("REDIS_NETWORK_TIMEOUT") { 5 }.to_i
143
+ }
144
+ end
145
+ ```
146
+
147
+ Update your Procfile or appropriate config to launch worker processes:
148
+
149
+ ```
150
+ worker: RAILS_MAX_THREADS=${SIDEKIQ_CONCURRENCY:-5} bundle exec sidekiq --config config/sidekiq.yml
151
+ ```
152
+
153
+ More detailed instructions about using Sidekiq can be found in the [Sidekiq Wiki](https://github.com/mperham/sidekiq/wiki).
154
+
155
+ ### Mount the Webhook Engine
156
+
157
+ If you opted to [auto generate the config](#auto-generate-config), this can be skipped.
158
+
159
+ Add the following to `config/routes.rb` in your app to mount the webhook:
160
+
161
+ ```ruby
162
+ mount SmSmsCampaignWebhook::Engine => "/sms_campaign"
163
+ ```
164
+
165
+ This sets the app up to receive POST requests from the SMS campaign service:
166
+
167
+ POST /sms_campaign/api/webhook
168
+
169
+ 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!
170
+
171
+ ### Webhook Initializer
172
+
173
+ If you opted to [auto generate the config](#auto-generate-config), this can be skipped.
174
+
175
+ 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`:
176
+
177
+ ```ruby
178
+ require "sm_sms_campaign_webhook"
179
+
180
+ SmSmsCampaignWebhook.config do |config|
181
+ # SMS campaign payload processor implementing SmSmsCampaignWebhook::Processable behavior.
182
+ # default: SmSmsCampaignWebhook::DefaultProcessor (raises errors for processing)
183
+ # config.processor = SmsPayloadProcessor
184
+ end
185
+ ```
186
+
187
+ ### Payload Processor
188
+
189
+ 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!
190
+
191
+ 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.
192
+
193
+ To create a processor, create a custom class mixing in `SmSmsCampaignWebhook::Processable` behavior. For example, we can create a custom processor named `SmsPayloadProcessor`:
194
+
195
+ ```ruby
196
+ class SmsPayloadProcessor
197
+ include SmSmsCampaignWebhook::Processable
198
+
199
+ # Implement required methods for Processable behavior.
200
+
201
+ # @param campaign_engagement [SmSmsCampaignWebhook::CampaignEngagement]
202
+ # def self.process_campaign_engagement(campaign_engagement)
203
+ # # NOOP - I need to be implemented.
204
+ # end
205
+ end
206
+ ```
207
+
208
+ 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.
209
+
210
+ 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`:
211
+
212
+ ```ruby
213
+ SmSmsCampaignWebhook.config do |config|
214
+ #...
215
+ config.processor = SmsPayloadProcessor
216
+ #...
217
+ end
218
+ ```
219
+
34
220
  ## Usage
35
221
 
36
- Right now, nothing happens! Soon, some useful details will emerge about how to ingest the SMS campaign payloads.
222
+ 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.
223
+
224
+ 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.
225
+
226
+ ### Campaign Engagement
227
+
228
+ This payload represents a user's phone interaction with the SMS campaign. This includes:
229
+
230
+ - First contact with a SMS campaign by keyword
231
+ - Responding to subsequent SMS campaign messages
232
+ - Continued engagement for multi-entry SMS campaigns
233
+
234
+ 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!
235
+
236
+ 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:
237
+
238
+ - Fields + value types that will be in answers
239
+ - Required fields to complete registration/entry
240
+ - How to interpret voting style numeric answers
241
+
242
+ #### Processor Expections
243
+
244
+ You must implement behavior for this method to ingest campaign engagement data in your paylod processor:
245
+
246
+ ```ruby
247
+ def self.process_campaign_engagement(campaign_engagement)
248
+ # ...
249
+ end
250
+ ```
251
+
252
+ 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:
253
+
254
+ - Registering/creating a user account
255
+ - Logging registration/entries
256
+ - Interpreting + logging vote responses
257
+
258
+ Check with your Southern Made project manager for expectations.
259
+
260
+ #### Campaign Engagement Data Model
261
+
262
+ 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.
263
+
264
+ Some example message passing to an instance:
265
+
266
+ ```ruby
267
+ campaign_engagement.event_uuid # UUID - unique payload event
268
+ campaign_engagement.campaign_keyword # String - SMS campaign entry point
269
+ campaign_engagement.phone_id # Integer - represents specific phone
270
+ campaign_engagement.phone_number # String - phone number interacting with SMS campaign
271
+
272
+ # This represents a specific phone engaging with a specific campaign.
273
+ # The value will differ for each entry in multi-entry campaigns.
274
+ # For standard campaigns we will only see one value.
275
+ campaign_engagement.phone_campaign_state_id # Integer
276
+
277
+ # These values help determine if and when answers were received
278
+ # for all campaign messages.
279
+ campaign_engagement.phone_campaign_state_completed? # TrueClass,FalseClass
280
+ campaign_engagement.phone_campaign_state_completed_at # DateTime
281
+ ```
282
+
283
+ It also provides a useful helper methods related to campaign engagement answers. For example:
284
+
285
+ ```ruby
286
+ # Are any campaign engagement answers in the payload?
287
+ campaign_engagement.phone_campaign_state_answers? # TrueClass,FalseClass
288
+
289
+ # This tries to find an answer for the requested field.
290
+ # If a match is found it returns instance of
291
+ # SmSmsCampaignWebhook::CampaignEngagement::Answer data model.
292
+ # If a match is not found it return nil (NilClass).
293
+ campaign_engagement.answer_for(field: "email") # Returned type answer specific
294
+ ```
295
+
296
+ #### Campaign Engagement Answer Data Model
297
+
298
+ 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:
299
+
300
+ - field (`String`)
301
+ - value (varies)
302
+ - collected_at (`DateTime`)
303
+
304
+ The value data types could be one of the following:
305
+
306
+ - string (`String`)
307
+ - email (`String`)
308
+ - date (`Date`)
309
+ - number (`Integer`)
310
+ - boolean (`TrueClass`, `FalseClass`)
311
+ - us_state (`String`)
312
+
313
+ #### Campaign Engagement Payload Example
314
+
315
+ Here is an example payload for campaign engagement that could come through to the payload processor. Be sure to check with your Southern Made project manager to gather details about the answer fields and data types:
316
+
317
+ ```json
318
+ {
319
+ "uuid": "99aaafe3-b52b-413f-a9cd-db52fa13b77a",
320
+ "object": "event",
321
+ "type": "campaign.engagement",
322
+ "created_at": "2019-08-09T18:29:05.052Z",
323
+ "data": {
324
+ "campaign": {
325
+ "id": 55,
326
+ "keyword": "KEYWORD"
327
+ },
328
+ "phone": {
329
+ "id": 80,
330
+ "number": "3335557777"
331
+ },
332
+ "phone_campaign_state": {
333
+ "id": 95,
334
+ "answers": {
335
+ "DOB": {
336
+ "value": "2001-07-04",
337
+ "collected_at": "2019-08-09T18:26:59.052Z"
338
+ },
339
+ "email": {
340
+ "value": "email@example.com",
341
+ "collected_at": "2019-08-09T18:27:59.052Z"
342
+ },
343
+ "vote-september": {
344
+ "value": 1,
345
+ "collected_at": "2019-08-09T18:28:59.052Z"
346
+ }
347
+ },
348
+ "completed": true,
349
+ "completed_at": "2019-08-09T18:28:59.052Z"
350
+ }
351
+ }
352
+ }
353
+ ```
354
+
355
+ [cURL](https://curl.haxx.se) example assuming the payload file path is `tmp/sms_campaign_payload.json`, app is running running with mount point `sms_campaign`, web server uses port `3000`, and that you use your app's webhook auth token:
356
+
357
+ ```bash
358
+ $ curl \
359
+ --header "Authorization: Bearer WEBHOOKAUTHTOKEN" \
360
+ --header "Content-Type: application/json" \
361
+ --header "Accept: application/json" \
362
+ --data @tmp/sms_campaign_payload.json \
363
+ http://localhost:3000/sms_campaign/api/webhook
364
+ ```
37
365
 
38
366
  ## Development
39
367
 
40
368
  This gem uses [git-flow](https://github.com/nvie/gitflow) to manage deployments. The default branches are used to manage development and production code.
41
369
 
370
+ ### StandardRB
371
+
372
+ This project uses [StandardRB](https://github.com/testdouble/standard), a hands-off wrapper around [Rubocop](https://docs.rubocop.org/en/stable/), to manage style/formatting/etc. Please apply changes before submitting pull requests:
373
+
374
+ $ bundle exec standardrb --fix
375
+
42
376
  ### Versioning
43
377
 
44
378
  Gem versioning follows [Semantic Versioning](https://semver.org).
45
379
 
46
-
47
380
  ### Testing
48
381
 
49
382
  This project uses Rspec for testing. Specs must be green for any PR to be accepted!
50
383
 
51
384
  $ bundle exec rspec
52
385
 
53
- The project is setup with [Travis CI](https://travis-ci.org) to automate test. The various environments that are regularly tested can be seen in [.travis.yml](https://github.com/SouthernMade/sm_sms_campaign_webhook/blob/develop/.travis.yml).
386
+ The project is setup with [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions) to automate testing. The various workflows and environments can be seen in [.github/workflows/ci.yml](https://github.com/SouthernMade/sm_sms_campaign_webhook/blob/develop/.github/workflows/ci.yml).
54
387
 
55
388
  ### Documentation
56
389
 
@@ -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
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module SmSmsCampaignWebhook
6
+ class CampaignEngagement
7
+ # Data model for campaign engagement answer data.
8
+ class Answer
9
+ # @param data [Hash] Answers from payload
10
+ # @return [Array<Answer>] Modeled answer data sorted by collected_at
11
+ def self.cast(data:)
12
+ data.map { |field, answer_hash|
13
+ new(field: field, answer_hash: answer_hash)
14
+ }.sort_by(&:collected_at)
15
+ end
16
+
17
+ attr_reader :field,
18
+ :answer_hash
19
+
20
+ # @param field [String] Field describing the answer
21
+ # @param answer_hash [Hash] Answer data from payload
22
+ def initialize(field:, answer_hash:)
23
+ @field = String(field)
24
+ @answer_hash = answer_hash.freeze
25
+ end
26
+
27
+ # Collected answer could be many different value types.
28
+ #
29
+ # The SMS campaign service collects answers of type:
30
+ # string, email, date, number, boolean, us_state
31
+ #
32
+ # The possible types are from SMS campaign service perspective.
33
+ # They are coerced to the appropriate type in Ruby.
34
+ #
35
+ # @return [String,Integer,Date,TrueClass,FalseClass] Coerced answer
36
+ # @raise [InvalidPayload] when value is missing from answer_hash
37
+ def value
38
+ # Could be boolean so cannot rely on double pipe assignment guard.
39
+ unless @value.nil?
40
+ return @value
41
+ end
42
+
43
+ # Extract the value and memoize it.
44
+ @value = begin
45
+ raw_value = answer_hash.fetch("value") {
46
+ raise InvalidPayload,
47
+ "value missing from answer_hash #{answer_hash.inspect}"
48
+ }.freeze
49
+
50
+ # Attempt to parse date value falling back to raw value.
51
+ begin
52
+ Date.strptime(raw_value, "%Y-%m-%d").freeze
53
+ rescue
54
+ raw_value
55
+ end
56
+ end
57
+ end
58
+
59
+ # @return [DateTime] Timestamp of answer value collection
60
+ # @raise [InvalidPayload] when collected_at missing from answer_hash
61
+ # @raise [InvalidPayloadValue] when collected_at not datetime
62
+ def collected_at
63
+ @collected_at ||= begin
64
+ raw_collected_at = answer_hash.fetch("collected_at") {
65
+ raise InvalidPayload,
66
+ "collected_at missing from answer_hash #{answer_hash.inspect}"
67
+ }
68
+ DateTime.parse(raw_collected_at).freeze
69
+ end
70
+ rescue ArgumentError
71
+ raise InvalidPayloadValue,
72
+ "collected_at has invalid datetime value #{answer_hash.inspect}"
73
+ end
74
+ end
75
+ end
76
+ end