diff --git a/.gitignore b/.gitignore index efc4f55cdb..bbac0ca3c9 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ dump.rdb *.pdf /spec/example_failures.txt +# Track, don't ignore, RSpec comparison PDFs +!spec/fixtures/files/*.pdf + # Ignore Docker stuff (see issues #503, #603) Dockerfile docker-compose.yml diff --git a/Gemfile.lock b/Gemfile.lock index e105eb0cec..bf54030eb1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -798,4 +798,4 @@ DEPENDENCIES webmock (~> 3.24) BUNDLED WITH - 2.5.21 + 2.5.22 diff --git a/app/pdfs/distribution_pdf.rb b/app/pdfs/distribution_pdf.rb index d9f6affcd6..6684ceeb74 100644 --- a/app/pdfs/distribution_pdf.rb +++ b/app/pdfs/distribution_pdf.rb @@ -36,23 +36,40 @@ def compute_and_render text @distribution.partner.name move_up 24 + profile = @distribution.partner.profile + text "Partner Primary Contact:", style: :bold, align: :right font_size 12 - text @distribution.partner.profile.primary_contact_name, align: :right + text profile.primary_contact_name, align: :right font_size 10 - text @distribution.partner.profile.primary_contact_email, align: :right - text @distribution.partner.profile.primary_contact_phone, align: :right + text profile.primary_contact_email, align: :right + text profile.primary_contact_phone, align: :right move_down 10 - if %w(shipped delivered).include?(@distribution.delivery_method) + if (profile.address1.present? || profile.program_address1.present?) && + (@distribution.delivery? || @distribution.shipped?) + if profile.program_address1.blank? + address1 = profile.address1 + address2 = profile.address2 + city = profile.city + state = profile.state + zip_code = profile.zip_code + else + address1 = profile.program_address1 + address2 = profile.program_address2 + city = profile.program_city + state = profile.program_state + zip_code = profile.program_zip_code.to_s + end + move_up 10 text "Delivery address:", style: :bold font_size 10 - text @distribution.partner.profile.address1 - text @distribution.partner.profile.address2 - text @distribution.partner.profile.city - text @distribution.partner.profile.state - text @distribution.partner.profile.zip_code + text address1 + text address2 + text city + text state + text zip_code move_up 40 text "Issued on:", style: :bold, align: :right diff --git a/db/schema.rb b/db/schema.rb index 91483f028f..a185dea300 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_08_30_015517) do +ActiveRecord::Schema[7.1].define(version: 2024_10_02_205346) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/fixtures/files/distribution_pickup.pdf b/spec/fixtures/files/distribution_pickup.pdf new file mode 100644 index 0000000000..44abad3532 Binary files /dev/null and b/spec/fixtures/files/distribution_pickup.pdf differ diff --git a/spec/fixtures/files/distribution_program_address.pdf b/spec/fixtures/files/distribution_program_address.pdf new file mode 100644 index 0000000000..4cfa55a2fb Binary files /dev/null and b/spec/fixtures/files/distribution_program_address.pdf differ diff --git a/spec/fixtures/files/distribution_same_address.pdf b/spec/fixtures/files/distribution_same_address.pdf new file mode 100644 index 0000000000..a38dd633c5 Binary files /dev/null and b/spec/fixtures/files/distribution_same_address.pdf differ diff --git a/spec/pdfs/distribution_pdf_spec.rb b/spec/pdfs/distribution_pdf_spec.rb index 268bd67484..4f22ebe2fc 100644 --- a/spec/pdfs/distribution_pdf_spec.rb +++ b/spec/pdfs/distribution_pdf_spec.rb @@ -1,72 +1,88 @@ -# avoid Rubocop failing with an infinite loop when it checks this cop -# rubocop:disable Layout/ArrayAlignment -describe DistributionPdf do - let(:organization) { create(:organization) } - let(:distribution) { create(:distribution, organization: organization) } - let(:item1) { FactoryBot.create(:item, name: "Item 1", package_size: 50, value_in_cents: 100) } - let(:item2) { FactoryBot.create(:item, name: "Item 2", value_in_cents: 200) } - let(:item3) { FactoryBot.create(:item, name: "Item 3", value_in_cents: 300) } - let(:item4) { FactoryBot.create(:item, name: "Item 4", package_size: 25, value_in_cents: 400) } +require_relative("../support/distribution_pdf_helper") - let(:org_hiding_packages_and_values) do - FactoryBot.create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, - hide_value_columns_on_receipt: true, hide_package_column_on_receipt: true) - end - let(:org_hiding_packages) { FactoryBot.create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, hide_package_column_on_receipt: true) } - let(:org_hiding_values) { FactoryBot.create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, hide_value_columns_on_receipt: true) } +RSpec.configure do |c| + c.include DistributionPDFHelper +end +describe DistributionPdf do before(:each) do - create(:line_item, itemizable: distribution, item: item1, quantity: 50) - create(:line_item, itemizable: distribution, item: item2, quantity: 100) - create(:item_unit, item: item4, name: "pack") - create(:request, :with_item_requests, distribution: distribution, - request_items: [ - {"item_id" => item2.id, "quantity" => 30}, - {"item_id" => item3.id, "quantity" => 50}, - {"item_id" => item4.id, "quantity" => 120, "request_unit" => "pack"} -]) + @organization, @storage_location, @item1, @item2, @item3, @item4 = create_organization_storage_items end - specify "#request_data" do - results = described_class.new(organization, distribution).request_data - expect(results).to eq([ - ["Items Received", "Requested", "Received", "Value/item", "In-Kind Value Received", "Packages"], - ["Item 1", "", 50, "$1.00", "$50.00", "1"], - ["Item 2", 30, 100, "$2.00", "$200.00", nil], - ["Item 3", 50, "", "$3.00", nil, nil], - ["Item 4", 120, "", "$4.00", nil, nil], - ["", "", "", "", ""], - ["Total Items Received", 200, 150, "", "$250.00", ""] - ]) - end + describe "pdf item and column displays" do + let(:org_hiding_packages_and_values) { + create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, + hide_value_columns_on_receipt: true, hide_package_column_on_receipt: true) + } + let(:org_hiding_packages) { create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, hide_package_column_on_receipt: true) } + let(:org_hiding_values) { create(:organization, name: DEFAULT_TEST_ORGANIZATION_NAME, hide_value_columns_on_receipt: true) } - specify "#request_data with custom units feature" do - Flipper.enable(:enable_packs) - results = described_class.new(organization, distribution).request_data - expect(results).to eq([ - ["Items Received", "Requested", "Received", "Value/item", "In-Kind Value Received", "Packages"], - ["Item 1", "", 50, "$1.00", "$50.00", "1"], - ["Item 2", 30, 100, "$2.00", "$200.00", nil], - ["Item 3", 50, "", "$3.00", nil, nil], - ["Item 4", "120 packs", "", "$4.00", nil, nil], - ["", "", "", "", ""], - ["Total Items Received", 200, 150, "", "$250.00", ""] - ]) - end + let(:distribution) { create(:distribution, organization: @organization, storage_location: @storage_location) } - specify "#non_request_data" do - results = described_class.new(organization, distribution).non_request_data - expect(results).to eq([ - ["Items Received", "Value/item", "In-Kind Value", "Quantity", "Packages"], - ["Item 1", "$1.00", "$50.00", 50, "1"], - ["Item 2", "$2.00", "$200.00", 100, nil], - ["", "", "", "", ""], - ["Total Items Received", "", "$250.00", 150, ""] - ]) - end + before(:each) do + create_line_items_request(distribution, @item1, @item2, @item3, @item4) + end + + specify "#request_data with custom units feature" do + Flipper.enable(:enable_packs) + results = described_class.new(@organization, distribution).request_data + expect(results).to eq([ + ["Items Received", "Requested", "Received", "Value/item", "In-Kind Value Received", "Packages"], + ["Item 1", "", 50, "$1.00", "$50.00", "1"], + ["Item 2", 30, 100, "$2.00", "$200.00", nil], + ["Item 3", 50, "", "$3.00", nil, nil], + ["Item 4", "120 packs", "", "$4.00", nil, nil], + ["", "", "", "", ""], + ["Total Items Received", 200, 150, "", "$250.00", ""] + ]) + end + + specify "#non_request_data" do + results = described_class.new(@organization, distribution).non_request_data + expect(results).to eq([ + ["Items Received", "Value/item", "In-Kind Value", "Quantity", "Packages"], + ["Item 1", "$1.00", "$50.00", 50, "1"], + ["Item 2", "$2.00", "$200.00", 100, nil], + ["", "", "", "", ""], + ["Total Items Received", "", "$250.00", 150, ""] + ]) + end + + context "with request data" do + describe "#hide_columns" do + it "hides value and package columns when true on organization" do + pdf = described_class.new(org_hiding_packages_and_values, distribution) + data = pdf.request_data + pdf.hide_columns(data) + expect(data).to eq([ + ["Items Received", "Requested", "Received"], + ["Item 1", "", 50], + ["Item 2", 30, 100], + ["Item 3", 50, ""], + ["Item 4", 120, ""], + ["", "", ""], + ["Total Items Received", 200, 150] + ]) + end - context "with request data" do - describe "#hide_columns" do + it "hides value columns when true on organization" do + pdf = described_class.new(org_hiding_values, distribution) + data = pdf.request_data + pdf.hide_columns(data) + expect(data).to eq([ + ["Items Received", "Requested", "Received", "Packages"], + ["Item 1", "", 50, "1"], + ["Item 2", 30, 100, nil], + ["Item 3", 50, "", nil], + ["Item 4", 120, "", nil], + ["", "", ""], + ["Total Items Received", 200, 150, ""] + ]) + end + end + end + + context "with non request data" do it "hides value and package columns when true on organization" do pdf = described_class.new(org_hiding_packages_and_values, distribution) data = pdf.request_data @@ -97,56 +113,73 @@ ]) end end + context "regardles of request data" do + describe "#hide_columns" do + it "hides package column when true on organization" do + pdf = described_class.new(org_hiding_packages, distribution) + data = pdf.request_data + pdf.hide_columns(data) + expect(data).to eq([ + ["Items Received", "Requested", "Received", "Value/item", "In-Kind Value Received"], + ["Item 1", "", 50, "$1.00", "$50.00"], + ["Item 2", 30, 100, "$2.00", "$200.00"], + ["Item 3", 50, "", "$3.00", nil], + ["Item 4", 120, "", "$4.00", nil], + ["", "", "", "", ""], + ["Total Items Received", 200, 150, "", "$250.00"] + ]) + end + end + end end - context "with non request data" do - it "hides value and package columns when true on organization" do - pdf = described_class.new(org_hiding_packages_and_values, distribution) - data = pdf.request_data - pdf.hide_columns(data) - expect(data).to eq([ - ["Items Received", "Requested", "Received"], - ["Item 1", "", 50], - ["Item 2", 30, 100], - ["Item 3", 50, ""], - ["Item 4", 120, ""], - ["", "", ""], - ["Total Items Received", 200, 150] - ]) + describe "address pdf output" do + before(:each) do + @partner = create_partner(@organization) + @expected_pickup_file_path, @expected_same_address_file_path, @expected_different_address_file_path = create_file_paths end - it "hides value columns when true on organization" do - pdf = described_class.new(org_hiding_values, distribution) - data = pdf.request_data - pdf.hide_columns(data) - expect(data).to eq([ - ["Items Received", "Requested", "Received", "Packages"], - ["Item 1", "", 50, "1"], - ["Item 2", 30, 100, nil], - ["Item 3", 50, "", nil], - ["Item 4", 120, "", nil], - ["", "", ""], - ["Total Items Received", 200, 150, ""] - ]) + context "when the partner has no addresses" do + before(:each) do + create_profile_no_address(@partner) + end + it "doesn't print any address if the delivery type is pickup" do + compare_pdf(@organization, create_dist(@partner, @organization, @storage_location, @item1, @item2, @item3, @item4, :pick_up), @expected_pickup_file_path) + end + it "doesn't print any address if the delivery type is delivery" do + compare_pdf(@organization, create_dist(@partner, @organization, @storage_location, @item1, @item2, @item3, @item4, :delivery), @expected_pickup_file_path) + end + it "doesn't print any address if the delivery type is shipped" do + compare_pdf(@organization, create_dist(@partner, @organization, @storage_location, @item1, @item2, @item3, @item4, :shipped), @expected_pickup_file_path) + end end - end - context "regardles of request data" do - describe "#hide_columns" do - it "hides package column when true on organization" do - pdf = described_class.new(org_hiding_packages, distribution) - data = pdf.request_data - pdf.hide_columns(data) - expect(data).to eq([ - ["Items Received", "Requested", "Received", "Value/item", "In-Kind Value Received"], - ["Item 1", "", 50, "$1.00", "$50.00"], - ["Item 2", 30, 100, "$2.00", "$200.00"], - ["Item 3", 50, "", "$3.00", nil], - ["Item 4", 120, "", "$4.00", nil], - ["", "", "", "", ""], - ["Total Items Received", 200, 150, "", "$250.00"] - ]) + context "when the partner doesn't have a different program address" do + before(:each) do + create_profile_without_program_address(@partner) + end + it "prints the address if the delivery type is delivery" do + compare_pdf(@organization, create_dist(@partner, @organization, @storage_location, @item1, @item2, @item3, @item4, :delivery), @expected_same_address_file_path) + end + it "prints the address if the delivery type is shipped" do + compare_pdf(@organization, create_dist(@partner, @organization, @storage_location, @item1, @item2, @item3, @item4, :shipped), @expected_same_address_file_path) + end + it "doesn't print the address if the delivery type is pickup" do + compare_pdf(@organization, create_dist(@partner, @organization, @storage_location, @item1, @item2, @item3, @item4, :pick_up), @expected_pickup_file_path) + end + end + context "when the partner has a different program/delivery address" do + before(:each) do + create_profile_with_program_address(@partner) + end + it "prints the delivery address if the delivery type is delivery" do + compare_pdf(@organization, create_dist(@partner, @organization, @storage_location, @item1, @item2, @item3, @item4, :delivery), @expected_different_address_file_path) + end + it "prints the delivery address if the delivery type is shipped" do + compare_pdf(@organization, create_dist(@partner, @organization, @storage_location, @item1, @item2, @item3, @item4, :shipped), @expected_different_address_file_path) + end + it "doesn't print any address if the delivery type is pickup" do + compare_pdf(@organization, create_dist(@partner, @organization, @storage_location, @item1, @item2, @item3, @item4, :pick_up), @expected_pickup_file_path) end end end end -# rubocop:enable Layout/ArrayAlignment diff --git a/spec/support/distribution_pdf_helper.rb b/spec/support/distribution_pdf_helper.rb new file mode 100644 index 0000000000..eb30d963b6 --- /dev/null +++ b/spec/support/distribution_pdf_helper.rb @@ -0,0 +1,126 @@ +module DistributionPDFHelper + def create_organization_storage_items + org = FactoryBot.create(:organization, + name: "Essentials Bank 1", + street: "1500 Remount Road", + city: "Front Royal", + state: "VA", + zipcode: "22630", + email: "email1@example.com") + + storage_location = FactoryBot.create(:storage_location, organization: org) + item1 = FactoryBot.create(:item, name: "Item 1", package_size: 50, value_in_cents: 100) + item2 = FactoryBot.create(:item, name: "Item 2", value_in_cents: 200) + item3 = FactoryBot.create(:item, name: "Item 3", value_in_cents: 300) + item4 = FactoryBot.create(:item, name: "Item 4", package_size: 25, value_in_cents: 400) + + [org, storage_location, item1, item2, item3, item4] + end + + def create_partner(organization) + FactoryBot.create(:partner, :uninvited, without_profile: true, + name: "Leslie Sue", + organization: organization) + end + + def create_file_paths + expected_pickup_file_path = Rails.root.join("spec", "fixtures", "files", "distribution_pickup.pdf") + expected_same_address_file_path = Rails.root.join("spec", "fixtures", "files", "distribution_same_address.pdf") + expected_different_address_file_path = Rails.root.join("spec", "fixtures", "files", "distribution_program_address.pdf") + [expected_pickup_file_path, expected_same_address_file_path, expected_different_address_file_path] + end + + private def create_profile(partner:, program_address1:, program_address2:, program_city:, program_state:, program_zip:, + address1: "Example Address 1", city: "Example City", state: "Example State", zip: "12345") + + FactoryBot.create(:partner_profile, + partner_id: partner.id, + primary_contact_name: "Jaqueline Kihn DDS", + primary_contact_email: "van@durgan.example", + address1: address1, + address2: "", + city: city, + state: state, + zip_code: zip, + program_address1: program_address1, + program_address2: program_address2, + program_city: program_city, + program_state: program_state, + program_zip_code: program_zip) + end + + def create_profile_no_address(partner) + create_profile(partner: partner, program_address1: "", program_address2: "", program_city: "", program_state: "", program_zip: "", address1: "", city: "", state: "", zip: "") + end + + def create_profile_without_program_address(partner) + create_profile(partner: partner, program_address1: "", program_address2: "", program_city: "", program_state: "", program_zip: "") + end + + def create_profile_with_program_address(partner) + create_profile(partner: partner, program_address1: "Example Program Address 1", program_address2: "", program_city: "Example Program City", program_state: "Example Program State", program_zip: 54321) + end + + def create_line_items_request(distribution, item1, item2, item3, item4) + FactoryBot.create(:line_item, itemizable: distribution, item: item1, quantity: 50) + FactoryBot.create(:line_item, itemizable: distribution, item: item2, quantity: 100) + FactoryBot.create(:item_unit, item: item4, name: "pack") + FactoryBot.create(:request, :with_item_requests, distribution: distribution, + request_items: [ + {"item_id" => item2.id, "quantity" => 30}, + {"item_id" => item3.id, "quantity" => 50}, + {"item_id" => item4.id, "quantity" => 120, "request_unit" => "pack"} +]) + end + + def create_dist(partner, organization, storage_location, item1, item2, item3, item4, delivery_method) + Time.zone = "America/Los_Angeles" + dist = FactoryBot.create(:distribution, partner: partner, delivery_method: delivery_method, issued_at: DateTime.new(2024, 7, 4, 0, 0, 0, "-07:00"), organization: organization, storage_location: storage_location) + create_line_items_request(dist, item1, item2, item3, item4) + dist + end + + def compare_pdf(organization, distribution, expected_file_path) + pdf = DistributionPdf.new(organization, distribution) + begin + pdf_file = pdf.compute_and_render + + # Run the following from Rails sandbox console (bin/rails/console --sandbox) to regenerate these comparison PDFs: + # => load "spec/support/distribution_pdf_helper.rb" + # => Rails::ConsoleMethods.send(:prepend, DistributionPDFHelper) + # => create_comparison_pdfs + expect(pdf_file).to eq(IO.binread(expected_file_path)) + rescue RSpec::Expectations::ExpectationNotMetError => e + File.binwrite(Rails.root.join("tmp", "failed_match_distribution_" + distribution.delivery_method.to_s + "_" + Time.current.to_s + ".pdf"), pdf_file) + raise e.class, "PDF does not match, written to tmp/", cause: nil + end + end + + private def create_comparison_pdf(organization, storage_location, item1, item2, item3, item4, profile_create_method, expected_file_path, delivery_method) + partner = create_partner(organization) + profile = profile_create_method.bind_call(Class.new.extend(DistributionPDFHelper), partner) + dist = create_dist(partner, organization, storage_location, item1, item2, item3, item4, delivery_method) + pdf_file = DistributionPdf.new(organization, dist).compute_and_render + File.binwrite(expected_file_path, pdf_file) + profile.destroy + dist.destroy + partner.destroy + end + + # helper function that can be called from Rails console to generate comparison PDFs + def create_comparison_pdfs + org, storage_location, item1, item2, item3, item4 = create_organization_storage_items + expected_pickup_file_path, expected_same_address_file_path, expected_different_address_file_path = create_file_paths + + create_comparison_pdf(org, storage_location, item1, item2, item3, item4, DistributionPDFHelper.instance_method(:create_profile_no_address), expected_pickup_file_path, :pick_up) + create_comparison_pdf(org, storage_location, item1, item2, item3, item4, DistributionPDFHelper.instance_method(:create_profile_without_program_address), expected_same_address_file_path, :shipped) + create_comparison_pdf(org, storage_location, item1, item2, item3, item4, DistributionPDFHelper.instance_method(:create_profile_with_program_address), expected_different_address_file_path, :delivery) + + storage_location.destroy + item1.destroy + item2.destroy + item3.destroy + item4.destroy + org.destroy + end +end