From bbe22b2fc32df5792bca686e717ff788214993f0 Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Fri, 21 Feb 2025 13:49:44 -0600 Subject: [PATCH 1/7] Use remote_ip instead of ip (#466) Correct an issue where the location of users in Mixpanel continues to be wrong. --- app/lib/generic_event_tracker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/generic_event_tracker.rb b/app/lib/generic_event_tracker.rb index d6116f96..e5dde557 100644 --- a/app/lib/generic_event_tracker.rb +++ b/app/lib/generic_event_tracker.rb @@ -7,7 +7,7 @@ def self.for_request(request) url_params = request.params.slice("client_agency_id", "locale") defaults = { # Not setting device_id because Mixpanel fixates on that as the distinct_id, which we do not want - ip: request.ip, + ip: request.remote_ip, cbv_flow_id: request.session[:cbv_flow_id], client_agency_id: url_params["client_agency_id"], locale: url_params["locale"], From 13bdc0a75afcbcc531f44657366f80183d0360bb Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Fri, 21 Feb 2025 14:17:01 -0600 Subject: [PATCH 2/7] Revert "Use remote_ip instead of ip (#466)" (#467) --- app/lib/generic_event_tracker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/generic_event_tracker.rb b/app/lib/generic_event_tracker.rb index e5dde557..d6116f96 100644 --- a/app/lib/generic_event_tracker.rb +++ b/app/lib/generic_event_tracker.rb @@ -7,7 +7,7 @@ def self.for_request(request) url_params = request.params.slice("client_agency_id", "locale") defaults = { # Not setting device_id because Mixpanel fixates on that as the distinct_id, which we do not want - ip: request.remote_ip, + ip: request.ip, cbv_flow_id: request.session[:cbv_flow_id], client_agency_id: url_params["client_agency_id"], locale: url_params["locale"], From 5401497e8a8e9e11dc2ad9c1340088321a33c821 Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Fri, 21 Feb 2025 16:26:31 -0600 Subject: [PATCH 3/7] FFS-2397-3: experimental: use remote_ip instead of ip (#468) --- app/lib/generic_event_tracker.rb | 2 +- app/lib/mixpanel_event_tracker.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/lib/generic_event_tracker.rb b/app/lib/generic_event_tracker.rb index d6116f96..e5dde557 100644 --- a/app/lib/generic_event_tracker.rb +++ b/app/lib/generic_event_tracker.rb @@ -7,7 +7,7 @@ def self.for_request(request) url_params = request.params.slice("client_agency_id", "locale") defaults = { # Not setting device_id because Mixpanel fixates on that as the distinct_id, which we do not want - ip: request.ip, + ip: request.remote_ip, cbv_flow_id: request.session[:cbv_flow_id], client_agency_id: url_params["client_agency_id"], locale: url_params["locale"], diff --git a/app/lib/mixpanel_event_tracker.rb b/app/lib/mixpanel_event_tracker.rb index be83c98e..c5cb75b5 100644 --- a/app/lib/mixpanel_event_tracker.rb +++ b/app/lib/mixpanel_event_tracker.rb @@ -20,7 +20,7 @@ def track(event_type, request, attributes = {}) tracker_attrs = { cbv_flow_id: flow_id } if request.present? - tracker_attrs.merge!({ ip: request.ip }) + tracker_attrs.merge!({ "$ip": request.remote_ip }) end @tracker.people.set(distinct_id, tracker_attrs) From ab90d33ae75d843899ae2cf73be4742df73afb85 Mon Sep 17 00:00:00 2001 From: Tom Dooner Date: Fri, 21 Feb 2025 18:14:03 -0600 Subject: [PATCH 4/7] FFS-1427: Implement custom 404/500 errors (#462) --- .github/workflows/owasp-scan.yml | 2 +- app/.rubocop.yml | 2 + app/app/controllers/pages_controller.rb | 19 ++++++ app/app/views/pages/error_404.html.erb | 13 ++++ app/app/views/pages/error_500.html.erb | 13 ++++ app/config/application.rb | 3 + app/config/i18n-tasks.yml | 1 + app/config/locales/en.yml | 10 +++ app/config/routes.rb | 3 + app/public/404.html | 67 ------------------- app/public/422.html | 67 ------------------- app/public/500.html | 66 ------------------ app/spec/controllers/pages_controller_spec.rb | 40 +++++++++++ app/zap.conf | 5 ++ zap_options.conf | 3 - 15 files changed, 110 insertions(+), 204 deletions(-) create mode 100644 app/app/views/pages/error_404.html.erb create mode 100644 app/app/views/pages/error_500.html.erb delete mode 100644 app/public/404.html delete mode 100644 app/public/422.html delete mode 100644 app/public/500.html create mode 100644 app/spec/controllers/pages_controller_spec.rb delete mode 100644 zap_options.conf diff --git a/.github/workflows/owasp-scan.yml b/.github/workflows/owasp-scan.yml index f499cc06..77572bd5 100644 --- a/.github/workflows/owasp-scan.yml +++ b/.github/workflows/owasp-scan.yml @@ -45,4 +45,4 @@ jobs: with: target: 'http://localhost:3000/' fail_action: true - cmd_options: -c app/zap.conf -z "-configfile /zap/wrk/zap_options.conf" + cmd_options: -c app/zap.conf diff --git a/app/.rubocop.yml b/app/.rubocop.yml index 0c9fe056..e74571f9 100644 --- a/app/.rubocop.yml +++ b/app/.rubocop.yml @@ -21,3 +21,5 @@ Layout/IndentationWidth: Layout/IndentationConsistency: Enabled: true EnforcedStyle: normal +Layout/EndAlignment: + EnforcedStyleAlignWith: keyword diff --git a/app/app/controllers/pages_controller.rb b/app/app/controllers/pages_controller.rb index 45f463e4..d2b23567 100644 --- a/app/app/controllers/pages_controller.rb +++ b/app/app/controllers/pages_controller.rb @@ -1,4 +1,23 @@ class PagesController < ApplicationController def home end + + def error_404 + # When in development environment, you'll need to set + # config.consider_all_requests_local = false + # in config/development.rb for these pages to actually show up. + @cbv_flow = if session[:cbv_flow_id] + CbvFlow.find(session[:cbv_flow_id]) + end + + render status: :not_found, formats: %i[html] + end + + def error_500 + # When in development environment, you'll need to set + # config.consider_all_requests_local = false + # in config/development.rb for these pages to actually show up. + + render status: :internal_server_error, formats: %i[html] + end end diff --git a/app/app/views/pages/error_404.html.erb b/app/app/views/pages/error_404.html.erb new file mode 100644 index 00000000..cc21c9cc --- /dev/null +++ b/app/app/views/pages/error_404.html.erb @@ -0,0 +1,13 @@ +<% header = t(".header") %> +<% content_for :title, header %> +

<%= header %>

+ +
+

<%= t(".error_code_html") %>

+ + <% if @cbv_flow && @cbv_flow.cbv_flow_invitation %> + <%= link_to t(".return_to_entry"), @cbv_flow.cbv_flow_invitation.to_url %> + <% else %> + <%= link_to t(".return_to_welcome"), root_url %> + <% end %> +
diff --git a/app/app/views/pages/error_500.html.erb b/app/app/views/pages/error_500.html.erb new file mode 100644 index 00000000..31842f8a --- /dev/null +++ b/app/app/views/pages/error_500.html.erb @@ -0,0 +1,13 @@ +<% header = t(".header") %> +<% content_for :title, header %> +

<%= header %>

+ +
+

<%= t(".error_code_html") %>

+ +

<%= t(".description") %>

+ +

+ <%= link_to t(".refresh"), "javascript:window.location.reload()" %> +

+
diff --git a/app/config/application.rb b/app/config/application.rb index c1af7641..79311d5f 100644 --- a/app/config/application.rb +++ b/app/config/application.rb @@ -35,6 +35,9 @@ class Application < Rails::Application # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") + # Allow specifying /404 and /500 routes for error pages + config.exceptions_app = self.routes + # Don't generate system test files. config.generators.system_tests = nil config.autoload_paths += %W[#{config.root}/lib] diff --git a/app/config/i18n-tasks.yml b/app/config/i18n-tasks.yml index 2125a508..f1cb2d12 100644 --- a/app/config/i18n-tasks.yml +++ b/app/config/i18n-tasks.yml @@ -138,6 +138,7 @@ ignore_missing: - "cbv.expired_invitations.show.body_2" - "help.*" - "shared.header.help" + - "pages.{error_404,error_500}.*" # - 'errors.messages.{accepted,blank,invalid,too_short,too_long}' # - '{devise,simple_form}.*' diff --git a/app/config/locales/en.yml b/app/config/locales/en.yml index 2d7f464e..b2659df1 100644 --- a/app/config/locales/en.yml +++ b/app/config/locales/en.yml @@ -517,6 +517,16 @@ en: title: Contact your employer's Human Resource Team title: I don't know my username pages: + error_404: + error_code_html: 'Error code: 404' + header: We can't find the page you're looking for + return_to_entry: Return to entry page + return_to_welcome: Return to welcome page + error_500: + description: This is a problem on our end. There was an error with the server and our team has been notified. Please refresh your page, or try again later. + error_code_html: 'Error code: 500' + header: It looks like something went wrong + refresh: Refresh page home: description_1: The SNAP Income Pilot is a new tool designed to help you connect your income details from your employer or payroll provider directly to your SNAP agency. description_2: Please note this pilot is not currently available. diff --git a/app/config/routes.rb b/app/config/routes.rb index b877cddc..9d6838c9 100644 --- a/app/config/routes.rb +++ b/app/config/routes.rb @@ -70,4 +70,7 @@ post :user_action, to: "user_events#user_action" end end + + match "/404", to: "pages#error_404", via: :all + match "/500", to: "pages#error_500", via: :all end diff --git a/app/public/404.html b/app/public/404.html deleted file mode 100644 index 2be3af26..00000000 --- a/app/public/404.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - The page you were looking for doesn't exist (404) - - - - - - -
-
-

The page you were looking for doesn't exist.

-

You may have mistyped the address or the page may have moved.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/app/public/422.html b/app/public/422.html deleted file mode 100644 index c08eac0d..00000000 --- a/app/public/422.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - The change you wanted was rejected (422) - - - - - - -
-
-

The change you wanted was rejected.

-

Maybe you tried to change something you didn't have access to.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/app/public/500.html b/app/public/500.html deleted file mode 100644 index 78a030af..00000000 --- a/app/public/500.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - We're sorry, but something went wrong (500) - - - - - - -
-
-

We're sorry, but something went wrong.

-
-

If you are the application owner check the logs for more information.

-
- - diff --git a/app/spec/controllers/pages_controller_spec.rb b/app/spec/controllers/pages_controller_spec.rb new file mode 100644 index 00000000..d3b4450c --- /dev/null +++ b/app/spec/controllers/pages_controller_spec.rb @@ -0,0 +1,40 @@ +require "rails_helper" + +RSpec.describe PagesController do + render_views + + describe "#home" do + it "renders" do + get :home + expect(response).to be_successful + expect(response.body).to include("Welcome") + end + end + + describe "#error_404" do + it "renders with a link to the homepage" do + get :error_404 + expect(response.status).to eq(404) + expect(response.body).to include("We can't find the page") + expect(response.body).to include("Return to welcome") + end + + describe "when a cbv_flow_id is in the session" do + let(:cbv_flow) { create(:cbv_flow) } + + it "renders with a link to restart that CBV flow" do + get :error_404, session: { cbv_flow_id: cbv_flow.id } + expect(response.status).to eq(404) + expect(response.body).to include("Return to entry page") + end + end + end + + describe "#error_500" do + it "renders" do + get :error_500 + expect(response.status).to eq(500) + expect(response.body).to include("It looks like something went wrong") + end + end +end diff --git a/app/zap.conf b/app/zap.conf index 8248449f..f8db3335 100644 --- a/app/zap.conf +++ b/app/zap.conf @@ -1,6 +1,10 @@ # zap-full-scan rule configuration file +# # Change WARN to IGNORE to ignore rule or FAIL to fail if rule matches # Active scan rules set to IGNORE will not be run which will speed up the scan +# Use OUTOFSCOPE with a regular expression to exclude URLs from triggering rules +# (see https://github.com/zaproxy/zaproxy/blob/d67299a/docker/zap_common.py#L162) +# # Only the rule identifiers are used - the names are just for info # You can add your own messages to each rule by appending them after a tab on each line. 0 WARN (Directory Browsing - Active/release) @@ -37,6 +41,7 @@ 10036 WARN (HTTP Server Response Header - Passive/beta) 10037 WARN (Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s) - Passive/release) 10038 FAIL (Content Security Policy (CSP) Header Not Set - Passive/beta) +10038 OUTOFSCOPE .*/sitemap\.xml 10039 WARN (X-Backend-Server Header Information Leak - Passive/beta) 10040 FAIL (Secure Pages Include Mixed Content - Passive/release) 10041 WARN (HTTP to HTTPS Insecure Transition in Form Post - Passive/beta) diff --git a/zap_options.conf b/zap_options.conf deleted file mode 100644 index 74ae025e..00000000 --- a/zap_options.conf +++ /dev/null @@ -1,3 +0,0 @@ -network.globalExclusions.exclusions(0).exclusion.name=Search -network.globalExclusions.exclusions(0).exclusion.enabled=true -network.globalExclusions.exclusions(0).exclusion.value=/.*\/(?:.{2}\/)?cbv\/employer_search\?authenticity\_token.*/gm From ad07a6840b39ae8e1e0fcc4d47cce367c6905acc Mon Sep 17 00:00:00 2001 From: George Byers II Date: Mon, 24 Feb 2025 17:23:19 -0500 Subject: [PATCH 5/7] FFS-2453: Replace Help Modal iframe with Turbo Frame (#459) Co-authored-by: George Byers --- app/app/assets/stylesheets/cbv.scss | 20 --------- app/app/controllers/application_controller.rb | 8 ++-- app/app/controllers/help_controller.rb | 3 ++ app/app/javascript/controllers/help.js | 44 ++++++------------- app/app/views/help/_help_modal.html.erb | 16 +++---- app/app/views/help/_help_topic_link.html.erb | 3 +- app/app/views/help/index.html.erb | 32 +++++++------- app/app/views/help/show.html.erb | 21 +++++---- 8 files changed, 57 insertions(+), 90 deletions(-) diff --git a/app/app/assets/stylesheets/cbv.scss b/app/app/assets/stylesheets/cbv.scss index 50d64ad8..3e2bdf6c 100644 --- a/app/app/assets/stylesheets/cbv.scss +++ b/app/app/assets/stylesheets/cbv.scss @@ -136,26 +136,6 @@ html { /** * Help Modal */ -#help-iframe { - min-height: 600px; - line-height: 1; - - // Small screens such as mobile - @media screen and (max-width: 375px) { - height: 73vh; - } - - // Medium screens such as tablets - @media screen and (min-width: 810px) { - height: 630px; - } - - // Larger screens such as desktop - @include at-media(desktop) { - height: 660px; - } -} - .main-bullets { list-style-type: disc; } diff --git a/app/app/controllers/application_controller.rb b/app/app/controllers/application_controller.rb index 1ef0977f..c03ab921 100644 --- a/app/app/controllers/application_controller.rb +++ b/app/app/controllers/application_controller.rb @@ -92,11 +92,9 @@ def redirect_if_maintenance_mode def check_help_param if params[:help] == "true" help_link = helpers.render(partial: "help/help_link", locals: { text: t("help.alert.help_options"), source: "banner" }) - flash.merge!( - alert: "#{t('help.alert.text_before')} #{help_link}", - alert_heading: t("help.alert.heading"), - alert_type: "warning" - ) + flash.now[:alert] = "#{t('help.alert.text_before')} #{help_link}" + flash.now[:alert_heading] = t("help.alert.heading") + flash.now[:alert_type] = "warning" end end end diff --git a/app/app/controllers/help_controller.rb b/app/app/controllers/help_controller.rb index 43529205..a1e85b00 100644 --- a/app/app/controllers/help_controller.rb +++ b/app/app/controllers/help_controller.rb @@ -4,6 +4,7 @@ class HelpController < ApplicationController def index @title = t("help.index.title") + render layout: false end def show @@ -25,6 +26,8 @@ def show rescue => ex Rails.logger.error "Unable to track event (ApplicantViewedHelpTopic): #{ex}" end + + render layout: false if turbo_frame_request? end private diff --git a/app/app/javascript/controllers/help.js b/app/app/javascript/controllers/help.js index 03d6ec24..681c026f 100644 --- a/app/app/javascript/controllers/help.js +++ b/app/app/javascript/controllers/help.js @@ -1,40 +1,24 @@ -import { Controller } from "@hotwired/stimulus" -import { trackUserAction } from "../utilities/api" +import { Controller } from "@hotwired/stimulus"; +import { trackUserAction } from "../utilities/api"; export default class extends Controller { - static targets = ["iframe"] + static targets = ["content"]; - connect() { - this.handleClick = (event) => { - if (event.target.href?.includes("#help-modal")) { - trackUserAction("ApplicantOpenedHelpModal", { source: event.target.dataset.source }) - } + handleClick(event) { + if (event.target.href?.includes("#help-modal")) { + trackUserAction("ApplicantOpenedHelpModal", { + source: event.target.dataset.source, + }); + // reset the help modal src on mousedown to ensure the help modal src is reset to "/help" + document.querySelector("#help_modal_content").src = "/help"; } - - document.addEventListener("click", this.handleClick) } - disconnect() { - document.removeEventListener("click", this.handleClick) + connect() { + document.addEventListener("click", this.handleClick); } - /** - * This function is used to prepare the next URL for the iframe. - * It generates a new random parameter and updates the iframe src - * so that when the modal is opened, the iframe will load the - * default help topics rather than the last viewed topic. - */ - prepareNextUrl() { - const iframe = this.iframeTarget - const currentSrc = new URL(iframe.src) - - // Generate a new random parameter - const randomHex = Array.from(crypto.getRandomValues(new Uint8Array(2))) - .map(b => b.toString(16).padStart(2, "0")) - .join("") - - // Update the iframe src with new random parameter - currentSrc.searchParams.set("r", randomHex) - iframe.src = currentSrc.toString() + disconnect() { + document.removeEventListener("click", this.handleClick); } } diff --git a/app/app/views/help/_help_modal.html.erb b/app/app/views/help/_help_modal.html.erb index 0e8422a1..1b31a768 100644 --- a/app/app/views/help/_help_modal.html.erb +++ b/app/app/views/help/_help_modal.html.erb @@ -6,17 +6,11 @@ data-controller="help" >
-
-
- +
+
+ <%= turbo_frame_tag "help_content" do %> + <%= render template: "help/index" %> + <% end %>
diff --git a/app/app/views/help/_help_topic_link.html.erb b/app/app/views/help/_help_topic_link.html.erb index 7f0e123e..b5969b5e 100644 --- a/app/app/views/help/_help_topic_link.html.erb +++ b/app/app/views/help/_help_topic_link.html.erb @@ -1,4 +1,5 @@ <%= link_to help_topic_path(topic: topic, locale: I18n.locale), - class: "usa-button margin-bottom-1 height-5 text-left display-block" do %> + class: "usa-button margin-bottom-1 height-5 text-left display-block", + data: { turbo_frame: "help_modal_content" } do %> <%= text %> <% end %> diff --git a/app/app/views/help/index.html.erb b/app/app/views/help/index.html.erb index a15cbfba..315372a9 100644 --- a/app/app/views/help/index.html.erb +++ b/app/app/views/help/index.html.erb @@ -1,20 +1,22 @@ -
-

<%= t("help.index.title") %>

-
-

<%= t("help.index.select_prompt") %>

+<%= turbo_frame_tag "help_modal_content" do %> +
+

<%= t("help.index.title") %>

+
+

<%= t("help.index.select_prompt") %>

- <%= render "help_topic_link", topic: "username", text: t("help.index.username") %> - <%= render "help_topic_link", topic: "password", text: t("help.index.password") %> - <%= render "help_topic_link", topic: "company-id", text: t("help.index.company_id") %> - <%= render "help_topic_link", topic: "employer", text: t("help.index.employer") %> - <%= render "help_topic_link", topic: "provider", text: t("help.index.provider") %> - <%= render "help_topic_link", topic: "credentials", text: t("help.index.credentials") %> + <%= render "help/help_topic_link", topic: "username", text: t("help.index.username") %> + <%= render "help/help_topic_link", topic: "password", text: t("help.index.password") %> + <%= render "help/help_topic_link", topic: "company-id", text: t("help.index.company_id") %> + <%= render "help/help_topic_link", topic: "employer", text: t("help.index.employer") %> + <%= render "help/help_topic_link", topic: "provider", text: t("help.index.provider") %> + <%= render "help/help_topic_link", topic: "credentials", text: t("help.index.credentials") %> - <% if current_agency && current_agency.caseworker_feedback_form.present? %> - <%= link_to feedback_form_url, class: "usa-button margin-bottom-1 height-5 text-left display-block", target: "_blank" do %> - <%= t("help.index.feedback") %> + <% if current_agency && current_agency.caseworker_feedback_form.present? %> + <%= link_to feedback_form_url, class: "usa-button margin-bottom-1 height-5 text-left display-block", target: "_blank" do %> + <%= t("help.index.feedback") %> + <% end %> <% end %> - <% end %> +
-
+<% end %> diff --git a/app/app/views/help/show.html.erb b/app/app/views/help/show.html.erb index 922eb1ad..90e7698f 100644 --- a/app/app/views/help/show.html.erb +++ b/app/app/views/help/show.html.erb @@ -1,11 +1,16 @@ -
-

- <%= t("help.show.#{@help_topic}.title") %> -

+<%= turbo_frame_tag "help_modal_content" do %> +
+

+ <%= t("help.show.#{@help_topic}.title") %> +

- <%= render partial: "help_topic_content", locals: { topic: @help_topic } %> + <%= render partial: "help_topic_content", locals: { topic: @help_topic } %> -
- <%= link_to t("help.show.go_back"), help_path, class: "usa-link" %> +
+ <%= link_to t("help.show.go_back"), + help_path, + class: "usa-link", + data: { turbo_frame: "help_modal_content" } %> +
-
+<% end %> From 1045804894f7cb0a52a0c6fbb88248ff4313f5de Mon Sep 17 00:00:00 2001 From: Matt Gardner Date: Thu, 27 Feb 2025 11:49:10 -0500 Subject: [PATCH 6/7] FFS-2466: Rename PinwheelAccount model to PayrollAccount (#470) * Rename pinwheel account to payroll account * Rename table * Rename table --- app/app/channels/paystubs_channel.rb | 2 +- .../cbv/employer_searches_controller.rb | 2 +- .../cbv/missing_results_controller.rb | 2 +- .../cbv/payment_details_controller.rb | 2 +- .../controllers/cbv/summaries_controller.rb | 8 +-- .../cbv/synchronizations_controller.rb | 2 +- .../webhooks/pinwheel/events_controller.rb | 10 ++-- app/app/helpers/cbv/pinwheel_data_helper.rb | 12 ++--- app/app/javascript/utilities/api.js | 3 +- app/app/models/cbv_flow.rb | 4 +- ...pinwheel_account.rb => payroll_account.rb} | 2 +- ...27162123_rename_pinwheel_accounts_table.rb | 5 ++ app/db/schema.rb | 8 +-- .../cbv/employer_searches_controller_spec.rb | 6 +-- .../cbv/entries_controller_spec.rb | 2 +- .../cbv/missing_results_controller_spec.rb | 2 +- .../cbv/payment_details_controller_spec.rb | 12 ++--- .../cbv/summaries_controller_spec.rb | 2 +- .../cbv/synchronization_failures_spec.rb | 6 +-- .../controllers/cbv/synchronizations_spec.rb | 10 ++-- .../pinwheel/events_controller_spec.rb | 20 +++---- app/spec/factories/cbv_flow.rb | 4 +- ...pinwheel_account.rb => payroll_account.rb} | 2 +- .../helpers/cbv/pinwheel_data_helper_spec.rb | 4 +- app/spec/mailers/caseworker_mailer_spec.rb | 2 +- .../previews/caseworker_mailer_preview.rb | 8 +-- ...ccount_spec.rb => payroll_account_spec.rb} | 50 +++++++++--------- app/spec/services/pdf_service_spec.rb | 6 +-- docs/app/rendered/database-schema.pdf | Bin 39264 -> 38286 bytes 29 files changed, 101 insertions(+), 97 deletions(-) rename app/app/models/{pinwheel_account.rb => payroll_account.rb} (97%) create mode 100644 app/db/migrate/20250227162123_rename_pinwheel_accounts_table.rb rename app/spec/factories/{pinwheel_account.rb => payroll_account.rb} (87%) rename app/spec/models/{pinwheel_account_spec.rb => payroll_account_spec.rb} (54%) diff --git a/app/app/channels/paystubs_channel.rb b/app/app/channels/paystubs_channel.rb index ca6e4639..09e56f55 100644 --- a/app/app/channels/paystubs_channel.rb +++ b/app/app/channels/paystubs_channel.rb @@ -9,7 +9,7 @@ def subscribed private def check_pinwheel_account_synchrony - pinwheel_account = PinwheelAccount.find_by_pinwheel_account_id(params["account_id"]) + pinwheel_account = PayrollAccount.find_by_pinwheel_account_id(params["account_id"]) if pinwheel_account.present? broadcast_to(@cbv_flow, { diff --git a/app/app/controllers/cbv/employer_searches_controller.rb b/app/app/controllers/cbv/employer_searches_controller.rb index 2b03bee0..5e6a373f 100644 --- a/app/app/controllers/cbv/employer_searches_controller.rb +++ b/app/app/controllers/cbv/employer_searches_controller.rb @@ -7,7 +7,7 @@ class Cbv::EmployerSearchesController < Cbv::BaseController def show @query = search_params[:query] @employers = @query.blank? ? [] : provider_search(@query) - @has_pinwheel_account = @cbv_flow.pinwheel_accounts.any? + @has_pinwheel_account = @cbv_flow.payroll_accounts.any? @selected_tab = search_params[:type] || "payroll" case search_params[:type] diff --git a/app/app/controllers/cbv/missing_results_controller.rb b/app/app/controllers/cbv/missing_results_controller.rb index 2dc46be5..11982a57 100644 --- a/app/app/controllers/cbv/missing_results_controller.rb +++ b/app/app/controllers/cbv/missing_results_controller.rb @@ -2,7 +2,7 @@ class Cbv::MissingResultsController < Cbv::BaseController before_action :track_missing_results_event, only: :show def show - @has_pinwheel_account = @cbv_flow.pinwheel_accounts.any? + @has_pinwheel_account = @cbv_flow.payroll_accounts.any? end def track_missing_results_event diff --git a/app/app/controllers/cbv/payment_details_controller.rb b/app/app/controllers/cbv/payment_details_controller.rb index 2bef3750..b1c2b43f 100644 --- a/app/app/controllers/cbv/payment_details_controller.rb +++ b/app/app/controllers/cbv/payment_details_controller.rb @@ -17,7 +17,7 @@ class Cbv::PaymentDetailsController < Cbv::BaseController def show account_id = params[:user][:account_id] - @pinwheel_account = @cbv_flow.pinwheel_accounts.find_by(pinwheel_account_id: account_id) + @pinwheel_account = @cbv_flow.payroll_accounts.find_by(pinwheel_account_id: account_id) # security check - make sure the account_id is associated with the current cbv_flow_id if @pinwheel_account.nil? diff --git a/app/app/controllers/cbv/summaries_controller.rb b/app/app/controllers/cbv/summaries_controller.rb index 0fb37672..e571923f 100644 --- a/app/app/controllers/cbv/summaries_controller.rb +++ b/app/app/controllers/cbv/summaries_controller.rb @@ -119,7 +119,7 @@ def transmit_to_caseworker employments: @employments, incomes: @incomes, identities: @identities, - payments_grouped_by_employer: summarize_by_employer(@payments, @employments, @incomes, @identities, @cbv_flow.pinwheel_accounts), + payments_grouped_by_employer: summarize_by_employer(@payments, @employments, @incomes, @identities, @cbv_flow.payroll_accounts), has_consent: has_consent } ) @@ -164,7 +164,7 @@ def transmit_to_caseworker end def generate_csv - pinwheel_account = PinwheelAccount.find_by(cbv_flow_id: @cbv_flow.id) + pinwheel_account = PayrollAccount.find_by(cbv_flow_id: @cbv_flow.id) data = { client_id: @cbv_flow.cbv_applicant.agency_id_number, @@ -195,7 +195,7 @@ def track_transmitted_event(cbv_flow, payments) cbv_applicant_id: cbv_flow.cbv_applicant_id, cbv_flow_id: cbv_flow.id, invitation_id: cbv_flow.cbv_flow_invitation_id, - account_count: cbv_flow.pinwheel_accounts.count, + account_count: cbv_flow.payroll_accounts.count, paystub_count: payments.count, account_count_with_additional_information: cbv_flow.additional_information.values.count { |info| info["comment"].present? }, @@ -213,7 +213,7 @@ def track_accessed_income_summary_event(cbv_flow, payments) cbv_flow_id: cbv_flow.id, cbv_applicant_id: cbv_flow.cbv_applicant_id, invitation_id: cbv_flow.cbv_flow_invitation_id, - account_count: cbv_flow.pinwheel_accounts.count, + account_count: cbv_flow.payroll_accounts.count, paystub_count: payments.count, account_count_with_additional_information: cbv_flow.additional_information.values.count { |info| info["comment"].present? }, diff --git a/app/app/controllers/cbv/synchronizations_controller.rb b/app/app/controllers/cbv/synchronizations_controller.rb index f4e2b3d5..1e2511b8 100644 --- a/app/app/controllers/cbv/synchronizations_controller.rb +++ b/app/app/controllers/cbv/synchronizations_controller.rb @@ -27,6 +27,6 @@ def redirect_if_sync_finished def set_pinwheel_account account_id = params[:user][:account_id] - @pinwheel_account = @cbv_flow.pinwheel_accounts.find_by(pinwheel_account_id: account_id) + @pinwheel_account = @cbv_flow.payroll_accounts.find_by(pinwheel_account_id: account_id) end end diff --git a/app/app/controllers/webhooks/pinwheel/events_controller.rb b/app/app/controllers/webhooks/pinwheel/events_controller.rb index 6c05ebf2..7c06abe5 100644 --- a/app/app/controllers/webhooks/pinwheel/events_controller.rb +++ b/app/app/controllers/webhooks/pinwheel/events_controller.rb @@ -12,20 +12,20 @@ def create if params["event"] == "account.added" supported_jobs = get_supported_jobs(params["payload"]["platform_id"]) - PinwheelAccount + PayrollAccount .create_with(cbv_flow: @cbv_flow, supported_jobs: supported_jobs) .find_or_create_by(pinwheel_account_id: params["payload"]["account_id"]) track_account_created_event(@cbv_flow, params["payload"]["platform_name"]) end - if PinwheelAccount::EVENTS_MAP.keys.include?(params["event"]) - pinwheel_account = PinwheelAccount.find_by_pinwheel_account_id(params["payload"]["account_id"]) + if PayrollAccount::EVENTS_MAP.keys.include?(params["event"]) + pinwheel_account = PayrollAccount.find_by_pinwheel_account_id(params["payload"]["account_id"]) if pinwheel_account.present? - pinwheel_account.update!(PinwheelAccount::EVENTS_MAP[params["event"]] => Time.now) + pinwheel_account.update!(PayrollAccount::EVENTS_MAP[params["event"]] => Time.now) if params.dig("payload", "outcome") == "error" || params.dig("payload", "outcome") == "pending" - pinwheel_account.update!(PinwheelAccount::EVENTS_ERRORS_MAP[params["event"]] => Time.now) + pinwheel_account.update!(PayrollAccount::EVENTS_ERRORS_MAP[params["event"]] => Time.now) end if pinwheel_account.has_fully_synced? diff --git a/app/app/helpers/cbv/pinwheel_data_helper.rb b/app/app/helpers/cbv/pinwheel_data_helper.rb index 25a27b06..cb66d210 100644 --- a/app/app/helpers/cbv/pinwheel_data_helper.rb +++ b/app/app/helpers/cbv/pinwheel_data_helper.rb @@ -16,7 +16,7 @@ def set_payments(account_id = nil) end def set_employments - @employments = @cbv_flow.pinwheel_accounts.map do |pinwheel_account| + @employments = @cbv_flow.payroll_accounts.map do |pinwheel_account| next unless pinwheel_account.job_succeeded?("employment") pinwheel.fetch_employment(account_id: pinwheel_account.pinwheel_account_id) @@ -24,7 +24,7 @@ def set_employments end def set_incomes - @incomes = @cbv_flow.pinwheel_accounts.map do |pinwheel_account| + @incomes = @cbv_flow.payroll_accounts.map do |pinwheel_account| next unless pinwheel_account.job_succeeded?("income") pinwheel.fetch_income(account_id: pinwheel_account.pinwheel_account_id) @@ -32,7 +32,7 @@ def set_incomes end def set_identities - @identities = @cbv_flow.pinwheel_accounts.map do |pinwheel_account| + @identities = @cbv_flow.payroll_accounts.map do |pinwheel_account| next unless pinwheel_account.job_succeeded?("identity") pinwheel.fetch_identity(account_id: pinwheel_account.pinwheel_account_id) @@ -43,7 +43,7 @@ def hours_by_earning_category(earnings) end def payments_grouped_by_employer - summarize_by_employer(@payments, @employments, @incomes, @identities, @cbv_flow.pinwheel_accounts) + summarize_by_employer(@payments, @employments, @incomes, @identities, @cbv_flow.payroll_accounts) end def total_gross_income @@ -74,7 +74,7 @@ def summarize_by_employer(payments, employments, incomes, identities, pinwheel_a private def fetch_paystubs(from_pay_date, to_pay_date) - @cbv_flow.pinwheel_accounts.flat_map do |pinwheel_account| + @cbv_flow.payroll_accounts.flat_map do |pinwheel_account| next [] unless pinwheel_account.job_succeeded?("paystubs") fetch_paystubs_for_account_id(pinwheel_account.pinwheel_account_id, from_pay_date, to_pay_date) @@ -90,7 +90,7 @@ def fetch_paystubs_for_account_id(account_id, from_pay_date, to_pay_date) end def does_pinwheel_account_support_job?(account_id, job) - pinwheel_account = PinwheelAccount.find_by_pinwheel_account_id(account_id) + pinwheel_account = PayrollAccount.find_by_pinwheel_account_id(account_id) return false unless pinwheel_account pinwheel_account.job_succeeded?(job) diff --git a/app/app/javascript/utilities/api.js b/app/app/javascript/utilities/api.js index 8ee35fc1..0fdb5cf0 100644 --- a/app/app/javascript/utilities/api.js +++ b/app/app/javascript/utilities/api.js @@ -15,10 +15,9 @@ export const trackUserAction = async (eventName, attributes) => { }).then(response => response.json()); } - export const fetchToken = (response_type, id, locale) => { return fetchInternal(PINWHEEL_TOKENS_GENERATE, { method: 'post', body: JSON.stringify({ response_type, id, locale }), }) -}; \ No newline at end of file +}; diff --git a/app/app/models/cbv_flow.rb b/app/app/models/cbv_flow.rb index 54d76ffb..8ab96818 100644 --- a/app/app/models/cbv_flow.rb +++ b/app/app/models/cbv_flow.rb @@ -1,5 +1,5 @@ class CbvFlow < ApplicationRecord - has_many :pinwheel_accounts, dependent: :destroy + has_many :payroll_accounts, dependent: :destroy belongs_to :cbv_flow_invitation, optional: true belongs_to :cbv_applicant, optional: true validates :client_agency_id, inclusion: Rails.application.config.client_agencies.client_agency_ids @@ -28,6 +28,6 @@ def self.create_from_invitation(cbv_flow_invitation) end def has_account_with_required_data? - pinwheel_accounts.any?(&:has_required_data?) + payroll_accounts.any?(&:has_required_data?) end end diff --git a/app/app/models/pinwheel_account.rb b/app/app/models/payroll_account.rb similarity index 97% rename from app/app/models/pinwheel_account.rb rename to app/app/models/payroll_account.rb index 13755d5f..64cbb92e 100644 --- a/app/app/models/pinwheel_account.rb +++ b/app/app/models/payroll_account.rb @@ -1,4 +1,4 @@ -class PinwheelAccount < ApplicationRecord +class PayrollAccount < ApplicationRecord belongs_to :cbv_flow after_update_commit { diff --git a/app/db/migrate/20250227162123_rename_pinwheel_accounts_table.rb b/app/db/migrate/20250227162123_rename_pinwheel_accounts_table.rb new file mode 100644 index 00000000..05ae4a0e --- /dev/null +++ b/app/db/migrate/20250227162123_rename_pinwheel_accounts_table.rb @@ -0,0 +1,5 @@ +class RenamePinwheelAccountsTable < ActiveRecord::Migration[7.1] + def change + rename_table :pinwheel_accounts, :payroll_accounts + end +end diff --git a/app/db/schema.rb b/app/db/schema.rb index aa6d9b34..ffeedbe4 100644 --- a/app/db/schema.rb +++ b/app/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_02_06_012936) do +ActiveRecord::Schema[7.1].define(version: 2025_02_27_162123) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -80,7 +80,7 @@ t.index ["cbv_flow_invitation_id"], name: "index_cbv_flows_on_cbv_flow_invitation_id" end - create_table "pinwheel_accounts", force: :cascade do |t| + create_table "payroll_accounts", force: :cascade do |t| t.bigint "cbv_flow_id", null: false t.string "pinwheel_account_id" t.datetime "paystubs_synced_at", precision: nil @@ -94,7 +94,7 @@ t.datetime "paystubs_errored_at", precision: nil t.datetime "identity_errored_at", precision: nil t.datetime "identity_synced_at", precision: nil - t.index ["cbv_flow_id"], name: "index_pinwheel_accounts_on_cbv_flow_id" + t.index ["cbv_flow_id"], name: "index_payroll_accounts_on_cbv_flow_id" end create_table "users", force: :cascade do |t| @@ -119,5 +119,5 @@ add_foreign_key "cbv_flow_invitations", "users" add_foreign_key "cbv_flows", "cbv_flow_invitations" - add_foreign_key "pinwheel_accounts", "cbv_flows" + add_foreign_key "payroll_accounts", "cbv_flows" end diff --git a/app/spec/controllers/cbv/employer_searches_controller_spec.rb b/app/spec/controllers/cbv/employer_searches_controller_spec.rb index 21fd8dc8..5daef230 100644 --- a/app/spec/controllers/cbv/employer_searches_controller_spec.rb +++ b/app/spec/controllers/cbv/employer_searches_controller_spec.rb @@ -100,9 +100,9 @@ render_views - context "when the user at least one pinwheel_account associated with their cbv_flow" do + context "when the user at least one payroll_account associated with their cbv_flow" do it "renders the view with a link to the summary page" do - create(:pinwheel_account, cbv_flow_id: cbv_flow.id) + create(:payroll_account, cbv_flow_id: cbv_flow.id) get :show, params: { query: "no_results" } expect(response).to be_successful expect(response.body).to include("continue to review your income report") @@ -110,7 +110,7 @@ end end - context "when the user has does not have a pinwheel_account associated with their cbv_flow" do + context "when the user has does not have a payroll_account associated with their cbv_flow" do it "renders the view with a link to exit income verification" do get :show, params: { query: "no_results" } expect(response).to be_successful diff --git a/app/spec/controllers/cbv/entries_controller_spec.rb b/app/spec/controllers/cbv/entries_controller_spec.rb index fb9476d3..b045b2ba 100644 --- a/app/spec/controllers/cbv/entries_controller_spec.rb +++ b/app/spec/controllers/cbv/entries_controller_spec.rb @@ -134,7 +134,7 @@ existing_cbv_flow.update(confirmation_code: "FOOBAR") end let!(:connected_account) do - create(:pinwheel_account, + create(:payroll_account, cbv_flow: existing_cbv_flow, pinwheel_account_id: SecureRandom.uuid, created_at: 4.minutes.ago diff --git a/app/spec/controllers/cbv/missing_results_controller_spec.rb b/app/spec/controllers/cbv/missing_results_controller_spec.rb index e94c1457..63dec1c3 100644 --- a/app/spec/controllers/cbv/missing_results_controller_spec.rb +++ b/app/spec/controllers/cbv/missing_results_controller_spec.rb @@ -16,7 +16,7 @@ end context "when the user has already linked a pinwheel account" do - let!(:pinwheel_account) { create(:pinwheel_account, cbv_flow: cbv_flow) } + let!(:payroll_account) { create(:payroll_account, cbv_flow: cbv_flow) } it "renders successfully" do get :show diff --git a/app/spec/controllers/cbv/payment_details_controller_spec.rb b/app/spec/controllers/cbv/payment_details_controller_spec.rb index a1b8a6b1..bc1d7bea 100644 --- a/app/spec/controllers/cbv/payment_details_controller_spec.rb +++ b/app/spec/controllers/cbv/payment_details_controller_spec.rb @@ -13,9 +13,9 @@ let(:income_errored_at) { nil } let(:paystubs_errored_at) { nil } let(:employment_errored_at) { nil } - let!(:pinwheel_account) do + let!(:payroll_account) do create( - :pinwheel_account, + :payroll_account, cbv_flow: cbv_flow, pinwheel_account_id: account_id, supported_jobs: supported_jobs, @@ -45,7 +45,7 @@ .with("ApplicantViewedPaymentDetails", anything, hash_including( cbv_flow_id: cbv_flow.id, invitation_id: cbv_flow.cbv_flow_invitation_id, - pinwheel_account_id: pinwheel_account.id, + pinwheel_account_id: payroll_account.id, payments_length: 1, has_employment_data: true, has_paystubs_data: true, @@ -57,7 +57,7 @@ .with("ApplicantViewedPaymentDetails", anything, hash_including( cbv_flow_id: cbv_flow.id, invitation_id: cbv_flow.cbv_flow_invitation_id, - pinwheel_account_id: pinwheel_account.id, + pinwheel_account_id: payroll_account.id, payments_length: 1, has_employment_data: true, has_paystubs_data: true, @@ -197,8 +197,8 @@ end it "redirects to the entry page when the resolved pinwheel_account is present, but does not match the current session" do - existing_pinwheel_account = create(:pinwheel_account) - get :show, params: { user: { account_id: existing_pinwheel_account.pinwheel_account_id } } + existing_payroll_account = create(:payroll_account) + get :show, params: { user: { account_id: existing_payroll_account.pinwheel_account_id } } expect(response).to redirect_to(cbv_flow_entry_url) expect(flash[:slim_alert]).to be_present expect(flash[:slim_alert][:message]).to eq(I18n.t("cbv.error_no_access")) diff --git a/app/spec/controllers/cbv/summaries_controller_spec.rb b/app/spec/controllers/cbv/summaries_controller_spec.rb index 320a10d7..d1d3ad61 100644 --- a/app/spec/controllers/cbv/summaries_controller_spec.rb +++ b/app/spec/controllers/cbv/summaries_controller_spec.rb @@ -30,7 +30,7 @@ "public_key" => @public_key }) - cbv_flow.pinwheel_accounts.first.update(pinwheel_account_id: "03e29160-f7e7-4a28-b2d8-813640e030d3") + cbv_flow.payroll_accounts.first.update(pinwheel_account_id: "03e29160-f7e7-4a28-b2d8-813640e030d3") end around do |ex| diff --git a/app/spec/controllers/cbv/synchronization_failures_spec.rb b/app/spec/controllers/cbv/synchronization_failures_spec.rb index aba91eea..4de67102 100644 --- a/app/spec/controllers/cbv/synchronization_failures_spec.rb +++ b/app/spec/controllers/cbv/synchronization_failures_spec.rb @@ -11,7 +11,7 @@ end context "when the user has already linked a pinwheel account" do - let!(:pinwheel_account) { create(:pinwheel_account, cbv_flow: cbv_flow) } + let!(:payroll_account) { create(:payroll_account, cbv_flow: cbv_flow) } it "shows continue to report button" do get :show @@ -19,8 +19,8 @@ end end - context "when the user has no successful pinwheel_accounts" do - let!(:pinwheel_account) { create(:pinwheel_account, :with_paystubs_errored, cbv_flow: cbv_flow) } + context "when the user has no successful payroll_accounts" do + let!(:payroll_account) { create(:payroll_account, :with_paystubs_errored, cbv_flow: cbv_flow) } it "shows cta button" do get :show diff --git a/app/spec/controllers/cbv/synchronizations_spec.rb b/app/spec/controllers/cbv/synchronizations_spec.rb index a420d9aa..8a630ea4 100644 --- a/app/spec/controllers/cbv/synchronizations_spec.rb +++ b/app/spec/controllers/cbv/synchronizations_spec.rb @@ -5,7 +5,7 @@ let(:cbv_flow) { create(:cbv_flow) } - let(:pinwheel_account) { create(:pinwheel_account, cbv_flow: cbv_flow) } + let(:payroll_account) { create(:payroll_account, cbv_flow: cbv_flow) } before do session[:cbv_flow_id] = cbv_flow.id @@ -13,16 +13,16 @@ describe "#update" do it "redirects to the payment details page" do - patch :update, params: { user: { account_id: pinwheel_account.pinwheel_account_id } } + patch :update, params: { user: { account_id: payroll_account.pinwheel_account_id } } - expect(response).to redirect_to(cbv_flow_payment_details_path(user: { account_id: pinwheel_account.pinwheel_account_id })) + expect(response).to redirect_to(cbv_flow_payment_details_path(user: { account_id: payroll_account.pinwheel_account_id })) end context "when the paystubs synchronization fails" do it "redirects to the synchronization failures page" do - pinwheel_account = create(:pinwheel_account, :with_paystubs_errored, cbv_flow: cbv_flow) + payroll_account = create(:payroll_account, :with_paystubs_errored, cbv_flow: cbv_flow) - patch :update, params: { user: { account_id: pinwheel_account.pinwheel_account_id } } + patch :update, params: { user: { account_id: payroll_account.pinwheel_account_id } } expect(response).to redirect_to(cbv_flow_synchronization_failures_path) end diff --git a/app/spec/controllers/webhooks/pinwheel/events_controller_spec.rb b/app/spec/controllers/webhooks/pinwheel/events_controller_spec.rb index 36b2104b..b76463e1 100644 --- a/app/spec/controllers/webhooks/pinwheel/events_controller_spec.rb +++ b/app/spec/controllers/webhooks/pinwheel/events_controller_spec.rb @@ -60,9 +60,9 @@ expect do post :create, params: valid_params - end.to change(PinwheelAccount, :count).by(1) + end.to change(PayrollAccount, :count).by(1) - pinwheel_account = PinwheelAccount.last + pinwheel_account = PayrollAccount.last expect(pinwheel_account).to have_attributes( cbv_flow_id: cbv_flow.id, supported_jobs: include(*supported_jobs), @@ -78,7 +78,7 @@ it "discards the webhook" do expect do post :create, params: valid_params - end.not_to change(PinwheelAccount, :count) + end.not_to change(PayrollAccount, :count) expect(response).to be_unauthorized end @@ -94,17 +94,17 @@ "outcome" => "success" } end - let(:pinwheel_account) { PinwheelAccount.create!(cbv_flow: cbv_flow, supported_jobs: supported_jobs, pinwheel_account_id: account_id) } + let(:payroll_account) { PayrollAccount.create!(cbv_flow: cbv_flow, supported_jobs: supported_jobs, pinwheel_account_id: account_id) } - it "updates the PinwheelAccount object with the current timestamp" do + it "updates the PayrollAccount object with the current timestamp" do expect { post :create, params: valid_params } - .to change { pinwheel_account.reload.paystubs_synced_at } + .to change { payroll_account.reload.paystubs_synced_at } .from(nil) .to(within(1.second).of(Time.now)) end it "sends events when fully synced" do - pinwheel_account.update( + payroll_account.update( created_at: 5.minutes.ago, employment_synced_at: Time.now, income_synced_at: Time.now, @@ -155,11 +155,11 @@ "outcome" => "pending" } end - let(:pinwheel_account) { PinwheelAccount.create!(cbv_flow: cbv_flow, supported_jobs: supported_jobs, pinwheel_account_id: account_id) } + let(:payroll_account) { PayrollAccount.create!(cbv_flow: cbv_flow, supported_jobs: supported_jobs, pinwheel_account_id: account_id) } - it "updates the PinwheelAccount object with an error state" do + it "updates the PayrollAccount object with an error state" do expect { post :create, params: valid_params } - .to change { pinwheel_account.reload.paystubs_errored_at } + .to change { payroll_account.reload.paystubs_errored_at } .from(nil) .to(within(1.second).of(Time.now)) end diff --git a/app/spec/factories/cbv_flow.rb b/app/spec/factories/cbv_flow.rb index b6550ee2..837311a9 100644 --- a/app/spec/factories/cbv_flow.rb +++ b/app/spec/factories/cbv_flow.rb @@ -18,8 +18,8 @@ end after(:build) do |cbv_flow, evaluator| - cbv_flow.pinwheel_accounts = [ - create(:pinwheel_account, cbv_flow: cbv_flow, supported_jobs: evaluator.supported_jobs, employment_errored_at: evaluator.employment_errored_at) + cbv_flow.payroll_accounts = [ + create(:payroll_account, cbv_flow: cbv_flow, supported_jobs: evaluator.supported_jobs, employment_errored_at: evaluator.employment_errored_at) ] end end diff --git a/app/spec/factories/pinwheel_account.rb b/app/spec/factories/payroll_account.rb similarity index 87% rename from app/spec/factories/pinwheel_account.rb rename to app/spec/factories/payroll_account.rb index 15e358bd..69b7d49c 100644 --- a/app/spec/factories/pinwheel_account.rb +++ b/app/spec/factories/payroll_account.rb @@ -1,5 +1,5 @@ FactoryBot.define do - factory :pinwheel_account, class: "PinwheelAccount" do + factory :payroll_account, class: "PayrollAccount" do cbv_flow pinwheel_account_id { SecureRandom.uuid } paystubs_synced_at { DateTime.now } diff --git a/app/spec/helpers/cbv/pinwheel_data_helper_spec.rb b/app/spec/helpers/cbv/pinwheel_data_helper_spec.rb index b84231e4..ea885286 100644 --- a/app/spec/helpers/cbv/pinwheel_data_helper_spec.rb +++ b/app/spec/helpers/cbv/pinwheel_data_helper_spec.rb @@ -28,12 +28,12 @@ let!(:cbv_flow) { create(:cbv_flow, :with_pinwheel_account) } before do - cbv_flow.pinwheel_accounts.first.update(pinwheel_account_id: account_id) + cbv_flow.payroll_accounts.first.update(pinwheel_account_id: account_id) end describe "aggregate payments" do it "groups by employer" do - summarized = helper.summarize_by_employer(payments, [ employment ], [ incomes ], [ identities ], cbv_flow.pinwheel_accounts) + summarized = helper.summarize_by_employer(payments, [ employment ], [ incomes ], [ identities ], cbv_flow.payroll_accounts) expect(summarized).to be_a(Hash) expect(summarized).to include(account_id) expect(summarized[account_id]).to match(hash_including( diff --git a/app/spec/mailers/caseworker_mailer_spec.rb b/app/spec/mailers/caseworker_mailer_spec.rb index 55081d94..3d1643e2 100644 --- a/app/spec/mailers/caseworker_mailer_spec.rb +++ b/app/spec/mailers/caseworker_mailer_spec.rb @@ -12,7 +12,7 @@ consented_to_authorized_use_at: Time.now )} let(:caseworker_email) { cbv_flow.cbv_flow_invitation.user.email } - let(:account_id) { cbv_flow.pinwheel_accounts.first.pinwheel_account_id } + let(:account_id) { cbv_flow.payroll_accounts.first.pinwheel_account_id } let(:payments) { stub_payments(account_id) } let(:employments) { stub_employments(account_id) } let(:incomes) { stub_incomes(account_id) } diff --git a/app/spec/mailers/previews/caseworker_mailer_preview.rb b/app/spec/mailers/previews/caseworker_mailer_preview.rb index cb24c444..244fd2ca 100644 --- a/app/spec/mailers/previews/caseworker_mailer_preview.rb +++ b/app/spec/mailers/previews/caseworker_mailer_preview.rb @@ -13,10 +13,10 @@ def summary_email :completed, cbv_flow_invitation: invitation ) - payments = stub_post_processed_payments(cbv_flow.pinwheel_accounts.first.pinwheel_account_id) - employments = stub_employments(cbv_flow.pinwheel_accounts.first.pinwheel_account_id) - incomes = stub_incomes(cbv_flow.pinwheel_accounts.first.pinwheel_account_id) - identities = stub_identities(cbv_flow.pinwheel_accounts.first.pinwheel_account_id) + payments = stub_post_processed_payments(cbv_flow.payroll_accounts.first.pinwheel_account_id) + employments = stub_employments(cbv_flow.payroll_accounts.first.pinwheel_account_id) + incomes = stub_incomes(cbv_flow.payroll_accounts.first.pinwheel_account_id) + identities = stub_identities(cbv_flow.payroll_accounts.first.pinwheel_account_id) CaseworkerMailer.with( email_address: invitation.email_address, diff --git a/app/spec/models/pinwheel_account_spec.rb b/app/spec/models/payroll_account_spec.rb similarity index 54% rename from app/spec/models/pinwheel_account_spec.rb rename to app/spec/models/payroll_account_spec.rb index 64f83a9b..68cbc831 100644 --- a/app/spec/models/pinwheel_account_spec.rb +++ b/app/spec/models/payroll_account_spec.rb @@ -1,11 +1,11 @@ require 'rails_helper' -RSpec.describe PinwheelAccount, type: :model do +RSpec.describe PayrollAccount, type: :model do let(:account_id) { SecureRandom.uuid } let(:supported_jobs) { %w[income paystubs employment] } let!(:cbv_flow) { create(:cbv_flow, case_number: "ABC1234", pinwheel_token_id: "abc-def-ghi", client_agency_id: "sandbox") } - let!(:pinwheel_account) do - create(:pinwheel_account, + let!(:payroll_account) do + create(:payroll_account, cbv_flow: cbv_flow, pinwheel_account_id: account_id, supported_jobs: supported_jobs, @@ -21,15 +21,15 @@ describe "#has_fully_synced?" do context "when income is supported" do it "returns true when all are synced" do - pinwheel_account.update!(income_synced_at: Time.current) - expect(pinwheel_account.has_fully_synced?).to be_truthy + payroll_account.update!(income_synced_at: Time.current) + expect(payroll_account.has_fully_synced?).to be_truthy end context "when income_synced_at is nil" do let(:income_synced_at) { nil } it "returns false when income_synced_at is nil" do - expect(pinwheel_account.has_fully_synced?).to be_falsey + expect(payroll_account.has_fully_synced?).to be_falsey end end end @@ -39,7 +39,7 @@ let(:income_synced_at) { nil } it "returns true when income_synced_at is nil" do - expect(pinwheel_account.has_fully_synced?).to be_truthy + expect(payroll_account.has_fully_synced?).to be_truthy end end end @@ -47,27 +47,27 @@ describe "#job_succeeded?" do context "when job is supported" do it "returns false when income is supported but not yet synced" do - pinwheel_account.update!(income_synced_at: nil) - expect(pinwheel_account.job_succeeded?('income')).to be_falsey + payroll_account.update!(income_synced_at: nil) + expect(payroll_account.job_succeeded?('income')).to be_falsey end it "returns true when income is supported and it succeeded" do - pinwheel_account.update!(income_synced_at: Time.current) - expect(pinwheel_account.job_succeeded?('income')).to be_truthy + payroll_account.update!(income_synced_at: Time.current) + expect(payroll_account.job_succeeded?('income')).to be_truthy end end context "when job is supported but it errored out" do it "returns false when income is supported but it errored out" do - pinwheel_account.update!(income_synced_at: Time.current) - pinwheel_account.update!(income_errored_at: Time.current) - expect(pinwheel_account.job_succeeded?('income')).to be_falsey + payroll_account.update!(income_synced_at: Time.current) + payroll_account.update!(income_errored_at: Time.current) + expect(payroll_account.job_succeeded?('income')).to be_falsey end it "returns false when employment is supported but it errored out" do - pinwheel_account.update!(employment_synced_at: Time.current) - pinwheel_account.update!(employment_errored_at: Time.current) - expect(pinwheel_account.job_succeeded?('employment')).to be_falsey + payroll_account.update!(employment_synced_at: Time.current) + payroll_account.update!(employment_errored_at: Time.current) + expect(payroll_account.job_succeeded?('employment')).to be_falsey end end end @@ -75,29 +75,29 @@ describe "#synchronization_status" do context "when status is succeeded" do it "returns succeeded" do - pinwheel_account.update!(income_synced_at: Time.current) - expect(pinwheel_account.synchronization_status('income')).to eq(:succeeded) + payroll_account.update!(income_synced_at: Time.current) + expect(payroll_account.synchronization_status('income')).to eq(:succeeded) end end context "when status is failed" do it "returns failed" do - pinwheel_account.update!(income_synced_at: Time.current, income_errored_at: Time.current) - expect(pinwheel_account.synchronization_status('income')).to eq(:failed) + payroll_account.update!(income_synced_at: Time.current, income_errored_at: Time.current) + expect(payroll_account.synchronization_status('income')).to eq(:failed) end end context "when status is in_progress" do it "returns in_progress" do - pinwheel_account.update!(income_synced_at: nil, income_errored_at: nil) - expect(pinwheel_account.synchronization_status('income')).to eq(:in_progress) + payroll_account.update!(income_synced_at: nil, income_errored_at: nil) + expect(payroll_account.synchronization_status('income')).to eq(:in_progress) end end context "when status is unsupported" do it "returns unsupported" do - pinwheel_account.update!(supported_jobs: supported_jobs.reject { |job| job == 'income' }) - expect(pinwheel_account.synchronization_status('income')).to eq(:unsupported) + payroll_account.update!(supported_jobs: supported_jobs.reject { |job| job == 'income' }) + expect(payroll_account.synchronization_status('income')).to eq(:unsupported) end end end diff --git a/app/spec/services/pdf_service_spec.rb b/app/spec/services/pdf_service_spec.rb index 2c3a4eaf..f6e7436e 100644 --- a/app/spec/services/pdf_service_spec.rb +++ b/app/spec/services/pdf_service_spec.rb @@ -16,12 +16,12 @@ cbv_flow_invitation: invitation ) end - let(:account_id) { cbv_flow.pinwheel_accounts.first.pinwheel_account_id } + let(:account_id) { cbv_flow.payroll_accounts.first.pinwheel_account_id } let(:payments) { stub_payments(account_id) } let(:employments) { stub_employments(account_id) } let(:incomes) { stub_incomes(account_id) } let(:identities) { stub_identities(account_id) } - let(:payments_grouped_by_employer) { summarize_by_employer(payments, employments, incomes, identities, cbv_flow.pinwheel_accounts) } + let(:payments_grouped_by_employer) { summarize_by_employer(payments, employments, incomes, identities, cbv_flow.payroll_accounts) } let(:variables) do { is_caseworker: true, @@ -37,7 +37,7 @@ let(:ma_user) { create(:user, email: "test@example.com", client_agency_id: 'ma') } before do - cbv_flow.pinwheel_accounts.first.update(pinwheel_account_id: "03e29160-f7e7-4a28-b2d8-813640e030d3") + cbv_flow.payroll_accounts.first.update(pinwheel_account_id: "03e29160-f7e7-4a28-b2d8-813640e030d3") end describe "#generate" do diff --git a/docs/app/rendered/database-schema.pdf b/docs/app/rendered/database-schema.pdf index 7a5cb52da82faab6efddc8edd3e6db28387ffd5a..3c89a99369ccea082e4df38a775a1c7bc88165e0 100644 GIT binary patch delta 10900 zcmZXZQ*7rA@a}7EZQHipt!-Od+xXV@uXek;_13m+duwcMx99zzoRf2LlDU|fOy=Ur zCzB_coqC9bN{A!{V2RMq>0tQc?HwWD9-@i;3KkXW1j`{A4EohsgsQKv<+$V7wqVPR zS?u*pep+8IUdywwrGu4`v3w43Y&VH8hGJRuxx>PlZP|Up@77pc=yKuhveIt73m~tl zhTHFQo2Dhou~y;T5E!Fe%F_@6X(4PFZh>mH4d><8>n{HRL@%Fv8&g!~I6b{fhVM3R zV;4gNt<;l$cU#(9A2a_79$^>^4TX+uIxrI%1a`h63s|--)bzA98mqr5cB;l7!TOz% z3^Sh$eNaCEv0VYV=kW_!m;N|KEwguul*k)jFZAQ*1weRYAoPyF^OtD%SKUvci*0nN zjvkNfN4Yvc$#%=`xc!Yy+)Iie*G6_f23CmKI@k%Z3J`zR{Nl-gG^!elEKKWN_@ zj4cdZhv;0>ey7Ue=1^JUrDs8SPWck5UP4Rl! zLRD$1E8R~e>)F=)pVSk`sd%r2!}Vngon=ELVl0cm(0)Xb8XLjxMTA=?oC<2|U06>( zs#lY#cnA#mtq9!P1__UaG(dR8<&~Cg5tp&O74?lrClk+X`eB{bPBcd0A)EcTkA*lU zbu~{2cw6gd{{sw~@~3rcx76UwH3;$sB6?=(mOW*O9bNDb`9TG2``#C0n?bD6ZPx)z z3IrEWz%9PxiY*F@dKcJ=j`J;YW4q#XF68?=Z~osias-r~p>Ly+r2-gxtp|IOw_r<3 zx0yxP7nr)cr5TeS!w`6EaL+{`Y32W-%UBz&}v82q!Hd_tPJ|# zZkF$?C1XmEz4L{BdX4TOjg#mmh%Ef7U6b_zvQp$4GJ9DB=w@(g=M{gy#u15FLHB%y zKxnzc2OpO_m@Hv^Lr!LZ=6(-^BES$l#cT|3jl<3a!!_W{Kqj!3864)yC6&@tHf=>y zsDKsa5szif!~lXR(1r z4FfBsE|uOG7Sc;VcCtO(fjl%hJA@%@s`S6Eh`t5*x1GtGH{ZWyp3K~=5*CJ%dfXBRNwM}Jltvs54Mws%3!aXqOz5qB z=vZ4JGTZIE|!kBmpr4QoNYv&1y?Qu)!Q)` ztON(q7RtGdahNC51lN?%tqaS+dxNhURYDG<&kRF2jI1m!F^&~pYbWWKyG2|Xv1)>k z0%4Q77BugC+KxkiQk+~>(+-o#U~M=zIC5wTLh=c|IeM-R!N3cA${|P!P`o8j?%RV3 zvuZDGt=KJ{6ViSIp#Wk-odXu3z`*sHQVh74O3b*N1UfamjcGdgvU1I@VaZ>)1l>e< z)_5@lJs)cXQyZ}NM?>qrKlQlh0`KmpH$oz7>Bz~eTF&P1{Z~2vigb?t(!+cStQOu` zRi=YSl}O>4xt2@2=iKiCelP43`m{LMpbrx!GYm;I$(S6=5fmFYZfs9E)TuhgPSV&L z9ckyDw{1-HfgeyAuvlsSXjS@tDEH1!XJqC2wW8zWk*SV^VLVyYLQOYe%R6uaK8OI* zWeMK=bI6!!CNWf|1bMo0rIf9nj1CFY@gq!xq@P`h@SWwsVB~uN;H3b;_x+be(|P;` zLmUX{^K5L$+cbYAE{#Q!CHFsJVX{O&EC#3Lh&-_>2>6WB2gWYlRHyPSaZCc40C2e z3TyiVFT09^M>ci`pl9v+_~yLX*WugdfIf7j%Ww^iT`}w*6}wYanl%|EQUrALE$39F z?1V;!UK8e>mm3YFXN>W>!zSIFO{L1LRCE=OA82XE))r^2`fDtw$6{Pt$Qkw#x2(y$ zY;v@YqjVWB(*GkV88TgrSB1zl01s+8b2F3=)RjcU6T}KY9Kmqw8Mtp|Z97AL%X#I8 z?<2I5Z6-xsva^z*DL=^?c89$)q>sUxAj=pkq6xQTR6=xPr_zOhdbU7q40*=+??^#; zQZBnCu#Gk=`*8h-p&9~XF=D(fhjTKd z&dKvAE(kCo6D8G=vIrK3X@e(4AmE{?F%5H#FCCBr8_u) z0Z;B7oW^BF`JS94XqK|3a~`|k(to3Y!l{kY$wL*{(z8{j0(Fq{+`e1DgKN+B57=~s zDZR3#1k1wzF;0{_Em_b+QHfkSIk|dD&Mu_HBOJ- zu|fjjuX5!PEM_{Yiv6Dm%qkP+lPQc2^CDRSb%dlVonE-?1y!bcB^bW}~VC zygeKg4Vg}XT@l4TKZH#eNPR&^lgh|w?+up`);{Dq_jfSp0kt|UV#DwSxY?KoQbs^Q zM{mw8c4jSK;>(NJoUgG6bzkQ4?xbU;iyRkH*TMgGLG9}WQ80li%j;mLPv8$L4kGC1 zpP3JHL+G^O4QqQI#h($nL(Ai$?{*)j@~no-Ce_9vh^!4YTq6s=hac!sTi<3G$am?+ z`H`6yNv+_%(FKoUw_b!5V}*cKLD~b2P6cAw>nN!3P+BgtWHJ?x5koMhyvDx$?0jgO z324bU>`;}x^3rXoZ?-zg$8bQ&paG@X!4btL3Cd(23se+ zz3{HFw&cT3KQSge9PK{3#OPicaYI+-9ym4BE|-SV2g5IMda1um!>1efGc%QTSi|um z`AuAhGRNStx|G(6QbD>%jls&6BsH9xsI^;sNdOe@Kr8lE~5j~30sdTTAE9Vd9rcb|O{ zWvv%&)Vt1zfFJQt^dj2a?p=BGE57gypYeAR-C+j0lRe!EjezZp$d*NY3CCbT=+}`3SC2zo#I;1W?Yv8m^FFB}pXc9_fPfLCl@h%2zu;@P z6j;`aPC@GeF!QtxhN@1A$yNLw{X6C@MJS|}X zyAcNug`TN>-N|Ho00dY*@cAnBZBB^u=k!o>jjVzrD)agaap=_#FcKIEHxDeq4?;?! zq_B{+CTX5Jj0;aK;{9=iy~>j{2!l|zFaP>I{D`W&w7a_2HIN4qq(=1 zN4q41Xf*6riawuj$=rskdA|J;Tf`tH+WWl|ts4gpi4~*gPHmDQ5Nq_cWwYE9%#dUL*hIU^fJ=MLeMn@?}l->-{=-**?@WYx_l{#RI?f`+owq9HS&#gi|n2lH%uNM0YOfSdVr_>~asQD3wO zabl%)fAkL+#!prbOlUS91V(CA5@>HEkb!h|TNGzHL^@)6MXWPs4@4`2JErws?c@Xt zr%VKC3A?A^{Xde{ePe~VdM<6`U0Q^4uuUCl;KR}1bu54Ovg ziPpgk7MF&SqyqOPUL;K%9C2Tq+yV{R%^A#-Q^w|I==|&zwoWN5GE8&3Wrr-25s448oX; zth>*QF3N`XfSy{>i|FuDELB&j{3jv6^HnRWv|yEgl=KD}YFwmcPeeSr`_|sOJ4o8S z(6pA%-*7>`_|S#J6co*#>-#{FaG#BDY~E;NuB8o%SQwex^?@s`Voh&Z(&*cF40Ul^ z)&E$vGQ-8V@`RIh1P#;i=4k~0voe{aT)dnA3YRWjrK6o?^x6${o2|fH82{kWG8406 zAJaRJhXslT-*H>a)o@ip7Rt}TM$IKv;hgL22PMN5G5)NhgeF~!${LS=YY5B0jEnBp zFvdrqx96W;{X#tq26zcKQNiMMaY$wP3ncofkvZlfo@q|mLdgm(Gn2;=EQQ2@bk5!s7(845DK&aJ zM%>?EOxN{Bq2PVg;JPruGx63O;RZE;f-ZOoAnwWlzui+auLYYb)CDG!c_@a+sS|! z%G`bFMXOWKu2&hG3q1^^MB%J>Z$0u00|}wr&V63G23+xhRzV#`dD~i@#8k$zJ%dT4 zp`rR(6+K#NyBl;#74^AF!59UMcg`@W7V zz*CD(CZFYV{M6TC%VgHNYeMd%q0t0VKoEm@liYD$#QKG;Lw~dUuX%LL@u1gg zqQ{r%d;*$Ji{fm1Jbs5sOO6fe+O7ullxHM|oW@BTe)qirm$5ICq}iH@C1f@xmR=-y zK#vI^86z()=P(Zwbi#(gbW(N&nZca{4yFC__F8-TShPM<&>rheXbzd>Z z$UEnkQT)fum6>+*O`<=n>~@>Y+v)TfeX;;jMl zpQI)vBkY7V=zQ*Nwy9_XmC#000x23AdGPoh+~wM~mo(U|*u?Vk7fr9q_N?8FnC8({ zb5INavU8JK4ZhqwJrbA}dzn@W7Gr-GGxDxEFb+9OAZY$r59wR+2^U)Ge{2pl4fux|Ivo zsrb_q4jJ|bhbA)4Se@m1-=(`GJa96x!*d{vG0&(e&R-w*sfJ8M7k=dOy%~n1Vnt;W z+a1cz;;_5iZ-Yf~LU?alA8k5U&^N3BNIW-lYnjZud9NZtQYvNP%xI?69hYL(Wqr1@ zz<$Xm>gi?L6^7x-4?>QR&)u}Dk2V(^91xb%a? zz788=pAr>?4p(O}{AiFxUcA3l)zbO`2s@nZx29KX%&pDpR^|QvtpKl~`x}9hkD!>9 z&%)6rj!)sUW2Ip58(qTbHM(=4A9wUu^jLW+WNq^QpRen) zoMYlaA+$y@lh45;h~X+{B|JU}+Q~@I_36oD?FMD%4ig@%%ED}|T2R}_8rP=oBO#5_ zc&ms6Dpm#u0DWmc9%niCyeEvfZp1A|EYQyy8<(nreCkY}m>$F6A~=ECuNq+uHl@u} zd}ZN#qDo!>qfJaM)VT#1`K@VjJomwddJg%FbMtTO*)me&hQ|#-WS4b6s>LVKimR8f ze0#GgNjB9P<-8SqN*hxJd}lWhXY>u?0OPDIjC-|t17bjCq<{sVJTCXprNWeBsR4{$UZ){DayXq@sZxkb0H9 zA(!htbsLS-q{n;!CPE*6lD@pH-Xm+vqO+JQ0=kdqup$Na1y+PO&;vj zk#yIBBN$_C7c2j6dB2|=T^i3!=Qp}_tqq-}7_{Au1(-EE4CZF3H&}Id{%QA}*s;>a zAE=tYwd{6&=r0hiyBWfCm2C#Md*`UwvogQWyIJ@}_#jIZopNI;Vn^4^-l^o^JX#_5 ztiXCUITi;*7ENl#Ze6z0Pm#3KA#!6DXd&0bw;fX#e+C6!rxb&EE3;)*8DDa+&hHTD zw!U3=2Y|Z;s#T0}u(g^!zCLW8uj8g*oHyw;-dubI3I{2QlF_Ms-DDJLdmQ#z!P zh8->-_D4bmI^i!h`?(F5X5KQ`T1gYl*e4X^eQjUhko}*St>TjL<(k9UOK9Rh#+6IX zl@I^nuDw{QR{7l{RS-{!%?uaKI~@dj8zQGsF4<;iZE+J)GUUKefX???v&pou0Z^3Od9CkJN@gJ5F=T6_df+;zM z;CrNbYBh1698q9H6i96_)rac&Rh2#HL*)oC4}ZpqG4ZR(BnU5nd78_mYV4p{4S7i8 zt`RNAi3oY?b?cx??16E(``77AxgG#VH>>ql1usK>d^!48iz0PJiBlIW>rz=5h!?v2 ztSBpZ{Leg5psZifg5R~0knSyg=LoKkd`7&Vs!9A!X%8i#V5gxx*P@#0r#7=u}u+ z9MGQ!9bm8e&Y}R1e3^j}AI}mFy2iAZK$#kF{Ka9yY#)4QK?bwQHw){{)K385$k8CE z?_!}R_8>c4lP6pSsn?<*7Xt_ECTQc<^i>p}&jUIr6kCW4`ywIkxJZcuF0{kCD}AE= z%I~O;(?J5JEawAPfUN&LX%70A>^u6n5C3!x@kr#zSt{X9T>b?4qEN59obOnlV!}VU zrDoh&t@kMQzv9=0p}pv?eqr(77{*`mZ!WO)g9O==6(krwh}UGcf9c*U=}D9PLYY&} z)U!fvi1Pt;6IT056YeRj@x-K4m6(Xr36@N^Vz zHy7j^KSb8VUQan)Q#z%dQvQMxN39PK4$IQBZK6D9txtu0W4gx~O}N=-*m|smtw&r^ zH~Io5-@>2Frs}xrwD+lQQ1$_70*o6S%{bhZexcd04nxG2A^=AZ7WVo=tUND*qSJ2{)zPuxig9Z3&>d)tV74H`UNih{HE)@okxva)4=3>Kpru8A- zEJiDKj-1oJC&D{!xA3-pS9=6IEhUiMn&i2bzgK<7n$hEm0UJ+3<>BRCdn?zbo)z&E z{}Xpli1EnNEl_atMP&h}9`5wbPL=;y)fxy4#jB+xYGVm<>JwB`$~MK2Gl#Vaaa^p% zzJ=!7bd)hTHtc_)Ya4q=g!y2sVj53Ev^PPT={A9Mgp0+7 zwp0WL#lQeX^tL|2Z;JXZEMcbu?L%Rw)X99K$b0p}ZeJ%0(IPv{SG;f~alMplNT={W zU7G}CK>T)$jHR`om!vtL(h7+b+#*H2@}>@SGVgkRK-D4DPSLK8+p+|w^mpaIGjfY1R1EgS zZxa~j&9C$aCBxy{QRCW2c2`ELQ9J!{?MBk8s=IddHHp2A?M8m9)f!S3Pa9gPGOcf) zXc$roNfYQ}&E%?|H+A3N2xz#*TNg-7_$-H6!M+=f@xZ^tc^}ioJ}0fsxi0hLw;#t3 z1E$Z57@J2B2i9g`#nY^xaK{CUn5Wr3@$rwhP;T+xCj2x#BReC9>wO9TuKpnjo!Ig2 zn%Vf^D})Px1iI^f+mwYror%C^r9ul;F{}ddml{FI84WM%mM@-Kh)V&@F=zO-%n}rL zglv)sB?^@w@@-%HUN?)!RNAtVB_c`-z%U?nP+PKSb1TuMpv{`u(|V$!xsY&WWIW2= zel#U%eC$zcZ!oRC+PkA`3#8ZHmA37vLA_j)x;nRRSP%<0T_usbgKWTNa79S1e|9#q zBiME;SjDdFB{W^@{39ZK9Fez=6nT6iKV$<@p)$t^=ak+{s{inT7Fy#kXyG3@K%?sX z@W4wCWU4b}u+$>m7z7C(Ta&3~W?E8D2dgi7dC}Mk)4A4nY^*VD=oAxET`xskt4Ls` z!lTa7o@ST0d%RGVhO`}klK%MOEo`X#uCdX;lnNB;mt3I`L}+7DLnZz|A~W!3fJ*J(B|s$xz8>HrYoxs)TnV&LncpH%^Sv41GXA3XWARA0qWe@Q zp;f=GkQoF(_Jg{KcgOx)3f-1hMJo6}Al`bUg<9Q373S2#*T0Z-ykQwu`gMky?*iG+ z(L=l|P%Ztebnu7_z1z`E7G}ML(C^U_=KL{*mLD&;G6DBjD9(8mFZc{VyL?3`ypER; zK86`3=qA2xTagGvsTa`>?z|xg={(gUb?T-XJ>DX`#Xj)KNAx-SB0F$c5vzU|51ui9 z8_8{>X}fo0jgtGw&n`{7{XlWu{`5OrDzxf)Mo9wvNF|I-rmctB!5g98U^&lfra0eq zcqvj{yu|4JMBGyvYEuAcTCsNg3HC>dq$Fms_HPqu-mnEsi39C0Bh_NQFibEE31@x6 z6!XV*oAOWNJ-T#>{wOb)cs|fUR$#1a-a~xR1|FvEaHJ+3tt7gReRofCxy{HvLu!7= zqCBt*nn4FrU>`HAA9%gjhA&?NbRD%jdCJgFAwk;QLfv;R&B_4R%#LBf=$CaAA#}q1 zfDqX&Iv>pbwQFYd3hB#n#`3KEfwi|wXWb!hTw4*e5xI0B%y2(#Ctyqvria+Vz%*Ty z9@uG@*kxM$(>M$N+BI6CDXjRwS$qQhE5H`L76m8?ytQCqh)7Ja+#v-G;4iyHj$PK3bLI0Chh4oD9UE>)k3`0hh zDMBnkajhxkM`B+mk%SfqA}%F~H4~3_&MZV3`kAnmFe}8RVAPlLH1)cLScDc(Ju8A? zg%Li!vjJhp`|q?=UHR&Lw6Q}o>UG#kkGbw&vv(SHbY5IvDE{V}I$7myl-Nxi>xR8EUh$(XnAuphsVIFxqy z#c0<_hVpSYBDI;Ucw$EGxAoxNAVz*!O>+63je?;(0-4#APWScbUFLSv&wW^ORQY3Q z5!a_4!)e-Z%IK;uJT|hrD0ukD=fZVu5nuP*jwzAxKFtrsvw7=RhvMbseHuwvbBw@0 zWTWPA{RuyTfxL052Nk!q`3Qn#J&w-h`T86MEo$Xv4Wr7oUFt0)SFeUnt1yX_K) zS=kEjMhsl^78@~Q%y8%0<1+kT&Gy$05a$@G(Zqz?$a!gbT{94uG(={md4_PMH9 zxvlc317QxQs>W{D?q~?;`;@;ikeYGD!9qSxhZq^W@|iIg|2Zbn3A+MMJj$r9%@qfN#|7A{Qwg~K8q^zX>laP{=N~ZTJ0aaO4;YVQGz=qB|3d20R|*WzJ9y>}bq&{MytsI%(INZm%;&0vkI? zU~c~Ra5K=bc0o`XwoWjt@p{(GNMogJ+e!;Pn^4Wspy{*1Cp)-kN%o#Pn87? z*d?U^yYhrLhcGg?1L?y_li#kHB7(}Ms;~O)IMv+OHAYOG&+HdnhONC_2B;qplC_Rk z|G((mtp87Rb{;;^K?V~zCn!-G55$lu0|DVBXBpXC4-N3awZ3%n6PFV z$kD-Mn5$i`JVVKig)pX6Rk6RpS#8`1>IMpRJzRZt)Iqzkt3Td@HS9D zCz`xN$U>%Wn;1b#@UUi{GuK1J(k~!CYwke@)2#)~!k@t2f<+^i2QA_sh!mPY%MN|> z330{eYn n)VKQbQjL<-MX-{0e*yQLW+wpgW)Xek;bUV%prVpgmO}VninujJ delta 11852 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 GPeux_G5VSS From dac5ee7f77019056cd2a786c9279281b905fd9ec Mon Sep 17 00:00:00 2001 From: Matt Gardner Date: Thu, 27 Feb 2025 12:03:08 -0500 Subject: [PATCH 7/7] Rename cbv_flow identifier column for clarity (#472) # Conflicts: # app/db/schema.rb # docs/app/rendered/database-schema.pdf --- .../controllers/api/pinwheel_controller.rb | 2 +- .../webhooks/pinwheel/events_controller.rb | 2 +- app/app/models/cbv_flow.rb | 2 +- .../migrate/20250226205049_rename_column.rb | 5 +++++ app/db/schema.rb | 2 +- .../pinwheel/events_controller_spec.rb | 6 +++--- .../services/data_retention_service_spec.rb | 10 +++++----- docs/app/rendered/database-schema.pdf | Bin 38286 -> 39388 bytes 8 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 app/db/migrate/20250226205049_rename_column.rb diff --git a/app/app/controllers/api/pinwheel_controller.rb b/app/app/controllers/api/pinwheel_controller.rb index 44fd0b51..67296697 100644 --- a/app/app/controllers/api/pinwheel_controller.rb +++ b/app/app/controllers/api/pinwheel_controller.rb @@ -8,7 +8,7 @@ def create_token token_response = pinwheel.create_link_token( response_type: token_params[:response_type], id: token_params[:id], - end_user_id: @cbv_flow.pinwheel_end_user_id, + end_user_id: @cbv_flow.end_user_id, language: token_params[:locale] ) token = token_response["data"]["token"] diff --git a/app/app/controllers/webhooks/pinwheel/events_controller.rb b/app/app/controllers/webhooks/pinwheel/events_controller.rb index 7c06abe5..e67748a0 100644 --- a/app/app/controllers/webhooks/pinwheel/events_controller.rb +++ b/app/app/controllers/webhooks/pinwheel/events_controller.rb @@ -86,7 +86,7 @@ def track_account_created_event(cbv_flow, platform_name) end def set_cbv_flow - @cbv_flow = CbvFlow.find_by_pinwheel_end_user_id(params["payload"]["end_user_id"]) + @cbv_flow = CbvFlow.find_by_end_user_id(params["payload"]["end_user_id"]) end def set_pinwheel diff --git a/app/app/models/cbv_flow.rb b/app/app/models/cbv_flow.rb index 8ab96818..4cf904a1 100644 --- a/app/app/models/cbv_flow.rb +++ b/app/app/models/cbv_flow.rb @@ -11,7 +11,7 @@ class CbvFlow < ApplicationRecord include Redactable has_redactable_fields( case_number: :string, - pinwheel_end_user_id: :uuid, + end_user_id: :uuid, additional_information: :object ) diff --git a/app/db/migrate/20250226205049_rename_column.rb b/app/db/migrate/20250226205049_rename_column.rb new file mode 100644 index 00000000..73e46f8d --- /dev/null +++ b/app/db/migrate/20250226205049_rename_column.rb @@ -0,0 +1,5 @@ +class RenameColumn < ActiveRecord::Migration[7.1] + def change + rename_column :cbv_flows, :pinwheel_end_user_id, :end_user_id + end +end diff --git a/app/db/schema.rb b/app/db/schema.rb index ffeedbe4..1ae9879a 100644 --- a/app/db/schema.rb +++ b/app/db/schema.rb @@ -68,7 +68,7 @@ t.date "payroll_data_available_from" t.bigint "cbv_flow_invitation_id" t.string "pinwheel_token_id" - t.uuid "pinwheel_end_user_id", default: -> { "gen_random_uuid()" }, null: false + t.uuid "end_user_id", default: -> { "gen_random_uuid()" }, null: false t.jsonb "additional_information", default: {} t.string "client_agency_id" t.string "confirmation_code" diff --git a/app/spec/controllers/webhooks/pinwheel/events_controller_spec.rb b/app/spec/controllers/webhooks/pinwheel/events_controller_spec.rb index b76463e1..2c570fda 100644 --- a/app/spec/controllers/webhooks/pinwheel/events_controller_spec.rb +++ b/app/spec/controllers/webhooks/pinwheel/events_controller_spec.rb @@ -37,7 +37,7 @@ let(:payload) do { "platform_id" => "00000000-0000-0000-0000-000000011111", - "end_user_id" => cbv_flow.pinwheel_end_user_id, + "end_user_id" => cbv_flow.end_user_id, "account_id" => account_id, "platform_name" => "acme" } @@ -90,7 +90,7 @@ let(:payload) do { "account_id" => account_id, - "end_user_id" => cbv_flow.pinwheel_end_user_id, + "end_user_id" => cbv_flow.end_user_id, "outcome" => "success" } end @@ -151,7 +151,7 @@ let(:payload) do { "account_id" => account_id, - "end_user_id" => cbv_flow.pinwheel_end_user_id, + "end_user_id" => cbv_flow.end_user_id, "outcome" => "pending" } end diff --git a/app/spec/services/data_retention_service_spec.rb b/app/spec/services/data_retention_service_spec.rb index 9ef43960..3570c25d 100644 --- a/app/spec/services/data_retention_service_spec.rb +++ b/app/spec/services/data_retention_service_spec.rb @@ -91,7 +91,7 @@ before do cbv_flow.update( - pinwheel_end_user_id: "11111111-1111-1111-1111-111111111111", + end_user_id: "11111111-1111-1111-1111-111111111111", additional_information: { "account-id" => "some string here" } ) end @@ -100,7 +100,7 @@ service.redact_incomplete_cbv_flows expect(cbv_flow.reload).to have_attributes( case_number: "REDACTED", - pinwheel_end_user_id: "00000000-0000-0000-0000-000000000000", + end_user_id: "00000000-0000-0000-0000-000000000000", additional_information: {} ) end @@ -163,7 +163,7 @@ service.redact_incomplete_cbv_flows expect(cbv_flow.reload).to have_attributes( case_number: "REDACTED", - pinwheel_end_user_id: "00000000-0000-0000-0000-000000000000", + end_user_id: "00000000-0000-0000-0000-000000000000", additional_information: {} ) end @@ -180,7 +180,7 @@ .create_from_invitation(cbv_flow_invitation) .tap do |cbv_flow| cbv_flow.update( - pinwheel_end_user_id: "11111111-1111-1111-1111-111111111111", + end_user_id: "11111111-1111-1111-1111-111111111111", additional_information: { "account-id" => "some string here" }, confirmation_code: "SANDBOX0002", transmitted_at: Time.new(2024, 8, 1, 12, 0, 0, "-04:00") @@ -221,7 +221,7 @@ service.redact_complete_cbv_flows expect(cbv_flow.reload).to have_attributes( case_number: "REDACTED", - pinwheel_end_user_id: "00000000-0000-0000-0000-000000000000", + end_user_id: "00000000-0000-0000-0000-000000000000", additional_information: {} ) end diff --git a/docs/app/rendered/database-schema.pdf b/docs/app/rendered/database-schema.pdf index 3c89a99369ccea082e4df38a775a1c7bc88165e0..8bcbe855d8378c721458155851a9bf6e1dc33288 100644 GIT binary patch delta 11989 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 delta 10896 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