Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Listen to Argyle Webhooks to check when paystubs are fully sync'd #13

Merged
merged 12 commits into from
May 2, 2024
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder where this churn is coming from. How many times will this end up getting added to the Gemfile, lol

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tdooner hmmm Yeah I"m not sure. I think when George did the Docker stuff it changed.

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;
}
Comment on lines +5 to +18
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty crappy.

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
21 changes: 21 additions & 0 deletions app/controllers/webhooks/argyle/events_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class Webhooks::Argyle::EventsController < ApplicationController
skip_before_action :verify_authenticity_token

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

if request.headers["X-Argyle-Signature"] == signature
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
else
render json: { error: 'Invalid signature' }, status: :unauthorized
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();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the event from Argyle is "fully_synced," submit the form and proceed to the next step!

}
}
});
}

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
Loading