Skip to content

Add Sorting and Status Filtering to PowerOfAttorneyRequestsController #21345

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,26 @@ def show

private

module Statuses
ALL = [
PENDING = 'pending',
PROCESSED = 'processed'
].freeze
def params_schema
PowerOfAttorneyRequestService::ParamsSchema
end

def validated_params
@validated_params ||= PowerOfAttorneyRequestService::ParamsSchema.validate_and_normalize!(params.to_unsafe_h)
@validated_params ||= params_schema.validate_and_normalize!(params.to_unsafe_h)
end

def poa_requests
@poa_requests ||= filter_by_status(policy_scope(PowerOfAttorneyRequest))
.includes(scope_includes).paginate(page:, per_page:)
.then { |it| sort_params.present? ? it.sorted_by(sort_params[:by], sort_params[:order]) : it }
.includes(scope_includes)
.paginate(page:, per_page:)
end

def filter_by_status(relation)
case status
when Statuses::PENDING
when params_schema::Statuses::PENDING
pending(relation)
when Statuses::PROCESSED
when params_schema::Statuses::PROCESSED
processed(relation)
when NilClass
relation
Expand All @@ -61,16 +60,20 @@ def filter_by_status(relation)
end
end

def sort_params
validated_params.fetch(:sort, {})
end

def page
validated_params[:page][:number]
validated_params.dig(:page, :number)
end

def per_page
validated_params[:page][:size]
validated_params.dig(:page, :size)
end

def status
params[:status].presence
validated_params.fetch(:status, nil)
end

def pending(relation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,29 @@ def mark_replaced!(superseding_power_of_attorney_request)
scope :resolved, -> { joins(:resolution) }

scope :decisioned, lambda {
where(
resolution: {
resolving_type: PowerOfAttorneyRequestDecision.to_s
}
)
joins(:resolution)
.where(
resolution: {
resolving_type: PowerOfAttorneyRequestDecision.to_s
}
)
}

scope :sorted_by, lambda { |sort_column, direction = :asc|
case sort_column
when 'created_at'
order(created_at: direction)
when 'resolved_at'
order_sql = if direction.to_sym == :asc
Arel.sql('ar_power_of_attorney_request_resolutions.created_at ASC NULLS LAST')
else
Arel.sql('ar_power_of_attorney_request_resolutions.created_at DESC NULLS LAST')
end

includes(:resolution).references(:resolution).order(order_sql)
else
raise ArgumentError, "Invalid sort column: #{sort_column}"
end
}

concerning :ProcessedScopes do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ module Size
end
end

module Sort
ALLOWED_FIELDS = %w[created_at].freeze
ALLOWED_ORDERS = %w[asc desc].freeze
DEFAULT_ORDER = 'desc'
end

module Statuses
ALL = [
PENDING = 'pending',
PROCESSED = 'processed'
].freeze
end

Schema = Dry::Schema.Params do
optional(:page).hash do
optional(:number).value(:integer, gteq?: 1)
Expand All @@ -24,7 +37,13 @@ module Size
lteq?: Page::Size::MAX
)
end
# Future PR: Add filter and sort schemas

optional(:sort).hash do
optional(:by).value(:string, included_in?: Sort::ALLOWED_FIELDS)
optional(:order).value(:string, included_in?: Sort::ALLOWED_ORDERS)
end

optional(:status).value(:string, included_in?: Statuses::ALL)
end

class << self
Expand All @@ -46,6 +65,10 @@ def apply_defaults(validated_params)
validated_params[:page] ||= {}
validated_params[:page][:number] ||= 1
validated_params[:page][:size] ||= Page::Size::DEFAULT

if validated_params[:sort].present? && validated_params[:sort][:by].present?
validated_params[:sort][:order] ||= Sort::DEFAULT_ORDER
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,93 @@
require 'rails_helper'

