Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -858,4 +858,4 @@ DEPENDENCIES
webmock (~> 3.26)

BUNDLED WITH
2.7.1
4.0.10
18 changes: 17 additions & 1 deletion app/controllers/distributions_by_county_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,26 @@ def report
start_date = helpers.selected_range.first.utc.iso8601
end_date = helpers.selected_range.last.utc.iso8601

@reporting_categories = Item.reporting_categories_for_select
@items = current_organization.items.loose.alphabetized.select(:id, :name)
@selected_reporting_category = filter_params[:by_reporting_category].presence
@selected_item = filter_params[:by_item_id].presence
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we bundle these into a view object? I'd like us to use more of these rather than relying on a bunch of instance variables.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use View::Donations.in donations#index.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nods) Ok - I see we have an example with Donations, so I'll see if I can apply that.


@breakdown = DistributionSummaryByCountyQuery.call(
organization_id: current_organization.id,
start_date: start_date,
end_date: end_date
end_date: end_date,
reporting_category: @selected_reporting_category,
item_id: @selected_item
)
end

helper_method \
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be more readable if you just define the filter_params method and call helper_method :filter_params at the top.

def filter_params
return {} unless params.key?(:filters)

params
.require(:filters)
.permit(:by_item_id, :by_reporting_category, :date_range)
end
end
1 change: 1 addition & 0 deletions app/models/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class Item < ApplicationRecord
scope :alphabetized, -> { order(:name) }
scope :by_base_item, ->(base_item) { where(base_item: base_item) }
scope :by_reporting_category, ->(reporting_category) { where(reporting_category: reporting_category) }
scope :by_name, ->(name) { where(name: name) }
scope :by_partner_key, ->(partner_key) { where(partner_key: partner_key) }

