diff --git a/app/Gemfile b/app/Gemfile index 46fb7aaff..36991771c 100644 --- a/app/Gemfile +++ b/app/Gemfile @@ -74,6 +74,7 @@ gem "gpgme", "~> 2.0", ">= 2.0.12" gem "pdf-reader", "~> 2.12.0" gem "maybe_later" +gem "activeresource" group :development, :test do gem "brakeman", "~> 5.2" @@ -82,13 +83,12 @@ group :development, :test do gem "debug", platforms: %i[mri mingw x64_mingw] gem "dotenv-rails", "~> 2.7" gem "erb_lint", require: false - gem "i18n-tasks", "~> 1.0" + gem "i18n-tasks", "~> 1.0", require: false gem "rspec-rails", "~> 6.1" gem "rubocop" gem "rubocop-rspec" gem "rubocop-rails-omakase" gem "selenium-webdriver" - gem "standard", "~> 1.7" gem "timecop" end diff --git a/app/Gemfile.lock b/app/Gemfile.lock index 26b4c562c..6b1747e6d 100644 --- a/app/Gemfile.lock +++ b/app/Gemfile.lock @@ -60,10 +60,18 @@ GEM globalid (>= 0.3.6) activemodel (7.1.5.1) activesupport (= 7.1.5.1) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) + builder (~> 3.1) activerecord (7.1.5.1) activemodel (= 7.1.5.1) activesupport (= 7.1.5.1) timeout (>= 0.4.0) + activeresource (6.1.4) + activemodel (>= 6.0) + activemodel-serializers-xml (~> 1.0) + activesupport (>= 6.0) activestorage (7.1.5.1) actionpack (= 7.1.5.1) activejob (= 7.1.5.1) @@ -225,11 +233,10 @@ GEM jmespath (1.6.2) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.7.2) + json (2.9.1) jwt (2.8.2) base64 - language_server-protocol (3.17.0.3) - lint_roller (1.1.0) + language_server-protocol (3.17.0.4) logger (1.6.2) loofah (2.23.1) crass (~> 1.0.2) @@ -294,8 +301,8 @@ GEM actionpack (>= 4.2) omniauth (~> 2.0) orm_adapter (0.5.0) - parallel (1.25.1) - parser (3.3.4.0) + parallel (1.26.3) + parser (3.3.7.0) ast (~> 2.4.1) racc pdf-reader (2.12.0) @@ -382,7 +389,7 @@ GEM rdoc (6.8.1) psych (>= 4.0.0) redis (4.8.1) - regexp_parser (2.9.2) + regexp_parser (2.10.0) reline (0.5.12) io-console (~> 0.5) responders (3.1.1) @@ -406,18 +413,17 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) - rubocop (1.64.1) + rubocop (1.71.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.36.2, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.38.0) parser (>= 3.3.1.0) rubocop-minitest (0.35.1) rubocop (>= 1.61, < 2.0) @@ -465,18 +471,6 @@ GEM activesupport (>= 6.1) sprockets (>= 3.0.0) stackprof (0.2.26) - standard (1.39.2) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.64.0) - standard-custom (~> 1.0.0) - standard-performance (~> 1.4) - standard-custom (1.0.2) - lint_roller (~> 1.0) - rubocop (~> 1.50) - standard-performance (1.4.0) - lint_roller (~> 1.1) - rubocop-performance (~> 1.21.0) stimulus-rails (1.3.3) railties (>= 6.0.0) stringio (3.1.2) @@ -492,7 +486,7 @@ GEM railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) uri (0.13.0) version_gem (1.1.4) view_component (3.13.0) @@ -532,6 +526,7 @@ PLATFORMS DEPENDENCIES actioncable-enhanced-postgresql-adapter + activeresource aws-actionmailer-ses aws-sdk-rails aws-sdk-s3 @@ -578,7 +573,6 @@ DEPENDENCIES sidekiq (~> 6.4) sprockets-rails stackprof - standard (~> 1.7) stimulus-rails timecop turbo-rails diff --git a/app/app/controllers/cbv/payment_details_controller.rb b/app/app/controllers/cbv/payment_details_controller.rb index b50acb7a1..bda7526e5 100644 --- a/app/app/controllers/cbv/payment_details_controller.rb +++ b/app/app/controllers/cbv/payment_details_controller.rb @@ -24,9 +24,15 @@ def show return redirect_to(cbv_flow_entry_url, flash: { slim_alert: { message: t("cbv.error_no_access"), type: "error" } }) end - @employment = has_employment_data? && pinwheel.fetch_employment(account_id: account_id)["data"] - @income_metadata = has_income_data? && pinwheel.fetch_income_metadata(account_id: account_id)["data"] - @payments = has_paystubs_data? ? set_payments(account_id) : [] + @employment = has_employment_data? && pinwheel.fetch_employment(account_id: account_id) + @income = has_income_data? && pinwheel.fetch_income(account_id: account_id) + + if has_paystubs_data? + set_payments(account_id) + else + @payments = [] + end + @account_comment = account_comment end @@ -65,50 +71,50 @@ def has_paystubs_data? def employer_name return I18n.t("cbv.payment_details.show.unknown") unless has_employment_data? - @employment["employer_name"] + @employment.employer_name end def employment_start_date return I18n.t("cbv.payment_details.show.unknown") unless has_employment_data? - @employment["start_date"] + @employment.start_date end def employment_end_date return I18n.t("cbv.payment_details.show.unknown") unless has_employment_data? - @employment["termination_date"] + @employment.termination_date end def employment_status return I18n.t("cbv.payment_details.show.unknown") unless has_employment_data? - @employment["status"]&.humanize + @employment.status&.humanize end def pay_frequency return I18n.t("cbv.payment_details.show.unknown") unless has_income_data? - @income_metadata["pay_frequency"] + @income.pay_frequency&.humanize end def compensation_unit return I18n.t("cbv.payment_details.show.unknown") unless has_income_data? - @income_metadata["compensation_unit"] + @income.compensation_unit end def compensation_amount return I18n.t("cbv.payment_details.show.unknown") unless has_income_data? - @income_metadata["compensation_amount"] + @income.compensation_amount end def gross_pay return I18n.t("cbv.payment_details.show.unknown") unless has_paystubs_data? @payments - .map { |payment| payment[:gross_pay_amount] } + .map { |payment| payment.gross_pay_amount.to_i } .reduce(:+) end diff --git a/app/app/controllers/cbv/summaries_controller.rb b/app/app/controllers/cbv/summaries_controller.rb index 6941d456b..f0e28c818 100644 --- a/app/app/controllers/cbv/summaries_controller.rb +++ b/app/app/controllers/cbv/summaries_controller.rb @@ -76,10 +76,6 @@ def has_consent params[:cbv_flow] && params[:cbv_flow][:consent_to_authorized_use] == "1" end - def total_gross_income - @payments.reduce(0) { |sum, payment| sum + payment[:gross_pay_amount] } - end - def transmit_to_caseworker case current_site.transmission_method when "shared_email" @@ -214,7 +210,7 @@ def track_accessed_income_summary_event(cbv_flow, payments) site_id: cbv_flow.site_id, cbv_flow_id: cbv_flow.id, invitation_id: cbv_flow.cbv_flow_invitation_id, - account_count: payments.map { |p| p[:account_id] }.uniq.count, + account_count: payments.map { |p| p.account_id }.uniq.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/helpers/cbv/pinwheel_data_helper.rb b/app/app/helpers/cbv/pinwheel_data_helper.rb index ee6d0e2fa..50ac827d7 100644 --- a/app/app/helpers/cbv/pinwheel_data_helper.rb +++ b/app/app/helpers/cbv/pinwheel_data_helper.rb @@ -5,72 +5,41 @@ def set_payments(account_id = nil) invitation = @cbv_flow.cbv_flow_invitation to_pay_date = invitation.snap_application_date from_pay_date = invitation.paystubs_query_begins_at - payments = account_id.nil? ? fetch_payroll(from_pay_date.strftime("%Y-%m-%d"), to_pay_date.strftime("%Y-%m-%d")) : fetch_payroll_for_account_id(account_id, from_pay_date.strftime("%Y-%m-%d"), to_pay_date.strftime("%Y-%m-%d")) + @payments = + if account_id.nil? + fetch_paystubs(from_pay_date, to_pay_date) + else + fetch_paystubs_for_account_id(account_id, from_pay_date, to_pay_date) + end @payments_ending_at = format_date(to_pay_date) @payments_beginning_at = format_date(from_pay_date) - @payments = parse_payments(payments) end - def set_employments(account_id = nil) - @employments = account_id.nil? ? fetch_employments : fetch_employments_for_account_id(account_id) - end + def set_employments + @employments = @cbv_flow.pinwheel_accounts.map do |pinwheel_account| + next unless pinwheel_account.job_succeeded?("employment") - def set_incomes(account_id = nil) - @incomes = account_id.nil? ? fetch_incomes : fetch_incomes_for_account_id(account_id) + pinwheel.fetch_employment(account_id: pinwheel_account.pinwheel_account_id) + end.compact end - def set_identities(account_id = nil) - @identities = account_id.nil? ? fetch_identities : fetch_identity_for_account_id(account_id) - end + def set_incomes + @incomes = @cbv_flow.pinwheel_accounts.map do |pinwheel_account| + next unless pinwheel_account.job_succeeded?("income") - def parse_payments(payments) - payments.map do |payment| - { - start: payment["pay_period_start"], - end: payment["pay_period_end"], - hours: total_hours_from_earnings(payment["earnings"]), - hours_by_earning_category: hours_by_earning_category(payment["earnings"]), - gross_pay_amount: payment["gross_pay_amount"].to_i, - net_pay_amount: payment["net_pay_amount"].to_i, - gross_pay_ytd: payment["gross_pay_ytd"].to_i, - pay_date: payment["pay_date"], - deductions: payment["deductions"].map { |deduction| { category: deduction["category"], amount: deduction["amount"] } }, - account_id: payment["account_id"] - } - end + pinwheel.fetch_income(account_id: pinwheel_account.pinwheel_account_id) + end.compact end - def fetch_known_end_user_account_ids - pinwheel_account_ids = pinwheel.fetch_accounts(end_user_id: @cbv_flow.pinwheel_end_user_id)["data"].pluck("id") - - PinwheelAccount.where(pinwheel_account_id: pinwheel_account_ids).pluck(:pinwheel_account_id) - end + def set_identities + @identities = @cbv_flow.pinwheel_accounts.map do |pinwheel_account| + next unless pinwheel_account.job_succeeded?("identity") - def total_hours_from_earnings(earnings) - base_hours = earnings - .filter { |e| e["category"] != "overtime" } - .map { |e| e["hours"] } - .compact - .max - return unless base_hours - - # Add overtime hours to the base hours, because they tend to be additional - # work beyond the other entries. (As opposed to category="premium", which - # often duplicates other earnings' hours.) - # - # See FFS-1773. - overtime_hours = earnings - .filter { |e| e["category"] == "overtime" } - .sum { |e| e["hours"] || 0.0 } - - base_hours + overtime_hours + pinwheel.fetch_identity(account_id: pinwheel_account.pinwheel_account_id) + end end def hours_by_earning_category(earnings) - earnings - .filter { |e| e["hours"].present? && e["hours"] > 0 } - .group_by { |e| e["category"] } - .transform_values { |earnings| earnings.sum { |e| e["hours"] } } end def payments_grouped_by_employer @@ -78,7 +47,7 @@ def payments_grouped_by_employer end def total_gross_income - @payments.reduce(0) { |sum, payment| sum + payment[:gross_pay_amount] } + @payments.reduce(0) { |sum, payment| sum + payment.gross_pay_amount } end def summarize_by_employer(payments, employments, incomes, identities, pinwheel_accounts) @@ -88,63 +57,36 @@ def summarize_by_employer(payments, employments, incomes, identities, pinwheel_a has_income_data = pinwheel_account.job_succeeded?("income") has_employment_data = pinwheel_account.job_succeeded?("employment") has_identity_data = pinwheel_account.job_succeeded?("identity") - account_payments = payments.filter { |payment| payment[:account_id] == account_id } + account_payments = payments.filter { |payment| payment.account_id == account_id } hash[account_id] ||= { - total: account_payments.sum { |payment| payment[:gross_pay_amount] }, - payments: account_payments, + total: account_payments.sum { |payment| payment.gross_pay_amount }, has_income_data: has_income_data, has_employment_data: has_employment_data, has_identity_data: has_identity_data, - income: has_income_data && incomes.find { |income| income["account_id"] == account_id }, - employment: has_employment_data && employments.find { |employment| employment["account_id"] == account_id }, - identity: has_identity_data && identities.find { |identity| identity["account_id"] == account_id } + income: has_income_data && incomes.find { |income| income.account_id == account_id }, + employment: has_employment_data && employments.find { |employment| employment.account_id == account_id }, + identity: has_identity_data && identities.find { |identity| identity.account_id == account_id }, + payments: account_payments } end end private - def fetch_payroll(from_pay_date, to_pay_date) - fetch_known_end_user_account_ids.map do |account_id| - fetch_payroll_for_account_id(account_id, from_pay_date, to_pay_date) - end.flatten - end - - def fetch_payroll_for_account_id(account_id, from_pay_date, to_pay_date) - pinwheel.fetch_paystubs(account_id: account_id, from_pay_date: from_pay_date, to_pay_date: to_pay_date)["data"] - end + def fetch_paystubs(from_pay_date, to_pay_date) + @cbv_flow.pinwheel_accounts.flat_map do |pinwheel_account| + next [] unless pinwheel_account.job_succeeded?("paystubs") - def fetch_employments - fetch_known_end_user_account_ids.map do |account_id| - next [] unless does_pinwheel_account_support_job?(account_id, "employment") - fetch_employments_for_account_id account_id - end.flatten - end - - def fetch_employments_for_account_id(account_id) - pinwheel.fetch_employment(account_id: account_id)["data"] - end - - def fetch_incomes - fetch_known_end_user_account_ids.map do |account_id| - next [] unless does_pinwheel_account_support_job?(account_id, "income") - fetch_incomes_for_account_id account_id - end.flatten - end - - def fetch_incomes_for_account_id(account_id) - pinwheel.fetch_income_metadata(account_id: account_id)["data"] - end - - def fetch_identities - fetch_known_end_user_account_ids.map do |account_id| - next [] unless does_pinwheel_account_support_job?(account_id, "identity") - fetch_identity_for_account_id account_id - end.flatten + fetch_paystubs_for_account_id(pinwheel_account.pinwheel_account_id, from_pay_date, to_pay_date) + end end - def fetch_identity_for_account_id(account_id) - pinwheel.fetch_identity(account_id: account_id)["data"] + def fetch_paystubs_for_account_id(account_id, from_pay_date, to_pay_date) + pinwheel.fetch_paystubs( + account_id: account_id, + from_pay_date: from_pay_date.strftime("%Y-%m-%d"), + to_pay_date: to_pay_date.strftime("%Y-%m-%d") + ) end def does_pinwheel_account_support_job?(account_id, job) diff --git a/app/app/services/pinwheel_service.rb b/app/app/services/pinwheel_service.rb index 8e98ff5b5..9f8d15403 100644 --- a/app/app/services/pinwheel_service.rb +++ b/app/app/services/pinwheel_service.rb @@ -103,6 +103,53 @@ class PinwheelService } ] + # Base class for wrapping responses from Pinwheel to allow accessing the data + # via dot-notation. + class ResponseObject < ActiveResource::Base + def initialize(*params, environment:) + # ActiveResource requires us to set the `site` (the API base url) on the + # class. Since Pinwheel's API base URL's differ per-environment, let's + # set the value dynamically as these records are instantiated. + self.class.site = environment[:base_url] + super(*params) + end + end + Employment = Class.new(ResponseObject) + Identity = Class.new(ResponseObject) + Income = Class.new(ResponseObject) + Paystub = Class.new(ResponseObject) do + alias_attribute :start, :pay_period_start + alias_attribute :end, :pay_period_end + + def hours + base_hours = earnings + .filter { |e| e.category != "overtime" } + .map { |e| e.hours } + .compact + .max + return unless base_hours + + # Add overtime hours to the base hours, because they tend to be additional + # work beyond the other entries. (As opposed to category="premium", which + # often duplicates other earnings' hours.) + # + # See FFS-1773. + overtime_hours = earnings + .filter { |e| e.category == "overtime" } + .sum { |e| e.hours || 0.0 } + + base_hours + overtime_hours + end + + def hours_by_earning_category + earnings + .filter { |e| e.hours && e.hours > 0 } + .group_by { |e| e.category } + .transform_values { |earnings| earnings.sum { |e| e.hours } } + end + end + + def initialize(environment, api_key = nil) @api_key = api_key || ENVIRONMENTS.fetch(environment.to_sym)[:api_key] @environment = ENVIRONMENTS.fetch(environment.to_sym) { |env| raise KeyError.new("PinwheelService unknown environment: #{env}") } @@ -146,19 +193,26 @@ def fetch_accounts(end_user_id:) end def fetch_paystubs(account_id:, **params) - @http.get(build_url("#{ACCOUNTS_ENDPOINT}/#{account_id}/paystubs"), params).body + json = @http.get(build_url("#{ACCOUNTS_ENDPOINT}/#{account_id}/paystubs"), params).body + json["data"].map { |paystub_json| Paystub.new(paystub_json, environment: @environment) } end def fetch_employment(account_id:) - @http.get(build_url("#{ACCOUNTS_ENDPOINT}/#{account_id}/employment")).body + json = @http.get(build_url("#{ACCOUNTS_ENDPOINT}/#{account_id}/employment")).body + + Employment.new(json["data"], environment: @environment) end def fetch_identity(account_id:) - @http.get(build_url("#{ACCOUNTS_ENDPOINT}/#{account_id}/identity")).body + json = @http.get(build_url("#{ACCOUNTS_ENDPOINT}/#{account_id}/identity")).body + + Identity.new(json["data"], environment: @environment) end - def fetch_income_metadata(account_id:) - @http.get(build_url("#{ACCOUNTS_ENDPOINT}/#{account_id}/income")).body + def fetch_income(account_id:) + json = @http.get(build_url("#{ACCOUNTS_ENDPOINT}/#{account_id}/income")).body + + Income.new(json["data"], environment: @environment) end def fetch_platform(platform_id:) diff --git a/app/app/views/cbv/payment_details/show.html.erb b/app/app/views/cbv/payment_details/show.html.erb index 842e5e6c6..052bedb18 100644 --- a/app/app/views/cbv/payment_details/show.html.erb +++ b/app/app/views/cbv/payment_details/show.html.erb @@ -45,21 +45,21 @@ <% if @payments.any? %> <% @payments.each do |payment| %> <%= table.with_row_section do |row| %> -

