Skip to content

Commit

Permalink
Beneficiary Stats API
Browse files Browse the repository at this point in the history
  • Loading branch information
samnang committed Jan 9, 2025
1 parent 195edad commit cc532ec
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 2 deletions.
33 changes: 33 additions & 0 deletions app/controllers/api/v1/beneficiaries/stats_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module API
module V1
module Beneficiaries
class StatsController < BaseController
def index
validate_request_schema(
with: ::V1::BeneficiaryStatsRequestSchema,
serializer_class: StatSerializer,
**serializer_options
) do |permitted_params|
AggregateDataQuery.new(permitted_params).apply(beneficiaries_scope)
end
end

private

def beneficiaries_scope
current_account.beneficiaries
end

def serializer_options
{
input_params: request.query_parameters,
decorator_class: nil,
pagination_options: {
sort_direction: :asc
}
}
end
end
end
end
end
15 changes: 15 additions & 0 deletions app/models/aggregate_data_query.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class AggregateDataQuery
attr_reader :filters, :groups

def initialize(options)
@filters = options.fetch(:filters, {})
@groups = Array(options.fetch(:groups))
end

def apply(scope)
result = scope.where(filters).group(groups).count
result.map.with_index do |(key, value), index|
StatResult.new(groups:, key:, value:, sequence_number: index + 1)
end
end
end
11 changes: 11 additions & 0 deletions app/models/stat_result.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
StatResult = Data.define(:key, :groups, :value, :sequence_number) do
def id
StatResult::IDGenerator.generate_id(key)
end
end

class StatResult::IDGenerator
def self.generate_id(key)
Digest::SHA256.hexdigest(Array(key).reject(&:blank?).map(&:downcase).join(":"))
end
end
49 changes: 49 additions & 0 deletions app/request_schemas/v1/beneficiary_stats_request_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module V1
class BeneficiaryStatsRequestSchema < ApplicationRequestSchema
# Group = Data.define(:name, :column)

# COUNTRY_GROUP = Group.new(name: "country", column: :iso_country_code)
# REGION_GROUP = Group.new(name: "region", column: :iso_region_code)
# LOCALITY_GROUP = Group.new(name: "locality", column: :locality)
#
# GROUPS = [ COUNTRY_GROUP, REGION_GROUP, LOCALITY_GROUP ].freeze
#
# VALID_GROUP_BY_OPTIONS = [
# [ COUNTRY_GROUP, REGION_GROUP, LOCALITY_GROUP ]
# ]

params do
optional(:filter).value(:hash).hash do
optional(:gender).filled(Types::UpcaseString, included_in?: Contact.gender.values)
end
required(:group_by).value(array[:string])
end

# rule(:group_by) do |context:|
# context[:groups] = find_groups(value)
# key.failure("is invalid") if context[:groups].blank?
# end

def output
result = super

# filter = params.fetch(:filter)
# conditions = filter.slice(:type, :locality)
# conditions[:iso_country_code] = filter.fetch(:country) if filter.key?(:country)
# conditions[:iso_region_code] = filter.fetch(:region) if filter.key?(:region)
#
# result = {}
#
# result[:named_scopes] = :available
# result[:conditions] = conditions
result[:groups] = result[:group_by]
result
end

private

def find_groups(group_names)
VALID_GROUP_BY_OPTIONS.find { |group_list| group_list.map(&:name).sort == group_names.sort }
end
end
end
17 changes: 17 additions & 0 deletions app/serailizers/stat_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class StatSerializer < JSONAPISerializer
attribute :result do |object|
result = {}

object.groups.each_with_index do |group, index|
group_value = Array(object.key)[index]

# NOTE: Handle field value object. Eg. enumerize field.
group_value = group_value.value if group_value.respond_to?(:value)

result[group] = group_value
end

result[:value] = object.value
result
end
end
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@

namespace :v1, module: "api/v1", as: "api_v1", defaults: { format: "json" } do
resources :beneficiaries, only: [ :index, :create, :show, :update ] do
get "stats" => "beneficiaries/stats#index", on: :collection

resources :addresses, only: [ :index, :create, :show, :destroy ]
end
end
Expand Down
3 changes: 1 addition & 2 deletions lib/invalid_request_schema_responder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ def display(_resource, given_options = {})
end

def json_resource_errors
serializer_class = options.fetch(:error_serializer_class) { resource.class.error_serializer_class }
serializer_class = options.fetch(:error_serializer_class) { JSONAPIRequestSchemaErrorsSerializer }
errors = serializer_class.new(resource).as_json

Rails.logger.info(errors)

errors
end
end

50 changes: 50 additions & 0 deletions spec/requests/open_ews_api/v1/beneficiaries_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,54 @@
)
end
end

get "/v1/beneficiaries/stats" do
with_options scope: :filter do
parameter(
:gender, "Must be either `M` or `F`",
required: false
)
end

parameter(
:group_by,
"An array of fields to group by.`",
required: true
)

example "Fetch beneficiaries stats" do
account = create(:account)
create_list(:beneficiary, 2, account:, gender: "M")
create(:beneficiary, account:, gender: "F")

set_authorization_header_for(account)
do_request(group_by: [ "gender" ])

expect(response_status).to eq(200)
expect(response_body).to match_jsonapi_resource_collection_schema("stat")
results = json_response.fetch("data").map { |data| data.dig("attributes", "result") }

expect(results).to eq(
[
{
"gender" => "M",
"value" => 2
},
{
"gender" => "F",
"value" => 1
}
]
)
end

example "Handles invalid requests", document: false do
account = create(:account)

set_authorization_header_for(account)
do_request

expect(response_status).to eq(400)
end
end
end
10 changes: 10 additions & 0 deletions spec/support/api_response_schemas/stat_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module APIResponseSchema
StatSchema = Dry::Schema.JSON do
required(:id).filled(:str?)
required(:type).filled(eql?: "stat")

required(:attributes).schema do
required(:result).filled(:hash?)
end
end
end

0 comments on commit cc532ec

Please sign in to comment.