scope :period_supplies, -> {
Expand Down
51 changes: 43 additions & 8 deletions app/queries/distribution_summary_by_county_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ class DistributionSummaryByCountyQuery
SQL_MULTILINE_COMMENTS = /\/\*.*?\*\//

DISTRIBUTION_BY_COUNTY_SQL = <<~SQL.squish.gsub(SQL_MULTILINE_COMMENTS, "").freeze
/* Calculate total item quantity and value per distribution */
WITH distribution_totals AS
/* Calculate total item quantity and value per distribution of "loose" items */

WITH loose_distribution_totals AS
(
SELECT DISTINCT d.id,
d.partner_id,
Expand All @@ -17,9 +18,39 @@ class DistributionSummaryByCountyQuery
JOIN items i ON i.id = li.item_id
WHERE d.issued_at BETWEEN :start_date AND :end_date
AND d.organization_id = :organization_id
AND i.reporting_category LIKE CONCAT('%', :reporting_category , '%')
AND i.id = CASE WHEN :item_id <> 0 THEN :item_id ELSE i.id END
GROUP BY d.id, li.id, i.id
),
/* Match distribution totals with client share and counties.
/* Calculate total item and value per distribution of items that happen to be in kits */
kitted_distribution_totals AS (
SELECT DISTINCT d.id,
d.partner_id,
COALESCE(SUM(li.quantity * kli.quantity) OVER (PARTITION BY d.id), 0) AS quantity,
COALESCE(SUM(COALESCE(ki.value_in_cents, 0) * li.quantity * kli.quantity) OVER (PARTITION BY d.id), 0) AS value
FROM distributions d
INNER JOIN line_items li ON li.itemizable_id = d.id AND li.itemizable_type = 'Distribution'
INNER JOIN items i ON i.id = li.item_id
INNER JOIN line_items AS kli ON i.id = kli.itemizable_id AND kli.itemizable_type = 'Item'
INNER JOIN items AS ki ON ki.id = kli.item_id
WHERE d.issued_at BETWEEN :start_date AND :end_date
AND d.organization_id = :organization_id
AND ki.reporting_category LIKE CONCAT('%', :reporting_category , '%')
AND ki.id = CASE WHEN :item_id <> 0 THEN :item_id ELSE ki.id END
GROUP BY d.id, li.id, i.id, kli.id, ki.id
),

/* Combine the loose and kitted */
full_distribution_totals as (
SELECT distinct COALESCE(ld.id,kd.id) as id,
COALESCE(ld.partner_id, kd.partner_id) AS partner_id,
COALESCE(ld.quantity,0) + COALESCE(kd.quantity, 0) as quantity,
COALESCE(ld.value,0) + COALESCE(kd.value, 0) as value
FROM loose_distribution_totals ld
FULL OUTER JOIN kitted_distribution_totals kd ON ld.id = kd.id
),

/* Match loose distribution totals with client share and counties.
If distribution has no associated county, set county name to "Unspecified"
and set region to ZZZ so it will be last when sorted */
totals_by_county AS
Expand All @@ -30,7 +61,7 @@ class DistributionSummaryByCountyQuery
COALESCE(psa.client_share::float / 100, 1) AS percentage,
COALESCE(c.name, 'Unspecified') county_name,
COALESCE(c.region, 'ZZZ') county_region
FROM distribution_totals dt
FROM full_distribution_totals dt
LEFT JOIN partners p ON p.id = dt.partner_id
LEFT JOIN partner_profiles pp ON pp.partner_id = p.id
LEFT JOIN partner_served_areas psa ON psa.partner_profile_id = pp.id
Expand Down Expand Up @@ -58,11 +89,13 @@ class DistributionSummaryByCountyQuery
class << self
# Timestamps are stored in Postgres without timezones so
# start_date and end_date must be strings in UTC.
def call(organization_id:, start_date: nil, end_date: nil)
def call(organization_id:, start_date: nil, end_date: nil, reporting_category: nil, item_id: nil)
params = {
organization_id: organization_id,
start_date: start_date || "1000-01-01",
end_date: end_date || "3000-01-01"
end_date: end_date || "3000-01-01",
reporting_category: reporting_category,
item_id: item_id
}

execute(to_sql(DISTRIBUTION_BY_COUNTY_SQL, **params)).to_a.map(&to_county_summary)
Expand All @@ -74,13 +107,15 @@ def execute(sql)
ActiveRecord::Base.connection.execute(sql)
end

def to_sql(query, organization_id:, start_date:, end_date:)
def to_sql(query, organization_id:, start_date:, end_date:, reporting_category:, item_id:)
ActiveRecord::Base.sanitize_sql_array(
[
query,
organization_id: organization_id,
start_date: start_date,
end_date: end_date
end_date: end_date,
reporting_category: reporting_category,
item_id: item_id
]
)
end
Expand Down
35 changes: 27 additions & 8 deletions app/views/distributions_by_county/report.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<h1>
Estimated Distributions by County for <%= current_organization.name %>
</h1>
<h5> Please note that any items within kits are included in these estimates
</h5>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
Expand All @@ -25,17 +27,33 @@
<div class="row">
<div class="col-12">
<!-- Default box -->
<div class="card">
<div class="row">
<div class="col-sm-3 col-6">
<div class="card card-primary">
<div class="card-body">

<div class="description-block border-right">
<%= simple_form_for :filters, url: distributions_by_county_report_path(current_organization), method: :get do |f| %>
<%= render partial: "shared/date_range_picker", locals: {css_class: "form-control"} %><br>
<%= filter_button %>
<%= form_tag(distributions_by_county_report_path(current_organization), method: :get) do |f| %>
<div class="row">
<% if @items.present? %>
<div class="form-group col-lg-2 col-md-2 col-sm-6 col-xs-12">
<%= filter_select(label: "Item", scope: :by_item_id, collection: @items, selected: @selected_item) %>
</div>
<% end %>
<div class="form-group col-lg-2 col-md-2 col-sm-6 col-xs-12">
<%= filter_select(label: "Reporting Category", scope: :by_reporting_category, collection: @reporting_categories, selected: @selected_reporting_category) %>
</div>

<div class="form-group col-lg-3 col-md-3 col-sm-6 col-xs-12">
<%= label_tag "Date Range", "Date Range" %>
<%= render partial: "shared/date_range_picker", locals: {css_class: "form-control"} %>
</div>
</div>
<div class="card-footer">
<%= filter_button %>
<%= clear_filter_button %>
</div>
<% end %>
</div>
<!-- /.description-block -->
</div>

</div>

<div class="card-body table-responsive p-0">
Expand Down Expand Up @@ -65,4 +83,5 @@
</div>
</div>
</div>
</div>
</section>
2 changes: 1 addition & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.0].define(version: 2025_10_07_141240) do
ActiveRecord::Schema[8.0].define(version: 2025_10_17_194543) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"

