Skip to content

Commit

Permalink
Merge pull request #13 from DSACMS/mg-webhooks
Browse files Browse the repository at this point in the history
Listen to Argyle Webhooks to check when paystubs are fully sync'd
  • Loading branch information
allthesignals authored May 2, 2024
2 parents abe5640 + 571e674 commit 68fac6f
Show file tree
Hide file tree
Showing 20 changed files with 163 additions and 29 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ARGYLE_API_TOKEN=
ARGYLE_SANDBOX=
ARGYLE_WEBHOOK_SECRET=
NGROK_URL=
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ GEM
nio4r (2.7.1)
nokogiri (1.16.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.4-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.4-x86_64-linux)
racc (~> 1.4)
parallel (1.24.0)
Expand Down Expand Up @@ -336,6 +338,7 @@ GEM

PLATFORMS
arm64-darwin-23
x86_64-darwin-20
x86_64-linux

DEPENDENCIES
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ guide for an introduction to the framework.
* Chromedriver must be allowed to run. You can either do that by:
* The command line: `xattr -d com.apple.quarantine $(which chromedriver)` (this is the only option if you are on Big Sur)
* Manually: clicking "allow" when you run the integration tests for the first time and a dialogue opens up
* [Ngrok](https://ngrok.com/download): brew install ngrok/ngrok/ngrok
* Sign up for an account: https://dashboard.ngrok.com/signup
* run `ngrok config add-authtoken {token goes here}`
* Set up rbenv and nodenv:
* `echo 'if which nodenv >/dev/null 2>/dev/null; then eval "$(nodenv init -)"; fi' >> ~/.zshrc`
* `echo 'if which rbenv >/dev/null 2>/dev/null; then eval "$(rbenv init -)"; fi' >> ~/.zshrc`
Expand Down Expand Up @@ -59,6 +62,8 @@ of the test.

To run locally, use `bin/dev`

Separately, run `ngrok 3000`. Copy the Forwarding URL into your .env.local value for `NGROK_URL`.

## Security

### Authentication
Expand Down
15 changes: 15 additions & 0 deletions app/assets/stylesheets/application.postcss.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/* Entry point for your PostCSS build */
@forward "uswds-settings.scss";
@forward "uswds-components.scss";

.argyle-loading {
position: relative;
}

.argyle-loading::after {
background-color: rgba(0, 0, 0, 0.128);
content: "";
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 9999;
}
3 changes: 3 additions & 0 deletions app/channels/application_cable/connection.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
def session
@request.session
end
end
end
10 changes: 10 additions & 0 deletions app/channels/argyle_paystubs_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class ArgylePaystubsChannel < ApplicationCable::Channel
def subscribed
cbv_flow = CbvFlow.find(connection.session[:cbv_flow_id])
stream_for cbv_flow
end

def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
10 changes: 5 additions & 5 deletions app/controllers/cbv_flows_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class CbvFlowsController < ApplicationController
USER_TOKEN_ENDPOINT = 'https://api-sandbox.argyle.com/v2/users';
ITEMS_ENDPOINT = 'https://api-sandbox.argyle.com/v2/items';
PAYSTUBS_ENDPOINT = 'https://api-sandbox.argyle.com/v2/paystubs?limit=2&user='
PAYSTUBS_ENDPOINT = 'https://api-sandbox.argyle.com/v2/paystubs?user='

before_action :set_cbv_flow

Expand Down Expand Up @@ -58,9 +58,9 @@ def next_path
def fetch_and_store_argyle_token
return session[:argyle_user_token] if session[:argyle_user_token].present?

raise "ARGYLE_API_TOKEN environment variable is blank. Make sure you have the .env.local.local from 1Password." if ENV['ARGYLE_API_TOKEN'].blank?
raise "ARGYLE_API_TOKEN environment variable is blank. Make sure you have the .env.local.local from 1Password." if Rails.application.credentials.argyle[:api_key].blank?

res = Net::HTTP.post(URI.parse(USER_TOKEN_ENDPOINT), "", {"Authorization" => "Basic #{ENV['ARGYLE_API_TOKEN']}"})
res = Net::HTTP.post(URI.parse(USER_TOKEN_ENDPOINT), "", {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"})
parsed = JSON.parse(res.body)
raise "Argyle API error: #{parsed['detail']}" if res.code.to_i >= 400

Expand All @@ -71,14 +71,14 @@ def fetch_and_store_argyle_token
end

def fetch_employers
res = Net::HTTP.get(URI.parse(ITEMS_ENDPOINT), {"Authorization" => "Basic #{ENV['ARGYLE_API_TOKEN']}"})
res = Net::HTTP.get(URI.parse(ITEMS_ENDPOINT), {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"})
parsed = JSON.parse(res)

parsed['results']
end

def fetch_payroll
res = Net::HTTP.get(URI.parse("#{PAYSTUBS_ENDPOINT}#{@cbv_flow.argyle_user_id}"), {"Authorization" => "Basic #{ENV['ARGYLE_API_TOKEN']}"})
res = Net::HTTP.get(URI.parse("#{PAYSTUBS_ENDPOINT}#{@cbv_flow.argyle_user_id}"), {"Authorization" => "Basic #{Rails.application.credentials.argyle[:api_key]}"})
parsed = JSON.parse(res)

parsed['results']
Expand Down
20 changes: 20 additions & 0 deletions app/controllers/webhooks/argyle/events_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class Webhooks::Argyle::EventsController < ApplicationController
skip_before_action :verify_authenticity_token

def create
signature = OpenSSL::HMAC.hexdigest('SHA512', ENV['ARGYLE_WEBHOOK_SECRET'], request.raw_post)

unless request.headers["X-Argyle-Signature"] == signature
return render json: { error: 'Invalid signature' }, status: :unauthorized
end

if params['event'] == 'paystubs.fully_synced' || params['event'] == 'paystubs.partially_synced'
@cbv_flow = CbvFlow.find_by_argyle_user_id(params['data']['user'])

if @cbv_flow
@cbv_flow.update(payroll_data_available_from: params['data']['available_from'])
ArgylePaystubsChannel.broadcast_to(@cbv_flow, params)
end
end
end
end
2 changes: 2 additions & 0 deletions app/helpers/webhooks/argyle/events_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module Webhooks::Argyle::EventsHelper
end
29 changes: 26 additions & 3 deletions app/javascript/controllers/cbv_flows_controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Controller } from "@hotwired/stimulus"
import * as ActionCable from '@rails/actioncable'

import metaContent from "../utilities/meta";
import { loadArgyle, initializeArgyle } from "../utilities/argyle"
Expand All @@ -8,23 +9,45 @@ function toOptionHTML({ value }) {
}

export default class extends Controller {
static targets = ["options", "continue", "userAccountId"];
static targets = ["options", "continue", "userAccountId", "fullySynced", "form"];
static classes = ["loading"]

selection = null;

argyle = null;

argyleUserToken = null;

cable = ActionCable.createConsumer();

// TODO: information stored on the CbvFlow model can infer whether the paystubs are sync'd
// by checking the value of payroll_data_available_from. We should make that the initial value.
fullySynced = false;

connect() {
// check for this value when connected
this.argyleUserToken = metaContent('argyle_user_token');
this.cable.subscriptions.create({ channel: 'ArgylePaystubsChannel' }, {
connected: () => {
console.log("Connected to the channel:", this);
},
disconnected: () => {
console.log("Disconnected");
},
received: (data) => {
console.log("Received some data:", data);
if (data.event === 'paystubs.fully_synced' || data.event === 'paystubs.partially_synced') {
this.fullySynced = true;

this.formTarget.submit();
}
}
});
}

onSignInSuccess(event) {
this.userAccountIdTarget.value = event.accountId;

this.element.submit();
this.element.classList.add(this.loadingClass);
}

onAccountError(event) {
Expand Down
42 changes: 22 additions & 20 deletions app/views/cbv_flows/employer_search.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@

<h2>Get payment info from your employer.</h2>

<%= form_with url: next_path, method: :post, data: { controller: "cbv-flows" } do |f| %>
<label class="usa-label" for="employer">Search for an employer</label>
<div class="usa-combo-box margin-bottom-3" data-action="input->cbv-flows#search">
<select class="usa-select" name="employer" id="employer" data-cbv-flows-target="options" data-action="change->cbv-flows#select">
<option value>Select an employer</option>
<% @companies.each do |company| %>
<option value='<%= company['id'] %>'>
<%= company['name'] %>
</option>
<% end %>
</select>
<input type="hidden" name="user[account_id]" data-cbv-flows-target="userAccountId" />
</div>
<%= f.submit "Continue",
class: "usa-button usa-button--outline",
disabled: "disabled",
type: "submit",
data: { action: "click->cbv-flows#submit", 'cbv-flows-target': "continue" }
%>
<% end %>
<div data-controller="cbv-flows" data-cbv-flows-loading-class="argyle-loading loader">
<%= form_with url: next_path, method: :post, data: { 'cbv-flows-target': "form" } do |f| %>
<label class="usa-label" for="employer">Search for an employer</label>
<div class="usa-combo-box margin-bottom-3" data-action="input->cbv-flows#search">
<select class="usa-select" name="employer" id="employer" data-cbv-flows-target="options" data-action="change->cbv-flows#select">
<option value>Select an employer</option>
<% @companies.each do |company| %>
<option value='<%= company['id'] %>'>
<%= company['name'] %>
</option>
<% end %>
</select>
<input type="hidden" name="user[account_id]" data-cbv-flows-target="userAccountId" />
</div>
<%= f.submit "Continue",
class: "usa-button usa-button--outline",
disabled: "disabled",
type: "submit",
data: { action: "click->cbv-flows#submit", 'cbv-flows-target': "continue" }
%>
<% end %>
</div>
2 changes: 2 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
# Enable server timing
config.server_timing = true

config.hosts << ENV['NGROK_URL'] if ENV['NGROK_URL'].present?

# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
Expand Down
6 changes: 6 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@
get '/reset' => 'cbv_flows#reset'
end
end

namespace :webhooks do
namespace :argyle do
resources :events, only: :create
end
end
end
5 changes: 5 additions & 0 deletions db/migrate/20240501192504_add_payroll_column_to_cbv_flows.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddPayrollColumnToCbvFlows < ActiveRecord::Migration[7.0]
def change
add_column :cbv_flows, :payroll_data_available_from, :date
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"@csstools/postcss-sass": "^5.1.1",
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo-rails": "^8.0.4",
"@rails/actioncable": "^7.1.3-2",
"@uswds/uswds": "^3.8.0",
"autoprefixer": "^10.4.19",
"esbuild": "^0.20.2",
Expand Down
5 changes: 5 additions & 0 deletions spec/channels/argyle_paystubs_channel_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'rails_helper'

RSpec.describe ArgylePaystubsChannel, type: :channel do
pending "add some examples to (or delete) #{__FILE__}"
end
15 changes: 15 additions & 0 deletions spec/helpers/webhooks/argyle/events_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'rails_helper'

# Specs in this file have access to a helper object that includes
# the Webhooks::Argyle::EventsHelper. For example:
#
# describe Webhooks::Argyle::EventsHelper do
# describe "string concat" do
# it "concats two strings with spaces" do
# expect(helper.concat_strings("this","that")).to eq("this that")
# end
# end
# end
RSpec.describe Webhooks::Argyle::EventsHelper, type: :helper do
pending "add some examples to (or delete) #{__FILE__}"
end
7 changes: 7 additions & 0 deletions spec/requests/webhooks/argyle/events_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require 'rails_helper'

RSpec.describe "Webhooks::Argyle::Events", type: :request do
describe "GET /index" do
pending "add some examples (or delete) #{__FILE__}"
end
end
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.1.3.tgz#4db480347775aeecd4dde2405659eef74a458881"
integrity sha512-ojNvnoZtPN0pYvVFtlO7dyEN9Oml1B6IDM+whGKVak69MMYW99lC2NOWXWeE3bmwEydbP/nn6ERcpfjHVjYQjA==

"@rails/actioncable@^7.1.3-2":
version "7.1.3-2"
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.1.3-2.tgz#7105fd8fc6db87d11510d5e9ac8e5b68add6124e"
integrity sha512-0j/bo3Aehk3R3LTc/Nu4eegvUR+8kRObq/t1BtBLGF8xdPzrUQciv+KvAABdACl4Vw82yo0LB1r0J0zVH9Gefg==

"@sindresorhus/merge-streams@^2.1.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958"
Expand Down

0 comments on commit 68fac6f

Please sign in to comment.