<%= t("cbv.payment_details.show.pay_date", pay_date: format_date(payment[:pay_date])) %>

+

<%= t("cbv.payment_details.show.pay_date", pay_date: format_date(payment.pay_date)) %>

<% end %> - <%= table.with_data_point(:pay_period, payment[:start], payment[:end]) %> - <%= table.with_data_point(:pay_gross, payment[:gross_pay_amount]) %> - <%= table.with_data_point(:number_of_hours_worked, payment[:hours]) %> - <% payment[:hours_by_earning_category].each do |category, total_hours| %> + <%= table.with_data_point(:pay_period, payment.start, payment.end) %> + <%= table.with_data_point(:pay_gross, payment.gross_pay_amount) %> + <%= table.with_data_point(:number_of_hours_worked, payment.hours) %> + <% payment.hours_by_earning_category.each do |category, total_hours| %> <%= table.with_data_point(:earnings_entry, category, total_hours) %> <% end %> - <%= table.with_data_point(:net_pay_amount, payment[:net_pay_amount]) %> - <% payment[:deductions].filter { |deduction| deduction[:amount] > 0 }.each do |deduction| %> - <%= table.with_data_point(:deduction, deduction[:category], deduction[:amount]) %> + <%= table.with_data_point(:net_pay_amount, payment.net_pay_amount) %> + <% payment.deductions.filter { |deduction| deduction.amount > 0 }.each do |deduction| %> + <%= table.with_data_point(:deduction, deduction.category, deduction.amount) %> <% end %> - <%= table.with_data_point(:pay_gross_ytd, payment[:gross_pay_ytd]) %> + <%= table.with_data_point(:pay_gross_ytd, payment.gross_pay_ytd) %> <% end %> <% else %> <%= table.with_row(t(".none_found")) %> diff --git a/app/app/views/cbv/summaries/show.html.erb b/app/app/views/cbv/summaries/show.html.erb index 5b871f155..7a73dcf5c 100644 --- a/app/app/views/cbv/summaries/show.html.erb +++ b/app/app/views/cbv/summaries/show.html.erb @@ -17,7 +17,7 @@ <% payments_grouped_by_employer.each_with_index do |(account_id, summary), index| %>

