Skip to content
Open
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
29 changes: 9 additions & 20 deletions app/services/exports/export_donations_csv_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ def initialize(donation_ids:, organization:)
).order(created_at: :asc)

@organization = organization
item_header_names = @organization.items.select("DISTINCT ON (LOWER(name)) items.name").order("LOWER(name) ASC").map(&:name)

@item_headers = if @organization.include_in_kind_values_in_exported_files
item_header_names.flat_map { |header| [header, "#{header} In-Kind Value"] }
else
item_header_names
end
end

def generate_csv
Expand Down Expand Up @@ -42,7 +49,7 @@ def generate_csv_data

def headers
# Build the headers in the correct order
base_headers + item_headers
base_headers + @item_headers
end

# Returns a Hash of keys to indexes so that obtaining the index
Expand Down Expand Up @@ -95,24 +102,6 @@ def base_headers
base_table.keys
end

def item_headers
return @item_headers if @item_headers

item_names = Set.new

donations.each do |donation|
donation.line_items.each do |line_item|
item_names.add(line_item.item.name)
end
end

@item_headers = item_names.sort

@item_headers = @item_headers.flat_map { |header| [header, "#{header} In-Kind Value"] } if @organization.include_in_kind_values_in_exported_files

@item_headers
end

def build_row_data(donation)
row = base_table.values.map { |closure| closure.call(donation) }
row += make_item_quantity_and_value_slots
Expand All @@ -128,7 +117,7 @@ def build_row_data(donation)
end

def make_item_quantity_and_value_slots
slots = Array.new(item_headers.size, 0)
slots = Array.new(@item_headers.size, 0)
slots = slots.map.with_index { |value, index| index.odd? ? Money.new(0) : value } if @organization.include_in_kind_values_in_exported_files
slots
end
Expand Down
210 changes: 209 additions & 1 deletion spec/services/exports/export_donations_csv_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ def source_name(donation)
end

context 'while "Include in-kind value in donation and distribution exports?" is set to yes' do
it 'should match the expected content with in-kind value of each item for the csv' do
before do
allow(organization).to receive(:include_in_kind_values_in_exported_files).and_return(true)
end

it 'should match the expected content with in-kind value of each item for the csv' do
csv = <<~CSV
Source,Date,Details,Storage Location,Quantity of Items,Variety of Items,In-Kind Total,Comments,A Item,A Item In-Kind Value,B Item,B Item In-Kind Value,C Item,C Item In-Kind Value,Dupe Item,Dupe Item In-Kind Value,E Item,E Item In-Kind Value
Product Drive,2025-01-01,#{source_name(donations[0])},Test Storage Location,15,2,94.0,It's a fine day for diapers.,7,70.00,0,0.00,0,0.00,8,24.00,0,0.00
Expand All @@ -86,6 +88,212 @@ def source_name(donation)
CSV
expect(subject).to eq(csv)
end

it 'should include inactive items in the export' do
inactive_item = create(:item, :inactive, name: "Inactive Item", organization: organization)
csv_data = described_class.new(donation_ids: donation_ids, organization: organization).generate_csv_data

# Verify the inactive item appears in headers
expect(csv_data[0][18]).to eq(inactive_item.name)
expect(csv_data[0][19]).to eq("#{inactive_item.name} In-Kind Value")

# Verify all rows have 0 quantity for the inactive item
csv_data[1..].each do |row|
expect(row[18]).to eq(0)
expect(row[19]).to eq(0)
end
end

it 'should include items that are not in any donation' do
unused_item = create(:item, name: "Unused Item", organization: organization)
csv_data = described_class.new(donation_ids: donation_ids, organization: organization).generate_csv_data

# Verify the unused item appears in headers
expect(csv_data[0][18]).to include(unused_item.name)
expect(csv_data[0][19]).to include("#{unused_item.name} In-Kind Value")

# Verify all rows have 0 quantity for the unused item
csv_data[1..].each do |row|
expect(row[18]).to eq(0)
expect(row[19]).to eq(0)
end
end
end
end

describe '#generate_csv_data' do
let(:organization) { create(:organization) }
let(:generated_csv_data) { described_class.new(donation_ids: donation_ids, organization: organization).generate_csv_data }
let(:donation_ids) { donations.map(&:id) }
let(:duplicate_item) { create(:item, organization: organization) }
let(:items_lists) do
[
[
[duplicate_item, 5],
[create(:item, organization: organization), 7],
[duplicate_item, 3]
],
*(Array.new(3) do |i|
[[create(
:item, name: "item_#{i}", organization: organization
), i + 1]]
end)
]
end

