From 055a48ff2ed97dac8610101255015ee534a99e38 Mon Sep 17 00:00:00 2001 From: Nick Tiberi Date: Fri, 29 Apr 2022 17:24:13 -0400 Subject: [PATCH] Add ability for patrons to opt into wage garnishment A new page has been added which allows a patron to accept the University Libraries' Lending Policy, which, if the user is eligible, opts them into wage garnishment. Currently, only faculty and staff are eligible to opt into this agreement. Opting in sets the patron standing to 'OK' and sets the GARNISH-DT (garnish date) to the current date. --- app/controllers/lending_policy_controller.rb | 16 ++++ app/javascript/accept_lending_policy.js | 14 +++ app/javascript/packs/application.js | 2 + app/models/patron.rb | 29 ++++++ app/services/symphony_client.rb | 34 ++++++- app/views/lending_policy/show.html.erb | 30 ++++++ app/views/lending_policy/thank_you.html.erb | 3 + config/routes.rb | 4 + config/settings.yml | 4 +- config/settings/test.yml | 2 +- .../lending_policy_controller_spec.rb | 62 +++++++++++++ spec/features/lending_policy_spec.rb | 36 +++++++ spec/jobs/view_checkouts_job_spec.rb | 4 +- spec/jobs/view_holds_job_spec.rb | 4 +- spec/models/patron_spec.rb | 45 ++++++++- spec/services/symphony_client_spec.rb | 93 ++++++++++++++++++- spec/support/data/patrons/patron1.json | 26 +++++- 17 files changed, 398 insertions(+), 10 deletions(-) create mode 100644 app/controllers/lending_policy_controller.rb create mode 100644 app/javascript/accept_lending_policy.js create mode 100644 app/views/lending_policy/show.html.erb create mode 100644 app/views/lending_policy/thank_you.html.erb create mode 100644 spec/controllers/lending_policy_controller_spec.rb create mode 100644 spec/features/lending_policy_spec.rb diff --git a/app/controllers/lending_policy_controller.rb b/app/controllers/lending_policy_controller.rb new file mode 100644 index 00000000..09759672 --- /dev/null +++ b/app/controllers/lending_policy_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class LendingPolicyController < ApplicationController + before_action :authenticate_user! + + def accept + if patron.eligible_for_wage_garnishment? + client = SymphonyClient.new + client.accept_lending_policy(patron: patron, session_token: current_user.session_token) + end + + # even if the patron is not eligible for wage garnishment (already opted in or some other reason), + # just redirect them to the thank you path to avoid further user confusion + redirect_to lending_policy_thank_you_path + end +end diff --git a/app/javascript/accept_lending_policy.js b/app/javascript/accept_lending_policy.js new file mode 100644 index 00000000..503b09e0 --- /dev/null +++ b/app/javascript/accept_lending_policy.js @@ -0,0 +1,14 @@ +const acceptLendingPolicy = () => { + const acceptLendingPolicyForm = document.getElementById('accept-lending-policy-form'); + + if (acceptLendingPolicyForm) { + const checkbox = document.getElementById('accept-lending-policy-checkbox'); + const button = document.getElementById('accept-lending-policy-submit'); + + checkbox.addEventListener('change', (event) => { + button.disabled = !event.currentTarget.checked; + }); + } +}; + +export default acceptLendingPolicy; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index a951e8c6..dd33779e 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -17,6 +17,7 @@ import 'bootstrap/dist/js/bootstrap' import './styles' // Application javascript +import acceptLendingPolicy from '../accept_lending_policy' import checkouts from "../view_checkouts" import holds from "../view_holds" import selectAll from "../select_all"; @@ -31,6 +32,7 @@ document.addEventListener("DOMContentLoaded", function() { holds(); checkouts(); viewRequestedHolds(); + acceptLendingPolicy(); }); diff --git a/app/models/patron.rb b/app/models/patron.rb index 11ff065f..18d3fc65 100644 --- a/app/models/patron.rb +++ b/app/models/patron.rb @@ -40,6 +40,12 @@ class Patron COLLECTION: 'The user has been sent to collection agency.' }.with_indifferent_access + WAGE_GARNISHMENT_EXEMPT_LIBRARIES = [ + 'DSL-CARL', + 'DSL-UP', + 'HERSHEY' + ].freeze + validates_presence_of :key def initialize(record) @@ -125,6 +131,24 @@ def address } end + def custom_information + fields['customInformation'] + end + + def eligible_for_wage_garnishment? + faculty_or_staff? && garnish_date == '00000000' && WAGE_GARNISHMENT_EXEMPT_LIBRARIES.exclude?(library) + end + + def faculty_or_staff? + profile = fields.dig('profile', 'key') + + ['FACULTY', 'STAFF'].include?(profile) + end + + def garnish_date + custom_field('GARNISH-DT') + end + private def fields @@ -142,4 +166,9 @@ def extract_address_data(address_field) def standing_code fields.dig('standing', 'key') end + + def custom_field(field_name) + field = custom_information.select { |k| k.dig('fields', 'code', 'key') == field_name } + field.first&.dig('fields', 'data') + end end diff --git a/app/services/symphony_client.rb b/app/services/symphony_client.rb index 0df39cbd..ef8004f3 100644 --- a/app/services/symphony_client.rb +++ b/app/services/symphony_client.rb @@ -47,6 +47,7 @@ def patron_info(patron_key:, session_token:, item_details: {}) params: { includeFields: [ '*', + 'customInformation{patronExtendedInformation{*}}', *patron_linked_resources_fields(item_details) ].join(',') }) @@ -147,6 +148,37 @@ def update_patron_info(patron:, params:, session_token:) json: body end + # Accepts the Libraries' lending policy/opts the user into wage garnishment. + # Changes the patron's standing from BARRED to OK and sets the garnish date to today's date. + def accept_lending_policy(patron:, session_token:) + new_garnish_date = DateTime.now.strftime('%Y%m%d') + + custom_information = patron.custom_information + + custom_information.select { |k| k.dig('fields', 'code', 'key') == 'GARNISH-DT' } + .map! { |k| k['fields']['data'] = new_garnish_date } + + body = { + "resource": '/user/patron', + "key": patron.key, + "fields": { + "standing": { + "resource": '/policy/patronStanding', + "key": 'OK' + }, + "customInformation": custom_information + } + } + + authenticated_request "/user/patron/key/#{patron.key}", + headers: { + 'x-sirs-sessionToken': session_token, + 'SD-Prompt-Return': Settings.symws.additional_headers.sd_prompt_return + }, + method: :put, + json: body + end + def get_hold_info(hold_key, session_token) response_raw = hold_request hold_key, session_token @@ -337,6 +369,6 @@ def base_url end def default_headers - DEFAULT_HEADERS.merge(Settings.symws.headers || {}) + DEFAULT_HEADERS.merge(Settings.symws.default_headers || {}) end end diff --git a/app/views/lending_policy/show.html.erb b/app/views/lending_policy/show.html.erb new file mode 100644 index 00000000..562ffc87 --- /dev/null +++ b/app/views/lending_policy/show.html.erb @@ -0,0 +1,30 @@ +