Expand Down
121 changes: 120 additions & 1 deletion spec/queries/distribution_summary_by_county_query_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
let(:organization_id) { organization.id }
let(:start_date) { nil }
let(:end_date) { nil }
let(:params) { {organization_id:, start_date:, end_date:} }
let(:reporting_category) { nil }
let(:item_id) { nil }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:} }

include_examples "distribution_by_county"

Expand Down Expand Up @@ -64,4 +66,121 @@
expect(num_with_0).to eq 0
end
end

describe "handling reporting categories" do
context "with loose items only" do
let(:reporting_category) { :cloth_diapers }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_2, organization: user.organization, partner: partner_1)

breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[4].quantity).to eq(0)
expect(breakdown[4].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(25)
expect(breakdown[i].value).to be_within(0.01).of(26250.0)
end
end
end
context "with kits only" do
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spacing :)

let(:reporting_category) { :pads }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: kit_a.item, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_2, organization: user.organization, partner: partner_1)

breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[-1].quantity).to eq(0)
expect(breakdown[-1].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(500)
expect(breakdown[i].value).to be_within(0.01).of(37500.0)
end
end
end

context "with an item that is in a kit and loose, and another item that is in the reporting category" do
let(:reporting_category) { :pads }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: kit_a.item, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_3, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_4, organization: user.organization, partner: partner_1)
breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[-1].quantity).to eq(0)
expect(breakdown[-1].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(550)
expect(breakdown[i].value).to be_within(0.01).of(40625.0)
end
end
end
end

describe "handling filtering by item" do
context "with loose items only" do
let(:item_id) { item_1.id }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:, item_id:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_2, organization: user.organization, partner: partner_1)

breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[4].quantity).to eq(0)
expect(breakdown[4].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(25)
expect(breakdown[i].value).to be_within(0.01).of(26250.0)
end
end
end

context "with kits only" do
let(:item_id) { item_3.id }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:, item_id:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: kit_a.item, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_2, organization: user.organization, partner: partner_1)

breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[-1].quantity).to eq(0)
expect(breakdown[-1].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(500)
expect(breakdown[i].value).to be_within(0.01).of(37500.0)
end
end
end

context "with an item that is in a kit and loose, and another item that is in the reporting category" do
let(:item_id) { item_3.id }
let(:params) { {organization_id:, start_date:, end_date:, reporting_category:, item_id:} }

it "divides the item numbers and values according to the partner profile" do
create(:distribution, :with_items, item: kit_a.item, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_3, organization: user.organization, partner: partner_1)
create(:distribution, :with_items, item: item_4, organization: user.organization, partner: partner_1)
breakdown = DistributionSummaryByCountyQuery.call(**params)
expect(breakdown.size).to eq(5)
expect(breakdown[-1].quantity).to eq(0)
expect(breakdown[-1].value).to be_within(0.01).of(0)
4.times do |i|
expect(breakdown[i].quantity).to eq(525)
expect(breakdown[i].value).to be_within(0.01).of(39375.0)
end
end
end
end
end
Loading
Loading