Skip to content
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

Art/poa requests/custom sorting #21345

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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 @@ -4,43 +4,52 @@ module AccreditedRepresentativePortal
module V0
class PowerOfAttorneyRequestsController < ApplicationController
include PowerOfAttorneyRequests

before_action do
authorize PowerOfAttorneyRequest
end

with_options only: :show do
before_action do
id = params[:id]
set_poa_request(id)
end
end

# rubocop:disable Metrics/MethodLength
def index
schema = PowerOfAttorneyRequestService::ParamsSchema
validated_params = schema.validate_and_normalize!(params.to_unsafe_h)
page_params = validated_params.fetch(:page, {})
sort_params = validated_params.fetch(:sort, {})
status = validated_params.fetch(:status, nil)

relation = policy_scope(PowerOfAttorneyRequest)
status = params[:status].presence

relation =
case status
when Statuses::PENDING
pending(relation)
when Statuses::PROCESSED
processed(relation)
when NilClass
relation
else
raise ActionController::BadRequest, <<~MSG.squish
Invalid status parameter.
Must be one of (#{Statuses::ALL.join(', ')})
MSG
end
relation = case status
when schema::Statuses::PENDING
pending(relation)
when schema::Statuses::PROCESSED
processed(relation)
when NilClass
relation
else
raise ActionController::BadRequest, <<~MSG.squish
Invalid status parameter.
Must be one of (#{Statuses::ALL.join(', ')})
MSG
end

# `limit(100)` in case pagination isn't introduced quickly enough.
poa_requests = relation.includes(scope_includes).limit(100)
serializer = PowerOfAttorneyRequestSerializer.new(poa_requests)
poa_requests = relation
.then { |it| sort_params.present? ? it.sorted_by(sort_params[:by], sort_params[:order]) : it }
.includes(scope_includes)
.paginate(page: page_params[:number], per_page: page_params[:size])

render json: serializer.serializable_hash, status: :ok
serializer = PowerOfAttorneyRequestSerializer.new(poa_requests)
render json: {
data: serializer.serializable_hash,
meta: pagination_meta(poa_requests)
}, status: :ok
end
# rubocop:enable Metrics/MethodLength

def show
serializer = PowerOfAttorneyRequestSerializer.new(@poa_request)
Expand All @@ -49,13 +58,6 @@ def show

private

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

def pending(relation)
relation
.not_processed
Expand All @@ -77,6 +79,17 @@ def scope_includes
{ resolution: :resolving }
]
end

def pagination_meta(poa_requests)
{
page: {
number: poa_requests.current_page,
size: poa_requests.limit_value,
total: poa_requests.total_entries,
total_pages: poa_requests.total_pages
}
}
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,26 @@ def mark_replaced!(superseding_power_of_attorney_request)
scope :resolved, -> { joins(:resolution) }

scope :decisioned, lambda {
where(
resolution: {
resolving_type: PowerOfAttorneyRequestDecision.to_s
}
)
left_outer_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'
safe_direction = direction.to_sym == :asc ? 'ASC' : 'DESC'
includes(:resolution)
.references(:resolution)
.order(Arel.sql("ar_power_of_attorney_request_resolutions.created_at #{safe_direction} NULLS LAST"))
else
raise ArgumentError, "Invalid sort column: #{sort_column}"
end
}

concerning :ProcessedScopes do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

module AccreditedRepresentativePortal
module PowerOfAttorneyRequestService
module ParamsSchema
# Load extensions needed for schema validation
Dry::Schema.load_extensions(:json_schema)
Dry::Schema.load_extensions(:hints)

ALLOWED_SORT_FIELDS = %w[created_at updated_at].freeze

module Page
module Size
MIN = 10
MAX = 100
DEFAULT = 10
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)
optional(:size).value(
:integer,
gteq?: Page::Size::MIN,
lteq?: Page::Size::MAX
)
end

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
def validate_and_normalize!(params)
result = Schema.call(params)
result.success? or raise(
ActionController::BadRequest,
"Invalid parameters: #{result.errors.messages}"
)

result.to_h.tap do |validated_params|
apply_defaults(validated_params)
end
end

private

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
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,109 @@
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' }

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:) }

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

describe '.sorted_by' do
context 'using created_at column' do
before do
pending1
pending2
pending3
end

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
before do
accepted_request
declined_request
expired_request
end

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

expect(result.first).to eq(accepted_request)
expect(result.second).to eq(declined_request)
expect(result.third).to eq(expired_request)
end

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

expect(result.first).to eq(expired_request)
expect(result.second).to eq(declined_request)
expect(result.third).to eq(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
Loading
Loading