Acceptance of University Libraries Lending Policy

+ +

+ The University Libraries has revised its Lending Policy to include the following statement: +

+ +

+ "For lost materials, if payment is not received for replacement costs within 6 months from notification that the money is due at the Libraries, payment will be payroll deducted for current University employees. For all other fees, if payment is not received within 60 days, payment will be payroll deducted. Additional fees may be charged for this process." +

+ +

+ The Lending Policy can be seen in its entirety on the Pennsylvania State University Libraries Web Page at https://libraries.psu.edu/services/borrow-renew/borrowing-privileges. The policy is subject to change without individual notice. Any change in borrower status or address should be reported to the Libraries immediately. +

+ +

+ By accepting these terms, the applicant agrees to the policies of the Pennsylvania State University Libraries. Please note that acceptance of this policy is necessary to maintain your borrowing privileges. +

+ +
+ +<%= form_with url: lending_policy_accept_path, method: :post, id: 'accept-lending-policy-form', local: true do %> +
+ + +
+ + <%= submit_tag('Accept & Continue', class: 'btn btn-primary', id: 'accept-lending-policy-submit', disabled: true) %> +<% end %> diff --git a/app/views/lending_policy/thank_you.html.erb b/app/views/lending_policy/thank_you.html.erb new file mode 100644 index 00000000..537ad4ab --- /dev/null +++ b/app/views/lending_policy/thank_you.html.erb @@ -0,0 +1,3 @@ +

Thank You

+ +

Thank you for accepting the Libraries' Fee Policy.

