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| %>
- <%= 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)) %> |
<% 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