- <% employer_name = summary[:has_employment_data] ? summary.dig(:employment, "employer_name") : nil %> + <% employer_name = summary[:has_employment_data] ? summary[:employment].employer_name : nil %> <% if employer_name %> <%= t(".table_caption", number: index + 1, employer_name: employer_name) %> <% else %> @@ -44,7 +44,7 @@

<% summary[:payments].each do |payment| %> - + <% end %> diff --git a/app/app/views/cbv/summaries/show.pdf.erb b/app/app/views/cbv/summaries/show.pdf.erb index 5727577ac..216a84a85 100644 --- a/app/app/views/cbv/summaries/show.pdf.erb +++ b/app/app/views/cbv/summaries/show.pdf.erb @@ -76,7 +76,7 @@

<%= t(".pdf.client.employment_payment_details") %>

<% payments_grouped_by_employer.each_with_index do |(account_id, summary), index| %> - <% employer_name = summary[:has_employment_data] ? summary.dig(:employment, "employer_name") : nil %> + <% employer_name = summary[:has_employment_data] ? summary[:employment].employer_name : nil %>

<%= t(".table_caption_no_name", number: index + 1) %>: <%= employer_name %>

<%= render(TableComponent.new) do |table| %> @@ -89,18 +89,19 @@ <% end %> <% if is_caseworker && summary[:has_identity_data] %> - <%= table.with_data_point(:client_full_name, summary[:identity]["full_name"]) %> + <%= table.with_data_point(:client_full_name, summary[:identity].full_name) %> <% end %> <% if summary[:has_employment_data] %> - <%= table.with_data_point(:employer_phone, summary.dig(:employment, "employer_phone_number", "value")) %> - <%= table.with_data_point(:employer_address, summary.dig(:employment, "employer_address", "raw")) %> - <%= table.with_data_point(:employment_status, summary[:employment]["status"]) %> - <%= table.with_data_point(:employment_start_date, summary[:employment]["start_date"]) %> - <%= table.with_data_point(:employment_end_date, summary[:employment]["termination_date"]) %> + <% employment = summary[:employment] %> + <%= table.with_data_point(:employer_phone, employment.employer_phone_number.value) %> + <%= table.with_data_point(:employer_address, employment.employer_address.raw) %> + <%= table.with_data_point(:employment_status, employment.status) %> + <%= table.with_data_point(:employment_start_date, employment.start_date) %> + <%= table.with_data_point(:employment_end_date, employment.termination_date) %> <% end %> <% if summary[:has_income_data] %> - <%= table.with_data_point(:pay_frequency, summary[:income]["pay_frequency"]&.humanize) %> - <%= table.with_data_point(:hourly_rate, summary[:income]["compensation_amount"], summary[:income]["compensation_unit"]) %> + <%= table.with_data_point(:pay_frequency, summary[:income].pay_frequency&.humanize) %> + <%= table.with_data_point(:hourly_rate, summary[:income].compensation_amount, summary[:income].compensation_unit) %> <% end %> <% end %> @@ -111,23 +112,23 @@ <% if employer_name %> <%= employer_name %> — <% end %> - Pay Date: <%= format_date(payment[:pay_date]) %> + Pay Date: <%= format_date(payment.pay_date) %> <% end %> <% if summary[:has_income_data] %> - <%= table.with_data_point(:pay_period_with_frequency, payment[:start], payment[:end], summary[:income]["pay_frequency"]&.humanize, highlight: is_caseworker) %> + <%= table.with_data_point(:pay_period_with_frequency, payment.start, payment.end, summary[:income].pay_frequency&.humanize, highlight: is_caseworker) %> <% else %> - <%= table.with_data_point(:pay_period_with_frequency, payment[:start], payment[:end], t("cbv.payment_details.show.frequency_unknown"), highlight: is_caseworker) %> + <%= table.with_data_point(:pay_period_with_frequency, payment.start, payment.end, t("cbv.payment_details.show.frequency_unknown"), highlight: is_caseworker) %> <% end %> - <%= table.with_data_point(:pay_gross, payment[:gross_pay_amount], highlight: is_caseworker) %> - <%= table.with_data_point(:number_of_hours_worked, payment[:hours], highlight: is_caseworker) %> - <% payment[:hours_by_earning_category].each do |category, total_hours| %> + <%= table.with_data_point(:pay_gross, payment.gross_pay_amount, highlight: is_caseworker) %> + <%= table.with_data_point(:number_of_hours_worked, payment.hours, highlight: is_caseworker) %> + <% payment.hours_by_earning_category.each do |category, total_hours| %> <%= table.with_data_point(:earnings_entry, category, total_hours) %> <% end %> - <%= table.with_data_point(:net_pay_amount, payment[:net_pay_amount]) %> - <% payment[:deductions].filter { |deduction| deduction[:amount] > 0 }.each do |deduction| %> - <%= table.with_data_point(:deduction, deduction[:category], deduction[:amount]) %> + <%= table.with_data_point(:net_pay_amount, payment.net_pay_amount) %> + <% payment.deductions.filter { |deduction| deduction.amount > 0 }.each do |deduction| %> + <%= table.with_data_point(:deduction, deduction.category, deduction.amount) %> <% end %> - <%= table.with_data_point(:pay_gross_ytd, payment[:gross_pay_ytd]) %> + <%= table.with_data_point(:pay_gross_ytd, payment.gross_pay_ytd) %> <% end %> <% end %> <% if summary[:payments].empty? %> diff --git a/app/spec/helpers/cbv/pinwheel_data_helper_spec.rb b/app/spec/helpers/cbv/pinwheel_data_helper_spec.rb index 68805d686..02b9fe2f7 100644 --- a/app/spec/helpers/cbv/pinwheel_data_helper_spec.rb +++ b/app/spec/helpers/cbv/pinwheel_data_helper_spec.rb @@ -6,23 +6,35 @@ let(:account_id) { "03e29160-f7e7-4a28-b2d8-813640e030d3" } let(:payments) do - load_relative_json_file('request_end_user_paystubs_response.json')['data'] + raw_payments_json = load_relative_json_file('request_end_user_paystubs_response.json')['data'] + + raw_payments_json.map do |payment_json| + PinwheelService::Paystub.new( + payment_json, + environment: PinwheelService::ENVIRONMENTS[:sandbox] + ) + end end - let(:employments) do - load_relative_json_file('request_employment_info_response.json')['data'] + let(:employment) do + PinwheelService::Employment.new( + load_relative_json_file('request_employment_info_response.json')['data'], + environment: PinwheelService::ENVIRONMENTS[:sandbox] + ) end let(:incomes) do - load_relative_json_file('request_income_metadata_response.json')['data'] + PinwheelService::Income.new( + load_relative_json_file('request_income_metadata_response.json')['data'], + environment: PinwheelService::ENVIRONMENTS[:sandbox] + ) end let(:identities) do - load_relative_json_file('request_identity_response.json')['data'] - end - - let(:parsed_payments) do - helper.parse_payments(payments) + PinwheelService::Identity.new( + load_relative_json_file('request_identity_response.json')['data'], + environment: PinwheelService::ENVIRONMENTS[:sandbox] + ) end let!(:cbv_flow) { create(:cbv_flow, :with_pinwheel_account) } @@ -33,144 +45,19 @@ describe "aggregate payments" do it "groups by employer" do - expect(helper.summarize_by_employer(parsed_payments, [ employments ], [ incomes ], [ identities ], cbv_flow.pinwheel_accounts)).to eq({ - account_id => { - payments: [ - { - account_id: account_id, - deductions: [ - { amount: 7012, category: "retirement" }, - { amount: 57692, category: "commuter" }, - { amount: 0, category: "empty_deduction" } - ], - end: "2020-12-24", - gross_pay_amount: 480720, - hours: 80, - pay_date: "2020-12-31", - start: "2020-12-10", - gross_pay_ytd: 6971151, - gross_pay_amount: 480720, - hours_by_earning_category: { "salary" => 80 }, - net_pay_amount: 321609 - } - ], - has_income_data: true, - has_employment_data: true, - has_identity_data: true, - employment: employments, - income: incomes, - identity: identities, - total: 480720 - } - }) - end - end - - describe "#parse_payments" do - let(:payments) do - load_relative_json_file('request_end_user_paystubs_response.json')['data'] - end - - let(:parsed_payments) do - helper.parse_payments(payments) - end - - it "parses payments" do - expect(helper.parse_payments(payments)).to eq( - [ - { - account_id: "03e29160-f7e7-4a28-b2d8-813640e030d3", - deductions: [ - { amount: 7012, category: "retirement" }, - { amount: 57692, category: "commuter" }, - { amount: 0, category: "empty_deduction" } - ], - end: "2020-12-24", - gross_pay_amount: 480720, - gross_pay_ytd: 6971151, - net_pay_amount: 321609, - hours: 80, - hours_by_earning_category: { "salary" => 80 }, - pay_date: "2020-12-31", - start: "2020-12-10" } - ] - ) - end - - context "when there are some 'earnings' entries with fewer hours worked" do - before do - payments[0]["earnings"].prepend( - "amount" => 100, - "category" => "other", - "name" => "One Hour of Paid Fun", - "rate" => 10, - "hours" => 1 - ) - payments[0]["earnings"].prepend( - "amount" => 100, - "category" => "other", - "name" => "Cell Phone", - "rate" => 0, - "hours" => 0 - ) - end - - it "returns the 'hours' from the one with the most hours" do - expect(parsed_payments).to include( - hash_including(hours: 80) - ) - end - end - - context "when there are 'earnings' with category='overtime'" do - let(:payments) do - load_relative_json_file('request_end_user_paystubs_with_overtime_response.json')['data'] - end - - it "adds in overtime into the base hours" do - # 18.0 = 13 hours (category="hourly") + 5 hours (category="overtime") - expect(parsed_payments).to include(hash_including(hours: 18.0)) - end - end - - context "when no 'earnings' have hours worked" do - let(:payments) do - load_relative_json_file('request_end_user_paystubs_with_no_hours_response.json')['data'] - end - - it "returns a 'nil' value for hours" do - expect(parsed_payments).to include(hash_including(hours: nil)) - end - end - - context "when there are 'earnings' with category='sick'" do - let(:payments) do - load_relative_json_file('request_end_user_paystubs_with_sick_time_response.json')['data'] - end - - it "ignores the sick time entries" do - expect(parsed_payments).to include(hash_including(hours: 4.0)) - end - end - - context "when there are 'earnings' with category='other'" do - let(:payments) do - load_relative_json_file('request_end_user_paystubs_with_start_bonus_response.json')['data'] - end - - it "ignores the entries for those bonuses" do - expect(parsed_payments).to include(hash_including(hours: 10.0)) - end - end - - context "when there are 'earnings' with category='premium'" do - let(:payments) do - load_relative_json_file('request_end_user_paystubs_with_multiple_hourly_rates_response.json')['data'] - end - - it "ignores the entries for those bonuses" do - expect(parsed_payments).to include(hash_including(hours: 3.5)) - end + summarized = helper.summarize_by_employer(payments, [ employment ], [ incomes ], [ identities ], cbv_flow.pinwheel_accounts) + expect(summarized).to be_a(Hash) + expect(summarized).to include(account_id) + expect(summarized[account_id]).to match(hash_including( + has_income_data: true, + has_employment_data: true, + has_identity_data: true, + employment: employment, + income: incomes, + identity: identities, + payments: payments, + total: 480720 + )) end end end diff --git a/app/spec/i18n_spec.rb b/app/spec/i18n_spec.rb index 890699c39..b7397a5e0 100644 --- a/app/spec/i18n_spec.rb +++ b/app/spec/i18n_spec.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "rails_helper" require "i18n/tasks" RSpec.describe I18n do diff --git a/app/spec/mailers/caseworker_mailer_spec.rb b/app/spec/mailers/caseworker_mailer_spec.rb index e7d6497a4..d1d8ae323 100644 --- a/app/spec/mailers/caseworker_mailer_spec.rb +++ b/app/spec/mailers/caseworker_mailer_spec.rb @@ -13,7 +13,7 @@ )} let(:caseworker_email) { cbv_flow.cbv_flow_invitation.user.email } let(:account_id) { cbv_flow.pinwheel_accounts.first.pinwheel_account_id } - let(:payments) { stub_post_processed_payments(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) } diff --git a/app/spec/services/pdf_service_spec.rb b/app/spec/services/pdf_service_spec.rb index f7e5a4ed9..9f6d2534e 100644 --- a/app/spec/services/pdf_service_spec.rb +++ b/app/spec/services/pdf_service_spec.rb @@ -17,7 +17,7 @@ ) end let(:account_id) { cbv_flow.pinwheel_accounts.first.pinwheel_account_id } - let(:payments) { stub_post_processed_payments(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) } diff --git a/app/spec/services/pinwheel_service_spec.rb b/app/spec/services/pinwheel_service_spec.rb index 4f14fc84c..70e6e3dac 100644 --- a/app/spec/services/pinwheel_service_spec.rb +++ b/app/spec/services/pinwheel_service_spec.rb @@ -59,8 +59,121 @@ end end - describe 'Error handling' do - skip 'raises an error when receiving a 400' do + describe "#fetch_employment" do + let(:account_id) { SecureRandom.uuid } + + before do + stub_request_employment_info_response + end + + it "returns an Employment object with expected attributes" do + employment = service.fetch_employment(account_id: account_id) + + expect(employment).to be_a(PinwheelService::Employment) + expect(employment).to have_attributes(status: "employed", start_date: "2010-01-01") + expect(employment.employer_phone_number).to have_attributes(value: "+16126597057", type: "work") + end + end + + describe PinwheelService::Paystub do + let(:raw_paystubs_json) do + load_relative_json_file('request_end_user_paystubs_response.json')['data'] + end + + let(:payments) do + raw_paystubs_json.map do |payment_json| + described_class.new( + payment_json, + environment: PinwheelService::ENVIRONMENTS[:sandbox] + ) + end + end + + it "has attributes necessary for rendering" do + expect(payments.first).to have_attributes( + start: "2020-12-10", + end: "2020-12-24", + ) + end + + describe "#hours" do + it "combines hours of earnings entries" do + expect(payments.first.hours).to eq(80) + end + + context "when there are some 'earnings' entries with fewer hours worked" do + before do + raw_paystubs_json[0]["earnings"].prepend( + "amount" => 100, + "category" => "other", + "name" => "One Hour of Paid Fun", + "rate" => 10, + "hours" => 1 + ) + raw_paystubs_json[0]["earnings"].prepend( + "amount" => 100, + "category" => "other", + "name" => "Cell Phone", + "rate" => 0, + "hours" => 0 + ) + end + + it "returns the 'hours' from the one with the most hours" do + expect(payments.first.hours).to eq(80) + end + end + + context "when there are 'earnings' with category='overtime'" do + let(:raw_paystubs_json) do + load_relative_json_file('request_end_user_paystubs_with_overtime_response.json')['data'] + end + + it "adds in overtime into the base hours" do + # 18.0 = 13 hours (category="hourly") + 5 hours (category="overtime") + expect(payments.first.hours).to eq(18.0) + end + end + + context "when no 'earnings' have hours worked" do + let(:raw_paystubs_json) do + load_relative_json_file('request_end_user_paystubs_with_no_hours_response.json')['data'] + end + + it "returns a 'nil' value for hours" do + expect(payments.first.hours).to eq(nil) + end + end + + context "when there are 'earnings' with category='sick'" do + let(:raw_paystubs_json) do + load_relative_json_file('request_end_user_paystubs_with_sick_time_response.json')['data'] + end + + it "ignores the sick time entries" do + expect(payments.first.hours).to eq(4.0) + end + end + + context "when there are 'earnings' with category='other'" do + let(:raw_paystubs_json) do + load_relative_json_file('request_end_user_paystubs_with_start_bonus_response.json')['data'] + end + + it "ignores the entries for those bonuses" do + expect(payments.first.hours).to eq(10.0) + end + end + + context "when there are 'earnings' with category='premium'" do + let(:raw_paystubs_json) do + load_relative_json_file('request_end_user_paystubs_with_multiple_hourly_rates_response.json')['data'] + end + + it "ignores the entries for those bonuses" do + expect(payments.first.hours).to eq(3.5) + end + end end end end diff --git a/app/spec/support/test_helpers.rb b/app/spec/support/test_helpers.rb index 878bb9220..4541795b9 100644 --- a/app/spec/support/test_helpers.rb +++ b/app/spec/support/test_helpers.rb @@ -6,26 +6,32 @@ def stub_environment_variable(variable, value, &block) ENV[variable] = previous_value end - def stub_post_processed_payments(account_id = SecureRandom.uuid) + def stub_payments(account_id = SecureRandom.uuid) 5.times.map do |i| - { + json = { account_id: account_id, employer: "Employer #{i + 1}", net_pay_amount: (100 * (i + 1)), gross_pay_amount: (120 * (i + 1)), - start: Date.today.beginning_of_month + i.months, - end: Date.today.end_of_month + i.months, - hours: (40 * (i + 1)), rate: (10 + i), + pay_date: "2020-01-14", + pay_period_start: "2020-01-01", + pay_period_end: "2020-01-14", + gross_pay_ytd: 1_000, deductions: [], - hours_by_earning_category: { salary: 80 } + earnings: [] } + + PinwheelService::Paystub.new( + json, + environment: PinwheelService::ENVIRONMENTS[:sandbox] + ) end end def stub_employments(account_id = SecureRandom.uuid) 5.times.map do |i| - { + fields = { "account_id" => account_id, "status" => "employed", "start_date" => "2010-01-01", @@ -39,12 +45,14 @@ def stub_employments(account_id = SecureRandom.uuid) }, "title" => nil } + + PinwheelService::Employment.new(fields, environment: PinwheelService::ENVIRONMENTS[:sandbox]) end end def stub_incomes(account_id = SecureRandom.uuid) 5.times.map do |i| - { + fields = { "account_id" => account_id, "id" => "c70bde4d-e1c2-427a-adc1-c17f61eff210", "created_at" => "2024-08-19T19:27:03.220201+00:00", @@ -54,12 +62,14 @@ def stub_incomes(account_id = SecureRandom.uuid) "currency" => "USD", "pay_frequency" => "bi-weekly" } + + PinwheelService::Income.new(fields, environment: PinwheelService::ENVIRONMENTS[:sandbox]) end end def stub_identities(account_id = SecureRandom.uuid) 5.times.map do |i| - { + fields = { "id" => "9583558c-f54c-455d-9519-554416106a0a", "created_at" => "2024-08-23T19:26:34.541298+00:00", "updated_at" => "2024-08-23T19:26:34.541298+00:00", @@ -86,6 +96,8 @@ def stub_identities(account_id = SecureRandom.uuid) } ] } + + PinwheelService::Identity.new(fields, environment: PinwheelService::ENVIRONMENTS[:sandbox]) end end
<%= t(".payment", amount: format_money(payment[:gross_pay_amount]), date: format_date(payment[:pay_date])) %><%= t(".payment", amount: format_money(payment.gross_pay_amount), date: format_date(payment.pay_date)) %>