diff --git a/config/routes.rb b/config/routes.rb index 5d94a7ef..d19c71ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,10 @@ delete '/holds/batch', to: 'holds#batch_destroy', as: :holds_batch_destroy patch '/checkouts/batch', to: 'checkouts#batch_update', as: :renewals_batch_update + get 'accept-lending-policy', to: 'lending_policy#show', as: :lending_policy_show + post 'accept-lending-policy', to: 'lending_policy#accept', as: :lending_policy_accept + get 'accept-lending-policy/thank-you', to: 'lending_policy#thank_you', as: :lending_policy_thank_you + get 'holds/all', to: 'holds#all', as: :holds_all get 'holds/result', to: 'holds#result', as: :result get 'checkouts/all', to: 'checkouts#all', as: :checkouts_all diff --git a/config/settings.yml b/config/settings.yml index fd1329bb..fa66d7e2 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -14,11 +14,13 @@ symws: url: <%= ENV.fetch("SYMWS_URL") { nil } %> username: <%= ENV.fetch("SYMWS_USERNAME") { nil } %> pin: "<%= ENV.fetch("SYMWS_PIN") { nil } %>" - headers: + default_headers: sd_originating_app_id: cs x_sirs_clientID: <%= ENV.fetch("X_SIRS_CLIENTID") { "PSUCATALOG" } %> content_type: 'application/json' accept: 'application/json' + additional_headers: + sd_prompt_return: <%= ENV.fetch("SYMWS_EDIT_OVERRIDE") { nil } %> redis: sidekiq: uri: <%= ENV.fetch("REDIS_SIDEKIQ_URI") { "redis://127.0.0.1:6379/1" } %> diff --git a/config/settings/test.yml b/config/settings/test.yml index c1902340..20e35379 100644 --- a/config/settings/test.yml +++ b/config/settings/test.yml @@ -1,7 +1,7 @@ symws: webaccess_url: https://example.com/webaccess url: https://example.com/symwsbc - headers: + default_headers: x_sirs_clientID: SymWSTestClient login_params: login: 'fake_user' diff --git a/spec/controllers/lending_policy_controller_spec.rb b/spec/controllers/lending_policy_controller_spec.rb new file mode 100644 index 00000000..7ba2e2e6 --- /dev/null +++ b/spec/controllers/lending_policy_controller_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe LendingPolicyController do + let(:mock_patron) { instance_double(Patron, eligible_for_wage_garnishment?: false) } + let(:mock_client) { instance_double(SymphonyClient, ping?: true) } + + context 'with unauthenticated user' do + it 'goes to the application root' do + get(:show) + expect(response).to redirect_to root_url + end + end + + context 'with an authenticated request' do + let(:user) { + instance_double(User, + username: 'zzz123', + name: 'Zeke', + patron_key: '1234567', + session_token: 'e0b5e1a3e86a399112b9eb893daeacfd') + } + + before do + warden.set_user(user) + allow(controller).to receive(:patron).and_return(mock_patron) + allow(controller).to receive(:current_user).and_return(user) + allow(SymphonyClient).to receive(:new).and_return(mock_client) + allow(mock_client).to receive(:accept_lending_policy) + end + + describe 'GET #show' do + it 'renders the show template' do + expect(get(:show)).to render_template 'show' + end + end + + describe 'POST #accept' do + context 'when the patron is not eligible for wage garnishment' do + it 'renders the thank you template' do + expect(post(:accept)).to redirect_to lending_policy_thank_you_path + end + end + + context 'when the patron is eligible for wage garnishment' do + let(:mock_patron) { instance_double(Patron, eligible_for_wage_garnishment?: true) } + + it 'accepts the lending policy' do + post(:accept) + + expect(mock_client).to have_received(:accept_lending_policy) + .with(patron: mock_patron, session_token: user.session_token) + end + + it 'renders the thank you template' do + expect(post(:accept)).to redirect_to lending_policy_thank_you_path + end + end + end + end +end diff --git a/spec/features/lending_policy_spec.rb b/spec/features/lending_policy_spec.rb new file mode 100644 index 00000000..cce7213d --- /dev/null +++ b/spec/features/lending_policy_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Lending Policy', type: :feature do + let(:mock_user) { 'patron1' } + + before do + login_permanently_as username: 'PATRON1', patron_key: mock_user + visit lending_policy_show_path + end + + after do + Warden::Manager._on_request.clear + Redis.current.flushall + end + + describe 'when the user clicks the checkbox', js: true do + it 'toggles the enabled state of the button' do + expect(page).to have_button('Accept & Continue', disabled: true) + + page.check 'accept-lending-policy-checkbox' + + expect(page).to have_button('Accept & Continue', disabled: false) + end + end + + describe 'when the user submits the form', js: true do + it 'redirects the user to the thank you page' do + page.check 'accept-lending-policy-checkbox' + page.click_button 'Accept & Continue' + + expect(page).to have_current_path lending_policy_thank_you_path, ignore_query: true + end + end +end diff --git a/spec/jobs/view_checkouts_job_spec.rb b/spec/jobs/view_checkouts_job_spec.rb index fc483bba..9c8921e1 100644 --- a/spec/jobs/view_checkouts_job_spec.rb +++ b/spec/jobs/view_checkouts_job_spec.rb @@ -32,7 +32,9 @@ context 'when SymphonyClient does not respond with 200/OK' do before do - stub_request(:get, 'https://example.com/symwsbc/user/patron/key/patron2?includeFields=*,circRecordList%7B*,bib%7Btitle,author,callList%7B*%7D%7D,item%7B*,bib%7Bshadowed,title,author%7D,call%7BsortCallNumber,dispCallNumber%7D%7D%7D') + stub_request(:get, 'https://example.com/symwsbc/user/patron/key/patron2?includeFields=*,'\ + 'customInformation{patronExtendedInformation{*}},circRecordList%7B*,bib%7Btitle,author,callList%7B*%7D%7D,'\ + 'item%7B*,bib%7Bshadowed,title,author%7D,call%7BsortCallNumber,dispCallNumber%7D%7D%7D') .to_return(status: 500, body: '{ "messageList": [{ "message": "A bad thing happened" }] }', headers: {}) end diff --git a/spec/jobs/view_holds_job_spec.rb b/spec/jobs/view_holds_job_spec.rb index 0627d37f..5f195a28 100644 --- a/spec/jobs/view_holds_job_spec.rb +++ b/spec/jobs/view_holds_job_spec.rb @@ -32,7 +32,9 @@ context 'when SymphonyClient does not respond with 200/OK' do before do - stub_request(:get, 'https://example.com/symwsbc/user/patron/key/patron1?includeFields=*,holdRecordList%7B*,bib%7Btitle,author,callList%7B*%7D%7D,item%7B*,bib%7Bshadowed,title,author%7D,call%7BsortCallNumber,dispCallNumber%7D%7D%7D') + stub_request(:get, 'https://example.com/symwsbc/user/patron/key/patron1?includeFields=*,'\ + 'customInformation{patronExtendedInformation{*}},holdRecordList%7B*,bib%7Btitle,author,callList%7B*%7D%7D,'\ + 'item%7B*,bib%7Bshadowed,title,author%7D,call%7BsortCallNumber,dispCallNumber%7D%7D%7D') .to_return(status: 500, body: '{ "messageList": [{ "message": "A bad thing happened" }] }', headers: {}) end diff --git a/spec/models/patron_spec.rb b/spec/models/patron_spec.rb index 8d4255df..71569989 100644 --- a/spec/models/patron_spec.rb +++ b/spec/models/patron_spec.rb @@ -26,6 +26,9 @@ library: { key: 'UP-PAT' }, + profile: { + key: 'STAFF' + }, address1: [{ resource: '/user/patron/address1', fields: { code: { resource: '/policy/patronAddress1', @@ -60,7 +63,31 @@ fields: { code: { resource: '/policy/patronAddress1', key: 'CITY/STATE' - }, data: 'Jersey Shore, PA' } }] + }, data: 'Jersey Shore, PA' } }], + customInformation: [ + { + resource: '/user/patron/customInformation', + key: '1', + fields: { + code: { + resource: '/policy/patronExtendedInformation', + key: 'PSUACCOUNT' + }, + data: '20050801' + } + }, + { + resource: '/user/patron/customInformation', + key: '19', + fields: { + code: { + resource: '/policy/patronExtendedInformation', + key: 'GARNISH-DT' + }, + data: '00000000' + } + } + ] } end @@ -80,6 +107,10 @@ expect(patron.last_name).to eq 'Borrower' end + it 'has a suffix' do + expect(patron.suffix).to eq 'Jr' + end + it 'has a display name' do expect(patron.display_name).to eq 'Borrower Jr, Student Person' end @@ -109,6 +140,18 @@ zip: '00000' }) end + it 'has a garnish date' do + expect(patron.garnish_date).to eq '00000000' + end + + it 'has the correct faculty/staff status' do + expect(patron.faculty_or_staff?).to be true + end + + it 'has the correct wage garnishment eligibility status' do + expect(patron.eligible_for_wage_garnishment?).to be true + end + context 'with checkouts' do before do fields[:circRecordList] = [{ key: 1, fields: { status: 'ACTIVE' } }] diff --git a/spec/services/symphony_client_spec.rb b/spec/services/symphony_client_spec.rb index 6b538cc1..031c2d5e 100644 --- a/spec/services/symphony_client_spec.rb +++ b/spec/services/symphony_client_spec.rb @@ -21,11 +21,14 @@ before do stub_request(:post, "#{Settings.symws.url}/user/staff/login") .with(body: Settings.symws.login_params.to_h, - headers: Settings.symws.headers) + headers: Settings.symws.default_headers) .to_return(body: { sessionToken: user.session_token }.to_json) + search_headers = Settings.symws.default_headers + .to_h.merge('X-Sirs-Sessiontoken': 'e0b5e1a3e86a399112b9eb893daeacfd') + stub_request(:get, "#{Settings.symws.url}/user/patron/search") - .with(headers: Settings.symws.headers.to_h.merge('X-Sirs-Sessiontoken': 'e0b5e1a3e86a399112b9eb893daeacfd'), + .with(headers: search_headers, query: hash_including(includeFields: '*')) .to_return(status: 200, body: { result: [{ key: Settings.symws.patron_key, fields: '' }] }.to_json) @@ -39,7 +42,7 @@ describe '#get_patron_record' do before do stub_request(:get, "#{Settings.symws.url}/user/patron/search") - .with(headers: Settings.symws.headers.to_h.merge('X-Sirs-Sessiontoken': 'token'), + .with(headers: Settings.symws.default_headers.to_h.merge('X-Sirs-Sessiontoken': 'token'), query: hash_including(includeFields: '*')) .to_return(status: 200, body: { result: [{ key: Settings.symws.patron_key, fields: '' }] }.to_json) @@ -126,6 +129,90 @@ end end + describe '#accept_lending_policy' do + let(:mock_patron) { instance_double(Patron, + barcode: '1234', + library: 'UP-PAT', + key: user.patron_key, + custom_information: [ + { + resource: '/user/patron/customInformation', + key: '1', + fields: { + code: { + resource: '/policy/patronExtendedInformation', + key: 'PSUACCOUNT' + }.with_indifferent_access, + data: '20050801' + }.with_indifferent_access + }.with_indifferent_access, + { + resource: '/user/patron/customInformation', + key: '19', + fields: { + code: { + resource: '/policy/patronExtendedInformation', + key: 'GARNISH-DT' + }.with_indifferent_access, + data: '00000000' + }.with_indifferent_access + }.with_indifferent_access + ], + garnish_date: '00000000') } + + let(:new_garnish_date) { DateTime.now.strftime('%Y%m%d') } + + let(:new_custom_information) { [ + { + resource: '/user/patron/customInformation', + key: '1', + fields: { + code: { + resource: '/policy/patronExtendedInformation', + key: 'PSUACCOUNT' + }.with_indifferent_access, + data: '20050801' + }.with_indifferent_access + }.with_indifferent_access, + { + resource: '/user/patron/customInformation', + key: '19', + fields: { + code: { + resource: '/policy/patronExtendedInformation', + key: 'GARNISH-DT' + }.with_indifferent_access, + data: new_garnish_date + }.with_indifferent_access + }.with_indifferent_access + ] } + + let(:request_body) { + { + resource: '/user/patron', + key: '1234567', + fields: { + standing: { + resource: '/policy/patronStanding', + key: 'OK' + }, + customInformation: new_custom_information + } + } + } + + before do + stub_request(:put, "#{Settings.symws.url}/user/patron/key/1234567") + .with(body: request_body) + end + + it 'sends a request to update the garnish date and patron standing' do + client.accept_lending_policy(patron: mock_patron, session_token: user.session_token) + expect(WebMock).to have_requested(:put, "#{Settings.symws.url}/user/patron/key/1234567") + .with(body: request_body) + end + end + describe '#change_pickup_library' do let(:symphony_response) { { status: 200 } } let(:change_pickup_library_response) { client.change_pickup_library(hold_key: 'a_hold_key', diff --git a/spec/support/data/patrons/patron1.json b/spec/support/data/patrons/patron1.json index 31b95c6a..b66cd6f6 100644 --- a/spec/support/data/patrons/patron1.json +++ b/spec/support/data/patrons/patron1.json @@ -869,6 +869,30 @@ "middleName": "H", "preferredName": "", "department": "", - "barcode": "999982273" + "barcode": "999982273", + "customInformation": [ + { + "resource": "/user/patron/customInformation", + "key": "1", + "fields": { + "code": { + "resource": "/policy/patronExtendedInformation", + "key": "PSUACCOUNT" + }, + "data": "20050801" + } + }, + { + "resource": "/user/patron/customInformation", + "key": "19", + "fields": { + "code": { + "resource": "/policy/patronExtendedInformation", + "key": "GARNISH-DT" + }, + "data": "00000000" + } + } + ] } } \ No newline at end of file