let(:base_headers) do
described_class.new(donation_ids: [], organization: organization).send(:base_headers)
end

let(:item_names) { items_lists.flatten(1).map(&:first).map(&:name).sort.uniq }

let(:donations) do
start_time = Time.current

items_lists.each_with_index.map do |items, i|
donation = create(
:donation,
organization: organization,
donation_site: create(
:donation_site, name: "Space Needle #{i}", organization: organization
),
issued_at: start_time + i.days,
comment: "This is the #{i}-th donation in the test."
)

items.each do |(item, quantity)|
donation.line_items << create(
:line_item, quantity: quantity, item: item
)
end

donation
end
end

let(:expected_headers) do
[
"Source",
"Date",
"Details",
"Storage Location",
"Quantity of Items",
"Variety of Items",
"In-Kind Total",
"Comments"
] + expected_item_headers
end

let(:total_item_quantities) do
template = item_names.index_with(0)

items_lists.map do |items_list|
row = template.dup
items_list.each do |(item, quantity)|
row[item.name] += quantity
end
row.values
end
end

let(:expected_item_headers) do
expect(item_names).not_to be_empty

item_names
end

it 'should match the expected content for the csv' do
expect(generated_csv_data[0]).to eq(expected_headers)

donations.zip(total_item_quantities).each_with_index do |(donation, total_item_quantity), idx|
row = [
donation.source,
donation.issued_at.strftime("%F"),
donation.details,
donation.storage_view,
donation.line_items.total,
total_item_quantity.count(&:positive?),
donation.in_kind_value_money,
donation.comment
]

row += total_item_quantity

expect(generated_csv_data[idx + 1]).to eq(row)
end
end

context 'when an organization\'s item exists but isn\'t in any donation' do
let!(:unused_item) { create(:item, name: "Unused Item", organization: organization) }
let!(:generated_csv_data) do
described_class.new(donation_ids: donations.map(&:id), organization: organization).generate_csv_data
end

it 'should include the unused item as a column with 0 quantities' do
expect(generated_csv_data[0][13]).to eq(unused_item.name)

donations.each_with_index do |_, idx|
row = generated_csv_data[idx + 1]
expect(row[13]).to eq(0)
end
end
end

context 'when an organization\'s item is inactive' do
let!(:inactive_item) { create(:item, name: "Inactive Item", organization: organization, active: false) }
let!(:generated_csv_data) do
described_class.new(donation_ids: donations.map(&:id), organization: organization).generate_csv_data
end

it 'should include the inactive item as a column with 0 quantities' do
expect(generated_csv_data[0][10]).to include(inactive_item.name)

donations.each_with_index do |_, idx|
row = generated_csv_data[idx + 1]
expect(row[10]).to eq(0)
end
end
end

context 'when generating CSV output' do
let(:generated_csv) { described_class.new(donation_ids: donation_ids, organization: organization).generate_csv }

it 'returns a valid CSV string' do
expect(generated_csv).to be_a(String)
expect { CSV.parse(generated_csv) }.not_to raise_error
end

it 'includes headers as first row' do
csv_rows = CSV.parse(generated_csv)
expect(csv_rows.first).to eq(expected_headers)
end

it 'includes data for all donations' do
csv_rows = CSV.parse(generated_csv)
expect(csv_rows.count).to eq(donations.count + 1) # +1 for headers
end
end

context 'when items have different cases' do
let(:item_names) { ["Zebra", "apple", "Banana"] }
let(:expected_order) { ["apple", "Banana", "Zebra"] }
let(:donation) { create(:donation, organization: organization) }
let(:case_sensitive_csv_data) do
# Create items in random order to ensure sort is working
item_names.shuffle.each do |name|
create(:item, name: name, organization: organization)
end

described_class.new(donation_ids: [donation.id], organization: organization).generate_csv_data
end

it 'should sort item columns case-insensitively, ASC' do
# Get just the item columns by removing the known base headers
item_columns = case_sensitive_csv_data[0] - base_headers

# Check that the remaining columns match our expected case-insensitive sort
expect(item_columns).to eq(expected_order)
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

See previous comments re hardcoded CSVs.

end
end
end
Loading