RSpec.describe AccreditedRepresentativePortal::PowerOfAttorneyRequest, type: :model do
it 'validates its form and claimant type' do
poa_request =
build(
:power_of_attorney_request,
power_of_attorney_form: build(
:power_of_attorney_form,
data: {}.to_json
),
power_of_attorney_holder_type: 'abc'
describe 'associations' do
it 'validates its form and claimant type' do
poa_request =
build(
:power_of_attorney_request,
power_of_attorney_form: build(
:power_of_attorney_form,
data: {}.to_json
),
power_of_attorney_holder_type: 'abc'
)

expect(poa_request).not_to be_valid
expect(poa_request.errors.full_messages).to contain_exactly(
'Claimant type is not included in the list',
'Power of attorney holder type is not included in the list',
'Power of attorney form data does not comply with schema'
)
end
end

describe 'scopes' do
let(:time) { Time.zone.parse('2024-12-21T04:45:37.000Z') }

let(:poa_code) { 'x23' }

describe '.sorted_by' do
context 'using created_at column' do
let!(:pending1) { create(:power_of_attorney_request, created_at: time, poa_code:) }
let!(:pending2) { create(:power_of_attorney_request, created_at: time + 1.day, poa_code:) }
let!(:pending3) { create(:power_of_attorney_request, created_at: time + 2.days, poa_code:) }

it 'sorts by creation date ascending' do
result = described_class.sorted_by('created_at', :asc)

expect(result.first).to eq(pending1)
expect(result.last).to eq(pending3)
end

it 'sorts by creation date descending' do
result = described_class.sorted_by('created_at', :desc)

expect(result.first).to eq(pending3)
expect(result.last).to eq(pending1)
end
end

context 'using resolution date' do
let!(:accepted_request) do
create(:power_of_attorney_request, :with_acceptance,
resolution_created_at: time,
created_at: time,
poa_code:)
end

let!(:declined_request) do
create(:power_of_attorney_request, :with_declination,
resolution_created_at: time + 1.day,
created_at: time + 1.day,
poa_code:)
end

let!(:expired_request) do
create(:power_of_attorney_request, :with_expiration,
resolution_created_at: time + 2.days,
created_at: time + 2.days,
poa_code:)
end

it 'sorts by resolution date ascending' do
result = described_class.where.not(resolution: nil).sorted_by('resolved_at', :asc)

expect(result).to eq([accepted_request, declined_request, expired_request])
end

it 'sorts by resolution date descending' do
result = described_class.where.not(resolution: nil).sorted_by('resolved_at', :desc)

expect(result).to eq([expired_request, declined_request, accepted_request])
end
end

expect(poa_request).not_to be_valid
expect(poa_request.errors.full_messages).to contain_exactly(
'Claimant type is not included in the list',
'Power of attorney holder type is not included in the list',
'Power of attorney form data does not comply with schema'
)
context 'with invalid column' do
it 'raises argument error' do
expect { described_class.sorted_by('invalid_column') }.to raise_error(ArgumentError)
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,61 @@ def load_response_fixture(path_suffix)
expect(parsed_response['data'].map { |p| p['id'] }).not_to include(other_poa_request.id)
end

describe 'a variety of POA request configurations' do
describe 'sorting' do
let!(:poa_requests) do
[
create(:power_of_attorney_request, :with_veteran_claimant,
created_at: time.to_time - 2.days,
poa_code:),
create(:power_of_attorney_request, :with_veteran_claimant,
created_at: time.to_time - 1.day,
poa_code:),
create(:power_of_attorney_request, :with_veteran_claimant,
created_at: time.to_time - 3.days,
poa_code:)
]
end

it 'sorts by created_at in ascending order' do
get('/accredited_representative_portal/v0/power_of_attorney_requests',
params: { sort: { by: 'created_at', order: 'asc' } })

expect(response).to have_http_status(:ok)

# check that they're sorted by created_at in ascending order
ids = parsed_response.to_h['data'].map { |item| item['id'] }[0..2]
expect(ids).to eq([poa_requests[2].id, poa_requests[0].id, poa_requests[1].id])
end

it 'sorts by created_at in descending order' do
get('/accredited_representative_portal/v0/power_of_attorney_requests',
params: { sort: { by: 'created_at', order: 'desc' } })

expect(response).to have_http_status(:ok)

# check that they're sorted by created_at in descending order
ids = parsed_response.to_h['data'].map { |item| item['id'] }[1..3]
expect(ids).to eq([poa_requests[1].id, poa_requests[0].id, poa_requests[2].id])
end

it 'returns error for invalid sort field' do
get('/accredited_representative_portal/v0/power_of_attorney_requests',
params: { sort: { by: 'invalid_field' } })

expect(response).to have_http_status(:bad_request)
expect(parsed_response.to_h['errors']).to include(/Invalid parameters/)
end

it 'returns error for invalid sort order' do
get('/accredited_representative_portal/v0/power_of_attorney_requests',
params: { sort: { by: 'created_at', order: 'invalid' } })

expect(response).to have_http_status(:bad_request)
expect(parsed_response.to_h['errors']).to include(/Invalid parameters/)
end
end

describe 'a variety of poa request configurations' do
let(:poa_requests) do
[].tap do |memo|
memo <<
Expand Down
Loading
Loading