-
-
Notifications
You must be signed in to change notification settings - Fork 527
/
Copy pathexport_donations_csv_service.rb
141 lines (119 loc) · 3.89 KB
/
export_donations_csv_service.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
module Exports
class ExportDonationsCSVService
def initialize(donation_ids:, organization:)
# Use a where lookup so that I can eager load all the resources
# needed rather than depending on external code to do it for me.
# This makes this code more self contained and efficient!
@donations = Donation.includes(
:storage_location,
:donation_site,
:product_drive,
:product_drive_participant,
line_items: [:item]
).where(
id: donation_ids,
).order(created_at: :asc)
@organization = organization
end
def generate_csv
csv_data = generate_csv_data
CSV.generate(headers: true) do |csv|
csv_data.each { |row| csv << row }
end
end
def generate_csv_data
csv_data = []
csv_data << headers
donations.each do |donation|
csv_data << build_row_data(donation)
end
csv_data
end
private
attr_reader :donations
def headers
# Build the headers in the correct order
base_headers + item_headers
end
# Returns a Hash of keys to indexes so that obtaining the index
# doesn't require a linear scan.
def headers_with_indexes
@headers_with_indexes ||= headers.each_with_index.to_h
end
# This method keeps the base headers associated with the lambdas
# for extracting the values for the base columns from the given
# donation.
#
# Doing so (as opposed to expressing them in distinct methods) makes
# it less likely that a future edit will inadvertently modify the
# order of the headers in a way that isn't also reflected in the
# values for the these base columns.
#
# Reminder: Since Ruby 1.9, Hashes are ordered based on insertion
# (or on the order of the literal).
def base_table
{
"Source" => ->(donation) {
donation.source
},
"Date" => ->(donation) {
donation.issued_at.strftime("%F")
},
"Details" => ->(donation) {
donation.details
},
"Storage Location" => ->(donation) {
donation.storage_view
},
"Quantity of Items" => ->(donation) {
donation.line_items.total
},
"Variety of Items" => ->(donation) {
donation.line_items.map(&:name).uniq.size
},
"In-Kind Total" => ->(donation) {
donation.in_kind_value_money
},
"Comments" => ->(donation) {
donation.comment
}
}
end
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 { |h| [h, "#{h} 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
donation.line_items.each do |line_item|
item_name = line_item.item.name
item_column_idx = headers_with_indexes[item_name]
row[item_column_idx] += line_item.quantity
row[item_column_idx + 1] += Money.new(line_item.value_per_line_item) if @organization.include_in_kind_values_in_exported_files
end
convert_to_dollar(row)
end
def make_item_quantity_and_value_slots
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
def convert_to_dollar(row)
row.map do |column|
column.is_a?(Money) ? column.to_f : column
end
end
end
end