diff --git a/.erb-lint.yml b/.erb_lint.yml similarity index 100% rename from .erb-lint.yml rename to .erb_lint.yml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92a9a83624..0fd34f2603 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,20 +53,49 @@ Make sure to install **Ubuntu** as your Linux distribution. (This should be defa
Bank Users 🏦 + Pawnee Diaper Bank + A fully set up bank with items, storage locations, donations, distributions, requests, etc. + The bank has multiple partners associated with it. ``` Organization Admin - Email: org_admin1@example.com + Email: org_admin1@example.com Password: password! User Email: user_1@example.com Password: password! ``` + + Second City Essentials Bank + A fully set up bank with items, storage locations, donations, distributions, requests, etc. + The bank has four items unique to it (named Second City Item #). + ``` + Organization Admin + Email: second_city_admin@example.com + Password: password! + + User + Email: second_city_user@example.com + Password: password! + ``` + + SF Diaper Bank + A bank which has just been accepted and so is not fully set up. It lacks many of the records the other banks have. + ``` + Organization Admin + Email: org_admin2@example.com + Password: password! + + User + Email: user_2@example.com + Password: password! + ```
Partner Users 👥 + Partners in Pawnee Diaper Bank partner groups ``` Verified Partner Email: verified@example.com @@ -87,10 +116,17 @@ Make sure to install **Ubuntu** as your Linux distribution. (This should be defa Waiting Approval Partner Email: waiting@example.com Password: password! - - Another approved partner (with all groups): + + Another verified partner (in second partner group): Email: approved_2@example.com - Pasword: password! + Password: password! + ``` + + Partners in Second City Essentials Bank partner group + ``` + Verified partner + Email: second_city_senior_center@example.com + Password: password! ```
@@ -221,7 +257,7 @@ Before submitting a pull request, run all tests and lints. Fix any broken tests - Once your first PR has been merged, all commits pushed to an open PR will also run these workflows. #### Local testing -- Run all lints with `bin/lint`. +- Run all lints with `bin/lint`. (You can lint a single file/folder with `bin/lint {path_to_folder_or_file}`.) - Run all tests with `bundle exec rspec` - You can run a single test with `bundle exec rspec {path_to_test_name}_spec.rb` or on a specific line by appending `:LineNumber` - If you need to skip a failing test, place `pending("Reason you are skipping the test")` into the `it` block rather than skipping with `xit`. This will allow rspec to deliver the error message without causing the test suite to fail. diff --git a/Gemfile b/Gemfile index 1744d1218e..1b9716089a 100644 --- a/Gemfile +++ b/Gemfile @@ -14,7 +14,7 @@ gem "pg", "~> 1.5.7" # Web server. gem "puma" # Rails web framework. -gem "rails", "7.1.3.4" +gem "rails", "7.2.2" ###### MODELS / DATABASE ####### @@ -100,6 +100,8 @@ gem "jwt" gem "newrelic_rpm" # Used to manage periodic cron-like jobs gem "clockwork" +# Speed up app boot time by caching expensive operations +gem 'bootsnap', require: false ##### DEPENDENCY PINS ###### # These are gems that aren't used directly, only as dependencies for other gems. diff --git a/Gemfile.lock b/Gemfile.lock index b84187030e..db952483cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,80 +2,77 @@ GEM remote: https://rubygems.org/ specs: Ascii85 (1.1.1) - actioncable (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) + actioncable (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.3.4) - actionpack (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activesupport (= 7.1.3.4) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) + actionmailer (7.2.2) + actionpack (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.3.4) - actionview (= 7.1.3.4) - activesupport (= 7.1.3.4) + actionpack (7.2.2) + actionview (= 7.2.2) + activesupport (= 7.2.2) nokogiri (>= 1.8.5) racc - rack (>= 2.2.4) + rack (>= 2.2.4, < 3.2) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.3.4) - actionpack (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + useragent (~> 0.16) + actiontext (7.2.2) + actionpack (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3.4) - activesupport (= 7.1.3.4) + actionview (7.2.2) + activesupport (= 7.2.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.1.3.4) - activesupport (= 7.1.3.4) + activejob (7.2.2) + activesupport (= 7.2.2) globalid (>= 0.3.6) - activemodel (7.1.3.4) - activesupport (= 7.1.3.4) - activerecord (7.1.3.4) - activemodel (= 7.1.3.4) - activesupport (= 7.1.3.4) + activemodel (7.2.2) + activesupport (= 7.2.2) + activerecord (7.2.2) + activemodel (= 7.2.2) + activesupport (= 7.2.2) timeout (>= 0.4.0) - activestorage (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activesupport (= 7.1.3.4) + activestorage (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activesupport (= 7.2.2) marcel (~> 1.0) - activesupport (7.1.3.4) + activesupport (7.2.2) base64 + benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) afm (0.2.2) @@ -96,6 +93,7 @@ GEM nokogiri (~> 1, >= 1.10.8) base64 (0.2.0) bcrypt (3.1.20) + benchmark (0.4.0) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -111,6 +109,8 @@ GEM bindex (0.8.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) + bootsnap (1.18.4) + msgpack (~> 1.2) bootstrap (5.2.3) autoprefixer-rails (>= 9.1.0) popper_js (>= 2.11.6, < 3) @@ -120,7 +120,7 @@ GEM bugsnag (6.27.1) concurrent-ruby (~> 1.0) builder (3.3.0) - bullet (8.0.0) + bullet (7.2.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) capybara (3.40.0) @@ -143,7 +143,7 @@ GEM coderay (1.1.3) concurrent-ruby (1.3.4) connection_pool (2.4.1) - coverband (6.1.4) + coverband (6.1.2) redis (>= 3.0) crack (1.0.0) bigdecimal @@ -157,14 +157,14 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.4) + date (3.4.0) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) debug_inspector (1.2.0) - delayed_job (4.1.12) - activesupport (>= 3.0, < 8.0) - delayed_job_active_record (4.1.10) + delayed_job (4.1.13) + activesupport (>= 3.0, < 9.0) + delayed_job_active_record (4.1.8) activerecord (>= 3.0, < 8.0) delayed_job (>= 3.0, < 5) delayed_job_web (1.4.4) @@ -218,10 +218,10 @@ GEM smart_properties erubi (1.13.0) execjs (2.10.0) - factory_bot (6.4.5) + factory_bot (6.5.0) activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) railties (>= 5.0.0) faker (3.4.2) i18n (>= 1.8.11, < 2) @@ -259,11 +259,11 @@ GEM ffi (1.17.0-x86_64-darwin) ffi (1.17.0-x86_64-linux-gnu) filterrific (5.2.5) - flipper (1.3.2) + flipper (1.3.1) concurrent-ruby (< 2) - flipper-active_record (1.3.2) - activerecord (>= 4.2, < 9) - flipper (~> 1.3.2) + flipper-active_record (1.3.0) + activerecord (>= 4.2, < 8) + flipper (~> 1.3.0) flipper-ui (1.3.1) erubi (>= 1.0.0, < 2.0.0) flipper (~> 1.3.1) @@ -310,18 +310,18 @@ GEM image_processing (1.13.0) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - importmap-rails (2.0.1) + importmap-rails (2.0.3) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - io-console (0.7.2) - irb (1.14.1) + io-console (0.8.0) + irb (1.14.3) rdoc (>= 4.0.0) reline (>= 0.4.2) jbuilder (2.13.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) - json (2.7.5) + json (2.9.1) jwt (2.9.1) base64 kaminari (1.2.2) @@ -348,7 +348,7 @@ GEM listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.0) + logger (1.6.3) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) @@ -373,7 +373,7 @@ GEM method_source (1.1.0) mini_magick (4.13.2) mini_mime (1.1.5) - minitest (5.25.2) + minitest (5.25.4) monetize (1.12.0) money (~> 6.12) money (6.16.0) @@ -383,16 +383,16 @@ GEM monetize (~> 1.9) money (~> 6.13) railties (>= 3.0) + msgpack (1.7.5) multi_xml (0.7.1) bigdecimal (~> 3.1) multipart-post (2.4.1) mustermann (3.0.0) ruby2_keywords (~> 0.0.1) - mutex_m (0.3.0) nenv (0.3.0) net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.12) + net-imap (0.5.0) date net-protocol net-pop (0.1.2) @@ -401,13 +401,13 @@ GEM timeout net-smtp (0.5.0) net-protocol - newrelic_rpm (9.13.0) - nio4r (2.7.3) - nokogiri (1.16.7-arm64-darwin) + newrelic_rpm (9.16.0) + nio4r (2.7.4) + nokogiri (1.17.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-darwin) + nokogiri (1.17.2-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.7-x86_64-linux) + nokogiri (1.17.2-x86_64-linux) racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) @@ -438,11 +438,11 @@ GEM capybara (>= 1.1) rspec (>= 2.14) orm_adapter (0.5.0) - paper_trail (15.1.0) + paper_trail (15.2.0) activerecord (>= 6.1) request_store (~> 1.4) parallel (1.26.3) - parser (3.3.5.1) + parser (3.3.6.0) ast (~> 2.4.1) racc pdf-core (0.9.0) @@ -479,36 +479,37 @@ GEM pry-stack_explorer (0.6.1) binding_of_caller (~> 1.0) pry (~> 0.13) - psych (5.1.2) + psych (5.2.2) + date stringio public_suffix (6.0.1) - puma (6.4.3) + puma (6.5.0) nio4r (~> 2.0) racc (1.8.1) - rack (2.2.10) - rack-protection (3.1.0) - rack (~> 2.2, >= 2.2.4) - rack-session (1.0.2) - rack (< 3) + rack (3.1.8) + rack-protection (4.0.0) + base64 (>= 0.1.0) + rack (>= 3.0.0, < 4) + rack-session (2.0.0) + rack (>= 3.0.0) rack-test (2.1.0) rack (>= 1.3) - rackup (1.0.0) - rack (< 3) - webrick - rails (7.1.3.4) - actioncable (= 7.1.3.4) - actionmailbox (= 7.1.3.4) - actionmailer (= 7.1.3.4) - actionpack (= 7.1.3.4) - actiontext (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activemodel (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + rackup (2.2.0) + rack (>= 3) + rails (7.2.2) + actioncable (= 7.2.2) + actionmailbox (= 7.2.2) + actionmailer (= 7.2.2) + actionpack (= 7.2.2) + actiontext (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activemodel (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) bundler (>= 1.15.0) - railties (= 7.1.3.4) + railties (= 7.2.2) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -522,13 +523,13 @@ GEM activesupport (>= 4.2) choice (~> 0.2.0) ruby-graphviz (~> 1.2) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) - nokogiri (~> 1.14) - railties (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) - irb + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) @@ -538,17 +539,17 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - rdoc (6.7.0) + rdoc (6.10.0) psych (>= 4.0.0) recaptcha (5.17.0) redis (5.3.0) redis-client (>= 0.22.0) redis-client (0.22.2) connection_pool - regexp_parser (2.9.2) - reline (0.5.10) + regexp_parser (2.9.3) + reline (0.6.0) io-console (~> 0.5) - request_store (1.5.1) + request_store (1.7.0) rack (>= 1.4) responders (3.1.1) actionpack (>= 5.2) @@ -577,20 +578,19 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.1) - rubocop (1.65.1) + rubocop (1.69.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.36.2, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.33.0) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.37.0) parser (>= 3.3.1.0) - rubocop-performance (1.22.1) + rubocop-performance (1.23.0) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) rubocop-rails (2.25.1) @@ -619,6 +619,7 @@ GEM sprockets (> 3.0) sprockets-rails tilt + securerandom (0.4.1) shellany (0.0.1) shoulda-matchers (6.2.0) activesupport (>= 5.2.0) @@ -631,10 +632,11 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - sinatra (3.1.0) + sinatra (4.0.0) mustermann (~> 3.0) - rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.1.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.0.0) + rack-session (>= 2.0.0, < 3) tilt (~> 2.0) slop (3.6.0) smart_properties (1.17.0) @@ -648,36 +650,39 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - standard (1.40.0) + standard (1.43.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) - rubocop (~> 1.65.0) + rubocop (~> 1.69.1) standard-custom (~> 1.0.0) - standard-performance (~> 1.4) + standard-performance (~> 1.6) standard-custom (1.0.2) lint_roller (~> 1.0) rubocop (~> 1.50) - standard-performance (1.5.0) + standard-performance (1.6.0) lint_roller (~> 1.1) - rubocop-performance (~> 1.22.0) + rubocop-performance (~> 1.23.0) stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.1) + stringio (3.1.2) strong_migrations (1.8.0) activerecord (>= 5.2) terser (1.2.4) execjs (>= 0.3.0, < 3) thor (1.3.2) tilt (2.2.0) - timeout (0.4.2) + timeout (0.4.1) ttfunk (1.7.0) turbo-rails (2.0.10) actionpack (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.6.0) + unicode-display_width (3.1.2) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) uniform_notifier (1.16.0) + useragent (0.16.10) version_gem (1.1.4) warden (1.2.9) rack (>= 2.0.9) @@ -690,21 +695,20 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.8.2) + webrick (1.9.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) yard (0.9.34) - zeitwerk (2.6.18) + zeitwerk (2.7.1) PLATFORMS arm64-darwin-20 arm64-darwin-21 arm64-darwin-22 arm64-darwin-23 - arm64-darwin-24 x86_64-darwin-20 x86_64-darwin-21 x86_64-darwin-22 @@ -717,6 +721,7 @@ DEPENDENCIES azure-storage-blob better_errors binding_of_caller + bootsnap bootstrap (~> 5.2) brakeman bugsnag @@ -775,7 +780,7 @@ DEPENDENCIES pry-rails pry-remote puma - rails (= 7.1.3.4) + rails (= 7.2.2) rails-controller-testing rails-erd recaptcha @@ -799,4 +804,4 @@ DEPENDENCIES webmock (~> 3.24) BUNDLED WITH - 2.5.23 + 2.5.22 diff --git a/app/controllers/audits_controller.rb b/app/controllers/audits_controller.rb index 5cb0f22d8b..8d68291cee 100644 --- a/app/controllers/audits_controller.rb +++ b/app/controllers/audits_controller.rb @@ -10,7 +10,7 @@ def index end def show - @items = View::Inventory.items_for_location(@audit.storage_location) + @items = View::Inventory.items_for_location(@audit.storage_location, include_omitted: true) end def edit @@ -93,7 +93,7 @@ def set_storage_locations end def set_items - @items = current_organization.items.alphabetized + @items = current_organization.items.where(active: true).alphabetized end def save_audit_status_and_redirect(params) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index c1af5332b2..1bc266c6bd 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -4,9 +4,13 @@ class DashboardController < ApplicationController def index @org_stats = OrganizationStats.new(current_organization) - @total_inventory = current_organization.total_inventory @partners_awaiting_review = current_organization.partners.awaiting_review - @outstanding_requests = current_organization.ordered_requests.where(status: %i[pending started]).order(:created_at) + @outstanding_requests = current_organization + .ordered_requests + .includes(:partner_user, :partner) + .where(status: %i[pending started]) + .order(:created_at) + .limit(25) @low_inventory_report = LowInventoryQuery.call(current_organization) diff --git a/app/controllers/distributions_by_county_controller.rb b/app/controllers/distributions_by_county_controller.rb index 715dc7256d..d1ca1ee9e7 100644 --- a/app/controllers/distributions_by_county_controller.rb +++ b/app/controllers/distributions_by_county_controller.rb @@ -4,7 +4,12 @@ class DistributionsByCountyController < ApplicationController def report setup_date_range_picker - distributions = current_organization.distributions.includes(:partner).during(helpers.selected_range) - @breakdown = DistributionByCountyReportService.new.get_breakdown(distributions) + start_date = helpers.selected_range.first.iso8601 + end_date = helpers.selected_range.last.iso8601 + @breakdown = DistributionSummaryByCountyQuery.call( + organization_id: current_organization.id, + start_date: start_date, + end_date: end_date + ) end end diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb index 3ff9d70e32..ef0cc2e113 100644 --- a/app/controllers/distributions_controller.rb +++ b/app/controllers/distributions_controller.rb @@ -50,7 +50,7 @@ def index @items = current_organization.items.alphabetized.select(:id, :name) @item_categories = current_organization.item_categories.select(:id, :name) @storage_locations = current_organization.storage_locations.active_locations.alphabetized.select(:id, :name) - @partners = Partner.joins(:distributions).where(distributions: @distributions).distinct.order(:name).select(:id, :name) + @partners = current_organization.partners.active.alphabetized.select(:id, :name) @selected_item = filter_params[:by_item_id].presence @distribution_totals = DistributionTotalsService.new(current_organization.distributions, scope_filters) @total_value_all_distributions = @distribution_totals.total_value @@ -117,7 +117,7 @@ def create elsif request_id @distribution.initialize_request_items end - @items = current_organization.items.alphabetized + @items = current_organization.items.active.alphabetized @partner_list = current_organization.partners.where.not(status: 'deactivated').alphabetized inventory = View::Inventory.new(@distribution.organization_id) @@ -152,7 +152,7 @@ def new @distribution.line_items.build @distribution.copy_from_donation(params[:donation_id], params[:storage_location_id]) end - @items = current_organization.items.alphabetized + @items = current_organization.items.active.alphabetized @partner_list = current_organization.partners.where.not(status: 'deactivated').alphabetized inventory = View::Inventory.new(current_organization.id) @@ -178,7 +178,7 @@ def edit if (!@distribution.complete? && @distribution.future?) || current_user.has_role?(Role::ORG_ADMIN, current_organization) @distribution.line_items.build if @distribution.line_items.size.zero? - @items = current_organization.items.alphabetized + @items = current_organization.items.active.alphabetized @partner_list = current_organization.partners.alphabetized @audit_warning = current_organization.audits .where(storage_location_id: @distribution.storage_location_id) @@ -209,7 +209,8 @@ def update flash[:error] = insufficient_error_message(result.error.message) @distribution.line_items.build if @distribution.line_items.size.zero? @distribution.initialize_request_items - @items = current_organization.items.alphabetized + @items = current_organization.items.active.alphabetized + @partner_list = current_organization.partners.alphabetized @storage_locations = current_organization.storage_locations.active_locations.alphabetized render :edit end diff --git a/app/controllers/partners/family_requests_controller.rb b/app/controllers/partners/family_requests_controller.rb index 6f4f94b34c..f1ba5f0f3c 100644 --- a/app/controllers/partners/family_requests_controller.rb +++ b/app/controllers/partners/family_requests_controller.rb @@ -19,7 +19,7 @@ def create create_service = Partners::FamilyRequestCreateService.new( partner_user_id: current_user.id, family_requests_attributes: family_requests_attributes, - for_families: true + request_type: "child" ) create_service.call @@ -37,7 +37,7 @@ def validate @partner_request = Partners::FamilyRequestCreateService.new( partner_user_id: current_user.id, family_requests_attributes: family_requests_attributes, - for_families: true + request_type: "child" ).initialize_only if @partner_request.valid? @total_items = @partner_request.total_items diff --git a/app/controllers/partners/individuals_requests_controller.rb b/app/controllers/partners/individuals_requests_controller.rb index 9432bdc16f..bd85254e04 100644 --- a/app/controllers/partners/individuals_requests_controller.rb +++ b/app/controllers/partners/individuals_requests_controller.rb @@ -12,7 +12,8 @@ def create create_service = Partners::FamilyRequestCreateService.new( partner_user_id: current_user.id, comments: individuals_request_params[:comments], - family_requests_attributes: individuals_request_params[:items_attributes]&.values + family_requests_attributes: individuals_request_params[:items_attributes]&.values, + request_type: "individual" ) create_service.call @@ -36,7 +37,8 @@ def validate @partner_request = Partners::FamilyRequestCreateService.new( partner_user_id: current_user.id, comments: individuals_request_params[:comments], - family_requests_attributes: individuals_request_params[:items_attributes]&.values + family_requests_attributes: individuals_request_params[:items_attributes]&.values, + request_type: "individual" ).initialize_only if @partner_request.valid? @total_items = @partner_request.total_items diff --git a/app/models/account_request.rb b/app/models/account_request.rb index c68fe75559..f28194996c 100644 --- a/app/models/account_request.rb +++ b/app/models/account_request.rb @@ -30,7 +30,7 @@ class AccountRequest < ApplicationRecord has_one :organization, dependent: :nullify - enum status: %w[started user_confirmed admin_approved rejected admin_closed].map { |v| [v, v] }.to_h + enum :status, %w[started user_confirmed admin_approved rejected admin_closed].map { |v| [v, v] }.to_h scope :requested, -> { where(status: %w[started user_confirmed]) } scope :closed, -> { where(status: %w[admin_approved rejected admin_closed]) } diff --git a/app/models/barcode_item.rb b/app/models/barcode_item.rb index 9e9141322c..cb05987b43 100644 --- a/app/models/barcode_item.rb +++ b/app/models/barcode_item.rb @@ -49,8 +49,11 @@ class BarcodeItem < ApplicationRecord scope :global, -> { where(barcodeable_type: "BaseItem") } - alias_attribute :item, :barcodeable - alias_attribute :base_item, :barcodeable + # aliases of barcodeable + belongs_to :item, polymorphic: true, dependent: :destroy, + counter_cache: :barcode_count, foreign_key: :barcodeable_id, foreign_type: :barcodeable_type + belongs_to :base_item, polymorphic: true, dependent: :destroy, + counter_cache: :barcode_count, foreign_key: :barcodeable_id, foreign_type: :barcodeable_type def to_h { diff --git a/app/models/broadcast_announcement.rb b/app/models/broadcast_announcement.rb index e1818f572f..af74b21d60 100644 --- a/app/models/broadcast_announcement.rb +++ b/app/models/broadcast_announcement.rb @@ -25,7 +25,7 @@ def expired? def self.filter_announcements(parent_org) BroadcastAnnouncement.where(organization_id: parent_org) - .where("expiry IS ? or expiry >= ?", nil, Time.zone.today) + .where("expiry IS NULL or expiry >= ?", Time.zone.today) .order(created_at: :desc) end end diff --git a/app/models/concerns/issued_at.rb b/app/models/concerns/issued_at.rb index 3abfa79c53..b28261b96f 100644 --- a/app/models/concerns/issued_at.rb +++ b/app/models/concerns/issued_at.rb @@ -5,23 +5,18 @@ module IssuedAt extend ActiveSupport::Concern included do - before_create :initialize_issued_at - before_save :initialize_issued_at scope :by_issued_at, ->(issued_at) { where(issued_at: issued_at.beginning_of_month..issued_at.end_of_month) } scope :for_year, ->(year) { where("extract(year from issued_at) = ?", year) } + validates :issued_at, presence: true validate :issued_at_cannot_be_before_2000 validate :issued_at_cannot_be_further_than_1_year end private - def initialize_issued_at - self.issued_at ||= created_at&.end_of_day - end - def issued_at_cannot_be_before_2000 if issued_at.present? && issued_at < Date.new(2000, 1, 1) - errors.add(:issued_at, "Cannot be before 2000") + errors.add(:issued_at, "cannot be before 2000") end end diff --git a/app/models/distribution.rb b/app/models/distribution.rb index 1d11c25d7b..1f7d10c857 100644 --- a/app/models/distribution.rb +++ b/app/models/distribution.rb @@ -44,9 +44,11 @@ class Distribution < ApplicationRecord before_save :combine_distribution, :reset_shipping_cost - enum state: { scheduled: 5, complete: 10 } - enum delivery_method: { pick_up: 0, delivery: 1, shipped: 2 } + enum :state, { scheduled: 5, complete: 10 } + enum :delivery_method, { pick_up: 0, delivery: 1, shipped: 2 } scope :active, -> { joins(:line_items).joins(:items).where(items: { active: true }) } + scope :with_diapers, -> { joins(line_items: :item).merge(Item.disposable.or(Item.cloth_diapers)) } + scope :with_period_supplies, -> { joins(line_items: :item).merge(Item.period_supplies) } # add item_id scope to allow filtering distributions by item scope :by_item_id, ->(item_id) { includes(:items).where(items: { id: item_id }) } # partner scope to allow filtering by partner @@ -71,6 +73,10 @@ class Distribution < ApplicationRecord where("issued_at > :start_date AND issued_at <= :end_date", start_date: Time.zone.today.beginning_of_week.beginning_of_day, end_date: Time.zone.today.end_of_week.end_of_day) end + scope :in_last_12_months, -> do + where("issued_at > :start_date AND issued_at <= :end_date", + start_date: 12.months.ago.beginning_of_day, end_date: Time.zone.today.end_of_day) + end delegate :name, to: :partner, prefix: true diff --git a/app/models/item.rb b/app/models/item.rb index 8f6b1eaec1..688bd34512 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -143,7 +143,7 @@ def is_in_kit?(kits = nil) end def can_delete?(inventory = nil, kits = nil) - can_deactivate_or_delete?(inventory, kits) && line_items.none? && !barcode_count&.positive? && !in_request? + can_deactivate_or_delete?(inventory, kits) && line_items.none? && !barcode_count&.positive? && !in_request? && kit.blank? end # @return [Boolean] diff --git a/app/models/partner.rb b/app/models/partner.rb index 2e7b5ab77b..858d01a67e 100644 --- a/app/models/partner.rb +++ b/app/models/partner.rb @@ -27,7 +27,7 @@ class Partner < ApplicationRecord "application/vnd.openxmlformats-officedocument.wordprocessingml.document" ].freeze - enum status: { uninvited: 0, invited: 1, awaiting_review: 2, approved: 3, error: 4, recertification_required: 5, deactivated: 6 } + enum :status, { uninvited: 0, invited: 1, awaiting_review: 2, approved: 3, error: 4, recertification_required: 5, deactivated: 6 } belongs_to :organization belongs_to :partner_group, optional: true @@ -74,6 +74,7 @@ class Partner < ApplicationRecord AGENCY_TYPES = { "CAREER" => "Career technical training", "ABUSE" => "Child abuse resource center", + "BNB" => "Basic Needs Bank", "CHURCH" => "Church outreach ministry", "COLLEGE" => "College and Universities", "CDC" => "Community development corporation", @@ -82,6 +83,7 @@ class Partner < ApplicationRecord "LEGAL" => "Correctional Facilities / Jail / Prison / Legal System", "CRISIS" => "Crisis/Disaster services", "DISAB" => "Developmental disabilities program", + "DISTRICT" => "School District", "DOMV" => "Domestic violence shelter", "ECE" => "Early Childhood Education/Childcare", "CHILD" => "Early childhood services", @@ -96,6 +98,7 @@ class Partner < ApplicationRecord "HOSP" => "Hospital", "INFPAN" => "Infant/Child Pantry/Closet", "LIB" => "Library", + "MHEALTH" => "Mental Health", "MILITARY" => "Military Bases/Veteran Services", "POLICE" => "Police Station", "PREG" => "Pregnancy resource center", @@ -185,7 +188,10 @@ def self.csv_export_headers "Contact Name", "Contact Phone", "Contact Email", - "Notes" + "Notes", + "Counties Served", + "Providing Diapers", + "Providing Period Supplies" ] end @@ -202,10 +208,21 @@ def csv_export_attributes contact_person[:name], contact_person[:phone], contact_person[:email], - notes + notes, + profile.county_list_by_region, + providing_diapers, + providing_period_supplies ] end + def providing_diapers + distributions.in_last_12_months.with_diapers.any? ? "Y" : "N" + end + + def providing_period_supplies + distributions.in_last_12_months.with_period_supplies.any? ? "Y" : "N" + end + def contact_person return @contact_person if @contact_person diff --git a/app/models/partners/profile.rb b/app/models/partners/profile.rb index 3fd9fcb397..271ec146b5 100644 --- a/app/models/partners/profile.rb +++ b/app/models/partners/profile.rb @@ -91,6 +91,7 @@ class Profile < Base has_many :served_areas, foreign_key: "partner_profile_id", class_name: "Partners::ServedArea", dependent: :destroy, inverse_of: :partner_profile + has_many :counties, through: :served_areas accepts_nested_attributes_for :served_areas, allow_destroy: true has_many_attached :documents @@ -125,6 +126,11 @@ def split_pick_up_emails pick_up_email.split(/,|\s+/).compact_blank end + def county_list_by_region + # provides a county list in case insensitive alpha order, by region, then county name + counties.order(%w(lower(region) lower(name))).pluck(:name).join("; ") + end + private def check_social_media diff --git a/app/models/request.rb b/app/models/request.rb index 0c2647ccc1..7e78fb0133 100644 --- a/app/models/request.rb +++ b/app/models/request.rb @@ -31,8 +31,8 @@ class Request < ApplicationRecord accepts_nested_attributes_for :item_requests, allow_destroy: true, reject_if: proc { |attributes| attributes["quantity"].blank? } has_many :child_item_requests, through: :item_requests - enum status: { pending: 0, started: 1, fulfilled: 2, discarded: 3 }, _prefix: true - enum request_type: %w[quantity individual child].map { |v| [v, v] }.to_h + enum :status, { pending: 0, started: 1, fulfilled: 2, discarded: 3 }, prefix: true + enum :request_type, %w[quantity individual child].map { |v| [v, v] }.to_h validates :distribution_id, uniqueness: true, allow_nil: true validate :item_requests_uniqueness_by_item_id diff --git a/app/models/transfer.rb b/app/models/transfer.rb index 4237f5fef0..49b2248f2a 100644 --- a/app/models/transfer.rb +++ b/app/models/transfer.rb @@ -20,7 +20,8 @@ class Transfer < ApplicationRecord include Itemizable include Filterable include Exportable - alias_attribute :storage_location, :from # to make it play nice with Itemizable + # to make it play nice with Itemizable - alias of `from` + belongs_to :storage_location, class_name: "StorageLocation", inverse_of: :transfers_from, foreign_key: :from_id scope :from_location, ->(location_id) { where(from_id: location_id) } scope :to_location, ->(location_id) { where(to_id: location_id) } scope :for_csv_export, ->(organization, *) { diff --git a/app/pdfs/distribution_pdf.rb b/app/pdfs/distribution_pdf.rb index 5942348593..896d5b48d5 100644 --- a/app/pdfs/distribution_pdf.rb +++ b/app/pdfs/distribution_pdf.rb @@ -162,8 +162,6 @@ def request_data "Value/item", "In-Kind Value Received", "Packages"]] - - inventory = nil inventory = View::Inventory.new(@distribution.organization_id) request_items = @distribution.request.request_items.map do |request_item| RequestItem.from_json(request_item, @distribution.request, inventory) diff --git a/app/pdfs/donation_pdf.rb b/app/pdfs/donation_pdf.rb index 773e1b9095..bbf2a19eb0 100644 --- a/app/pdfs/donation_pdf.rb +++ b/app/pdfs/donation_pdf.rb @@ -20,9 +20,13 @@ def initialize(donation) @address = nil @email = nil when Donation::SOURCES[:product_drive] - @name = donation.product_drive_participant.business_name - @address = donation.product_drive_participant.address - @email = donation.product_drive_participant.email + if donation.product_drive_participant + @name = donation.product_drive_participant.business_name + @address = donation.product_drive_participant.address + @email = donation.product_drive_participant.email + else + @name = "Product Drive -- #{donation.product_drive.name}" + end when Donation::SOURCES[:misc] @name = "Misc. Donation" @address = nil diff --git a/app/queries/distribution_summary_by_county_query.rb b/app/queries/distribution_summary_by_county_query.rb new file mode 100644 index 0000000000..afe9dc157d --- /dev/null +++ b/app/queries/distribution_summary_by_county_query.rb @@ -0,0 +1,90 @@ +class DistributionSummaryByCountyQuery + CountySummary = Data.define(:name, :quantity, :value) + + # No need to send comments in the query + 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 + ( + SELECT DISTINCT d.id, + d.partner_id, + COALESCE(SUM(li.quantity) OVER (PARTITION BY d.id), 0) AS quantity, + COALESCE(SUM(COALESCE(i.value_in_cents, 0) * li.quantity) OVER (PARTITION BY d.id), 0) AS value + FROM distributions d + JOIN line_items li ON li.itemizable_id = d.id AND li.itemizable_type = 'Distribution' + 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 + GROUP BY d.id, li.id, i.id + ), + /* Match 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 + ( + SELECT dt.id, + dt.quantity, + dt.value, + 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 + 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 + LEFT JOIN counties c ON c.id = psa.county_id + UNION + /* Previous behavior was to add a row for unspecified counties + even if all distributions have an associated county */ + SELECT 0 AS id, + 0 AS quantity, + 0 AS value, + 1 AS percentage, + 'Unspecified' AS county_name, + 'ZZZ' AS county_region + ) + /* Distribution value and quantities per county share may not be whole numbers, + so we cast to an integer for rounding purposes */ + SELECT tbc.county_name AS name, + SUM((tbc.quantity * percentage)::int) AS quantity, + SUM((tbc.value * percentage)::int) AS value + FROM totals_by_county tbc + GROUP BY county_name, county_region + ORDER BY county_region ASC; + SQL + + class << self + def call(organization_id:, start_date: nil, end_date: nil) + params = { + organization_id: organization_id, + start_date: start_date || "1000-01-01", + end_date: end_date || "3000-01-01" + } + + execute(to_sql(DISTRIBUTION_BY_COUNTY_SQL, **params)).to_a.map(&to_county_summary) + end + + private + + def execute(sql) + ActiveRecord::Base.connection.execute(sql) + end + + def to_sql(query, organization_id:, start_date:, end_date:) + ActiveRecord::Base.sanitize_sql_array( + [ + query, + organization_id: organization_id, + start_date: start_date, + end_date: end_date + ] + ) + end + + def to_county_summary + ->(params) { CountySummary.new(**params) } + end + end +end diff --git a/app/services/distribution_by_county_report_service.rb b/app/services/distribution_by_county_report_service.rb deleted file mode 100644 index c20d263923..0000000000 --- a/app/services/distribution_by_county_report_service.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -class DistributionByCountyReportService - Breakdown = Struct.new(:name, :region, :num_items, :amount) - def get_breakdown(distributions) - breakdowns = {} - breakdowns["Unspecified"] = Breakdown.new("Unspecified", "ZZZ", 0, 0.00) - distributions.each do |distribution| - served_areas = distribution.partner.profile.served_areas - num_items_for_distribution = distribution.line_items.total - value_of_distribution = distribution.line_items.total_value - if served_areas.size == 0 - breakdowns["Unspecified"].num_items += num_items_for_distribution - breakdowns["Unspecified"].amount += value_of_distribution - else - served_areas.each do |served_area| - name = served_area.county.name - percentage = served_area.client_share / 100.0 - if !breakdowns[name] - breakdowns[name] = Breakdown.new(name, served_area.county.region, - (num_items_for_distribution * percentage).round(0), value_of_distribution * percentage) - else - breakdowns[name].num_items = breakdowns[name].num_items + (num_items_for_distribution * percentage).round(0) - breakdowns[name].amount = breakdowns[name].amount + value_of_distribution * percentage - end - end - end - end - - breakdown_array = breakdowns.sort_by { |k, v| [v.region, k] } - @breakdown = breakdown_array.map { |a| a[1] } - end -end diff --git a/app/services/partners/family_request_create_service.rb b/app/services/partners/family_request_create_service.rb index 8e018530db..c84da76a8a 100644 --- a/app/services/partners/family_request_create_service.rb +++ b/app/services/partners/family_request_create_service.rb @@ -6,13 +6,13 @@ module Partners class FamilyRequestCreateService include ServiceObjectErrorsMixin - attr_reader :partner_user_id, :comments, :family_requests_attributes, :partner_request + attr_reader :partner_user_id, :comments, :family_requests_attributes, :partner_request, :request_type - def initialize(partner_user_id:, family_requests_attributes:, comments: nil, for_families: false) + def initialize(partner_user_id:, family_requests_attributes:, request_type:, comments: nil) @partner_user_id = partner_user_id @comments = comments @family_requests_attributes = family_requests_attributes.presence || [] - @for_families = for_families + @request_type = request_type end def call @@ -81,9 +81,5 @@ def convert_person_count_to_item_quantity(item_id:, person_count:) def included_items_by_id @included_items_by_id ||= Item.where(id: family_requests_attributes.pluck(:item_id)).index_by(&:id) end - - def request_type - @for_families ? "child" : "individual" - end end end diff --git a/app/views/audits/_form.html.erb b/app/views/audits/_form.html.erb index d9014cae5f..8cb798a588 100644 --- a/app/views/audits/_form.html.erb +++ b/app/views/audits/_form.html.erb @@ -14,7 +14,7 @@
<%= simple_form_for @audit, data: { controller: "form-input" }, html: {class: "storage-location-required"} do |f| %> - <%= render partial: "storage_locations/source", object: f, locals: { label: "Storage location", error: "What storage location are you auditing?" } %> + <%= render partial: "storage_locations/source", object: f, locals: { label: "Storage location", error: "What storage location are you auditing?", include_omitted_items: true } %>
Items in this audit
diff --git a/app/views/dashboard/_outstanding_requests.html.erb b/app/views/dashboard/_outstanding_requests.html.erb index 3ea3f23e01..b80180e293 100644 --- a/app/views/dashboard/_outstanding_requests.html.erb +++ b/app/views/dashboard/_outstanding_requests.html.erb @@ -8,7 +8,7 @@ footer_options: { class: "text-center" }, ) do %> - <% if @outstanding_requests.empty? %> + <% if @outstanding_requests.length.zero? %> No outstanding requests! <% else %> @@ -21,12 +21,12 @@ - <% @outstanding_requests.take(25).each do |item| %> + <% @outstanding_requests.each do |request| %> - - - - + + + + <% end %> diff --git a/app/views/distributions/edit.html.erb b/app/views/distributions/edit.html.erb index a25558c548..843036a66a 100644 --- a/app/views/distributions/edit.html.erb +++ b/app/views/distributions/edit.html.erb @@ -24,7 +24,7 @@
- <% unless @distribution.future? %> + <% if @distribution.issued_at && !@distribution.future? %>
- - + + <% end %> diff --git a/app/views/donations/_donation_form.html.erb b/app/views/donations/_donation_form.html.erb index fd0a0ba6e3..977ef7319c 100644 --- a/app/views/donations/_donation_form.html.erb +++ b/app/views/donations/_donation_form.html.erb @@ -39,7 +39,7 @@
<%= f.association :product_drive_participant, collection: @product_drive_participants, - selected: default_location(@donation), + selected: donation_form.product_drive_participant_id, include_blank: true, label_method: lambda { |x| "#{x.try(:business_name) }" }, label: "Product Drive Participant", @@ -67,7 +67,7 @@ collection: @storage_locations, label: "Storage Location", error: "Where is it being stored?", - selected: donation_form.storage_location&.id || current_organization.intake_location, + selected: donation_form.storage_location&.id || default_location(@donation), include_blank: true, wrapper: :input_group %>
diff --git a/app/views/purchases/index.html.erb b/app/views/purchases/index.html.erb index cdbda040b8..42f2d482f6 100644 --- a/app/views/purchases/index.html.erb +++ b/app/views/purchases/index.html.erb @@ -89,7 +89,7 @@ - <%= render partial: "purchase_row", collection: @purchases %> + <%= render partial: "purchase_row", collection: @paginated_purchases %> diff --git a/bin/lint b/bin/lint index fa2178887a..daed34265e 100755 --- a/bin/lint +++ b/bin/lint @@ -1,4 +1,16 @@ #!/bin/bash -bundle exec rubocop -A -bundle exec erblint --lint-all -a +# Note: Any arguments passed in will be passed along to rubocop or erb_lint +# +# Example to only lint Rails models: +# $ bin/lint app/models +# +echo "Running rubocop" +bundle exec rubocop -A $@ + +echo "Running erb_lint" +if [ $# -eq 0 ]; then # If no arguments are supplied check all files + bundle exec erb_lint --lint-all -a +else # otherwise pass those arguments along (likely a specific file or folder) + bundle exec erb_lint $@ -a +fi diff --git a/config/boot.rb b/config/boot.rb index d2ebcbca6b..988a5ddc46 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,4 @@ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. -# require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/environments/development.rb b/config/environments/development.rb index 535f87276b..bb2c0ac011 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -47,7 +47,7 @@ config.action_mailer.perform_caching = false - config.action_mailer.preview_path = "#{Rails.root}/lib/previews" + config.action_mailer.preview_paths << "#{Rails.root}/lib/previews" # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb index ae9a1e508e..cbd5682c0d 100644 --- a/config/initializers/kaminari_config.rb +++ b/config/initializers/kaminari_config.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true Kaminari.configure do |config| - config.default_per_page = 50 + if Rails.env.development? || Rails.env.staging? + config.default_per_page = 5 + else + config.default_per_page = 50 + end # config.max_per_page = nil # config.window = 4 # config.outer_window = 0 diff --git a/config/initializers/money.rb b/config/initializers/money.rb index edf7bce635..13e1e8911b 100644 --- a/config/initializers/money.rb +++ b/config/initializers/money.rb @@ -1,3 +1,12 @@ +# This MoneyRails::ActionViewExtension module should get loaded +# automatically when ActionView is loaded, but *very* rarely when +# setting up the app locally ItemHelper is loaded beforehand, +# which depends on MoneyRails::ActionViewExtension, so without +# this line an uninitialized constant error is raised. +# +# See: https://github.com/RubyMoney/money-rails/issues/614 +require "money-rails/helpers/action_view_extension" + MoneyRails.configure do |config| # set the default currency config.default_currency = :usd diff --git a/config/locales/en.yml b/config/locales/en.yml index 752819b154..a2232adb78 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -29,4 +29,12 @@ # To learn more, please read the Rails Internationalization guide # available at http://guides.rubyonrails.org/i18n.html. -en: \ No newline at end of file +en: + activerecord: + attributes: + donation: + issued_at: "Issue date" + purchase: + issued_at: "Purchase date" + distribution: + issued_at: "Distribution date and time" diff --git a/db/migrate/20241122201255_cleanup_partner_agency_types.rb b/db/migrate/20241122201255_cleanup_partner_agency_types.rb new file mode 100644 index 0000000000..e362c97694 --- /dev/null +++ b/db/migrate/20241122201255_cleanup_partner_agency_types.rb @@ -0,0 +1,109 @@ +class CleanupPartnerAgencyTypes < ActiveRecord::Migration[7.1] + # based on veesus' work in PR 4261 + + def up + # Some agency types have trailing spaces -- need tp get rid of them first + + update 'UPDATE partner_profiles SET agency_type = TRIM(agency_type)' + + + mapping = [['Vocational training program','CAREER'], + ['Church','CHURCH'], + ['Church','CHURCH'], + ['Church ministry','CHURCH'], + ['Crisis/Disaster services','CRISIS'], + ['Domestic Violence/Non-Profit','DOMV'], + ['Child Care Center','ECE'], + ['Education','EDU'], + ['Educational','EDU'], + ['Non-Profit - Education','EDU'], + ['public elementary school','ES'], + ['Church food pantry','FOOD'], + ['Food bank','FOOD'], + ['Food pantry','FOOD'], + ['Food Pantry','FOOD'], + ['Food Pantry - Emergency Diaper Distribution','FOOD'], + ['Foster Care','FOSTER'], + ['Government Agency/Affiliate','GOVT'], + ['Community health program','HEALTH'], + ['Home visits','HOMEVISIT'], + ['Public School','HS'], + ['Infant/Child Pantry/Closet','INFPAN'], + ['ASO','HEALTH'], + ['Case Management','OUTREACH'], + ['Child Advocacy Center','CHILD'], + ['Child Placing Agency','FOSTER'], + ['Community Ministry','CHURCH'], + ['Community outreach services','OUTREACH'], + ['Community Service Organization','OUTREACH'], + ['Diaper agency','BNB'], + ['Diaper Bank','BNB'], + ['Emergency Crisis/Human Trafficking/Refuee Service','REF'], + ['Human Services','OUTREACH'], + ['Maternity Home','PREG'], + ['Mental Health','MHEALTH'], + ['Non-Profit Victim Services and Family Resource Center','FAMILY'], + ['Non-Profit. Clothing, Food and Diaper Assistance','INFPAN'], + ['Nonprofit/School','EDU'], + ['Perinatal Mental Health Peer Support','MHEALTH'], + ['Primary Care Doctor','HEALTH'],['Public Health','HEALTH'], + ['Reproductive Health Care, Maternity Services','PREG'], + ['Safety Net Organization','OUTREACH'], + ['School','District'], + ['School District','District'], + ['School District','District'], + ['Women Substance Abuse','TREAT'], + ['Law Enforcement Agency','POLICE'], + ['Pregnancy Center - Charitable Health','PREG'], + ['Nonprofit preschool','PRESCH'], + ['Child abuse resource center','ABUSE'], + ['Career technical training','CAREER'], + ['Community development corporation','CDC'], + ['Early childhood services','CHILD'], + ['Church outreach ministry','CHURCH'], + ['Developmental disabilities program','DISAB'], + ['Domestic violence shelter','DOMV'], + ['Education program','EDU'], + ['School - Elementary School','ES'], + ['Family resource center','FAMILY'], + ['Food bank/pantry','FOOD'], + ['Head Start/Early Head Start','HEADSTART'], + ['Community health program or clinic','HEALTH'], + ['Homeless resource center','HOMELESS'], + ['Hospital','HOSP'], + ['Hospital','HOSP'], + ['School - High School','HS'], + ['Correctional Facilities / Jail / Prison / Legal System','LEGAL'], + ['School - Middle School','MS'], + ['Pregnancy resource center','PREG'], + ['Pregnancy Resource Center','PREG'], + ['Refugee resource center','REF'], + ['Treatment clinic','TREAT'], + ['Women, Infants and Children','WIC']] + + mapping.each do |type_pair| + profiles = Partners::Profile.where(agency_type: type_pair[0]) + profiles.each do |profile| + profile.agency_type = Partner::AGENCY_TYPES[type_pair[1]] + profile.save! + end + + end + + + profiles = Partners::Profile + .where.not(agency_type: Partner::AGENCY_TYPES.values) + .in_batches + + profiles.each_record do |profile| + profile.other_agency_type = profile.agency_type + profile.agency_type = Partner::AGENCY_TYPES['OTHER'] + profile.save! + end + + end + + def down + # Irreversible data migration + end +end diff --git a/db/migrate/20241220020009_data_fix_for_mhm_and_aad.rb b/db/migrate/20241220020009_data_fix_for_mhm_and_aad.rb new file mode 100644 index 0000000000..41838dc640 --- /dev/null +++ b/db/migrate/20241220020009_data_fix_for_mhm_and_aad.rb @@ -0,0 +1,29 @@ +class DataFixForMhmAndAad < ActiveRecord::Migration[7.2] + def up + # This is a one-time, one-way data fix for MHM and AAD. See support ticket starting Sept 9, 2024, 1327 + + return unless Rails.env.production? + + # This request should be marked fulfilled and associated with distribution 74466. We have checked that no request is already associated with that distribution + + request = Request.find("39408") + request.distribution_id = "74466" + request.status = 2 + request.save! + + + ## these addoitinal 3 requests have been identified as having the status 'started', when they should be 'fulfilled' + # They already have distributions associated with them + + ["42063", "37900"].each do |request_id| + request = Request.find(request_id) + request.status = 2 + request.save! + end + + + end + def down + # treating this as irreversible. Though I suppose technically we could. + end +end diff --git a/db/seeds.rb b/db/seeds.rb index efb93dc358..27b35ce2e8 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -37,32 +37,62 @@ def random_record_for_org(org, klass) # ---------------------------------------------------------------------------- pdx_org = Organization.find_or_create_by!(short_name: "diaper_bank") do |organization| - organization.name = "Pawnee Diaper Bank" - organization.street = "P.O. Box 22613" - organization.city = "Pawnee" - organization.state = "Indiana" + organization.name = "Pawnee Diaper Bank" + organization.street = "P.O. Box 22613" + organization.city = "Pawnee" + organization.state = "Indiana" organization.zipcode = "12345" - organization.email = "info@pawneediaper.org" + organization.email = "info@pawneediaper.org" end Organization.seed_items(pdx_org) sf_org = Organization.find_or_create_by!(short_name: "sf_bank") do |organization| - organization.name = "SF Diaper Bank" - organization.street = "P.O. Box 12345" - organization.city = "San Francisco" - organization.state = "CA" + organization.name = "SF Diaper Bank" + organization.street = "P.O. Box 12345" + organization.city = "San Francisco" + organization.state = "CA" organization.zipcode = "90210" - organization.email = "info@sfdiaperbank.org" + organization.email = "info@sfdiaperbank.org" end Organization.seed_items(sf_org) +sc_org = Organization.find_or_create_by!(short_name: "sc_bank") do |organization| + organization.name = "Second City Essentials Bank" + organization.street = Faker::Address.street_address + organization.city = Faker::Address.city + organization.state = Faker::Address.state_abbr + organization.zipcode = Faker::Address.zip_code + organization.email = "info@scdiaperbank.org" +end +Organization.seed_items(sc_org) + +# The list of organizations that will have donations, purchases, requests, distributions, +# and the records those rely on generated. +complete_orgs = [pdx_org, sc_org] + # At least one of the items is marked as inactive -Organization.all.each do |org| +Organization.all.find_each do |org| org.items.order(created_at: :desc).last.update(active: false) end +def seed_random_item_with_name(organization, name) + base_items = BaseItem.all.map(&:to_h) + base_item = Array.wrap(base_items).sample + base_item[:name] = name + organization.seed_items(base_item) +end + +# Add a couple unique items based on random base items named after the sc_bank +# so it will be clear if they are showing up where they aren't supposed to be +4.times do |index| + seed_random_item_with_name(sc_org, "Second City Item ##{index + 1}") +end + +# Keep a list of these unique items so its easy to use them for later records +sc_org_unique_items = sc_org.items.where("name ilike ?", "%Second City Item #%") + # Assign a value to some organization items to verify totals are working -Organization.all.each do |org| +Organization.all.find_each do |org| org.items.where(value_in_cents: 0).limit(10).each do |item| item.update(value_in_cents: 100) end @@ -72,18 +102,20 @@ def random_record_for_org(org, klass) # Request Units # ---------------------------------------------------------------------------- -%w(pack box flat).each do |name| - Unit.create!(organization: pdx_org, name: name) -end +complete_orgs.each do |org| + %w[pack box flat].each do |name| + Unit.create!(organization: org, name: name) + end -pdx_org.items.each_with_index do |item, i| - if item.name == 'Pads' - %w(box pack).each { |name| item.request_units.create!(name: name) } - elsif item.name == 'Wipes (Baby)' - item.request_units.create!(name: 'pack') - elsif item.name == 'Kids Pull-Ups (5T-6T)' - %w(pack flat).each do |name| - item.request_units.create!(name: name) + org.items.each_with_index do |item, i| + if item.name == "Pads" + %w[box pack].each { |name| item.request_units.create!(name: name) } + elsif item.name == "Wipes (Baby)" + item.request_units.create!(name: "pack") + elsif item.name == "Kids Pull-Ups (5T-6T)" + %w[pack flat].each do |name| + item.request_units.create!(name: name) + end end end end @@ -92,8 +124,8 @@ def random_record_for_org(org, klass) # Item Categories # ---------------------------------------------------------------------------- -Organization.all.each do |org| - ['Diapers', 'Period Supplies', 'Adult Incontinence'].each do |letter| +Organization.all.find_each do |org| + ["Diapers", "Period Supplies", "Adult Incontinence"].each do |letter| FactoryBot.create(:item_category, organization: org, name: "Category #{letter}") end end @@ -102,19 +134,19 @@ def random_record_for_org(org, klass) # Item < - > ItemCategory # ---------------------------------------------------------------------------- -Organization.all.each do |org| +Organization.all.find_each do |org| # Added `nil` to randomly choose to not categorize items sometimes via sample item_category_ids = org.item_categories.map(&:id) + [nil] org.items.each do |item| - item.update_column(:item_category_id, item_category_ids.sample) + item.update(item_category_id: item_category_ids.sample) end end # ---------------------------------------------------------------------------- # Partner Group & Item Categories # ---------------------------------------------------------------------------- -Organization.all.each do |org| +Organization.all.find_each do |org| # Setup the Partner Group & their item categories partner_group_one = FactoryBot.create(:partner_group, organization: org) @@ -136,18 +168,20 @@ def random_record_for_org(org, klass) # ---------------------------------------------------------------------------- [ - { email: 'superadmin@example.com', organization_admin: false, super_admin: true }, - { email: 'org_admin1@example.com', organization_admin: true, organization: pdx_org }, - { email: 'org_admin2@example.com', organization_admin: true, organization: sf_org }, - { email: 'user_1@example.com', organization_admin: false, organization: pdx_org }, - { email: 'user_2@example.com', organization_admin: false, organization: sf_org }, - { email: 'test@example.com', organization_admin: false, organization: pdx_org, super_admin: true }, - { email: 'test2@example.com', organization_admin: true, organization: pdx_org } + {email: "superadmin@example.com", organization_admin: false, super_admin: true}, + {email: "org_admin1@example.com", organization_admin: true, organization: pdx_org}, + {email: "org_admin2@example.com", organization_admin: true, organization: sf_org}, + {email: "second_city_admin@example.com", organization_admin: true, organization: sc_org}, + {email: "user_1@example.com", organization_admin: false, organization: pdx_org}, + {email: "user_2@example.com", organization_admin: false, organization: sf_org}, + {email: "second_city_user@example.com", organization_admin: false, organization: sc_org}, + {email: "test@example.com", organization_admin: false, organization: pdx_org, super_admin: true}, + {email: "test2@example.com", organization_admin: true, organization: pdx_org} ].each do |user_data| user = User.create( email: user_data[:email], - password: 'password!', - password_confirmation: 'password!' + password: "password!", + password_confirmation: "password!" ) if user_data[:organization] @@ -167,15 +201,17 @@ def random_record_for_org(org, klass) # Donation Sites # ---------------------------------------------------------------------------- -[ - { name: "Pawnee Hardware", address: "1234 SE Some Ave., Pawnee, OR 12345" }, - { name: "Parks Department", address: "2345 NE Some St., Pawnee, OR 12345" }, - { name: "Waffle House", address: "3456 Some Bay., Pawnee, OR 12345" }, - { name: "Eagleton Country Club", address: "4567 Some Blvd., Eagleton, OR 12345" } -].each do |donation_option| - DonationSite.find_or_create_by!(name: donation_option[:name]) do |donation| - donation.address = donation_option[:address] - donation.organization = pdx_org +complete_orgs.each do |org| + [ + {name: "#{org.city} Hardware", address: "1234 SE Some Ave., #{org.city}, #{org.state} 12345"}, + {name: "#{org.city} Parks Department", address: "2345 NE Some St., #{org.city}, #{org.state} 12345"}, + {name: "Waffle House", address: "3456 Some Bay., #{org.city}, #{org.state} 12345"}, + {name: "Eagleton Country Club", address: "4567 Some Blvd., Eagleton, #{org.state} 12345"} + ].each do |donation_option| + DonationSite.find_or_create_by!(address: donation_option[:address]) do |donation| + donation.name = donation_option[:name] + donation.organization = org + end end end @@ -224,39 +260,51 @@ def random_record_for_org(org, klass) status: :approved, email: "approved_2@example.com", notes: note.sample + }, + { + name: "Second City Senior Center", + email: "second_city_senior_center@example.com", + status: :approved, + quota: 500, + notes: note.sample, + organization: sc_org } ].each do |partner_option| p = Partner.find_or_create_by!(partner_option) do |partner| - partner.organization = pdx_org + partner.organization = if partner_option.key?(:organization) + partner_option[:organization] + else + pdx_org + end - if partner_option[:name] == "Second Street Community Outreach" - partner.partner_group = pdx_org.partner_groups.find_by(name: 'Group 2') + partner.partner_group = if partner_option[:name] == "Second Street Community Outreach" + pdx_org.partner_groups.find_by(name: "Group 2") else - partner.partner_group = pdx_org.partner_groups.first + partner.organization.partner_groups.first end end - profile = Partners::Profile.create!({ - essentials_bank_id: p.organization_id, - partner_id: p.id, - address1: Faker::Address.street_address, - address2: "", - city: Faker::Address.city, - state: Faker::Address.state_abbr, - zip_code: Faker::Address.zip, - website: Faker::Internet.domain_name, - zips_served: Faker::Address.zip, - executive_director_name: Faker::Name.name, - executive_director_email: p.email, - executive_director_phone: Faker::PhoneNumber.phone_number, - primary_contact_name: Faker::Name.name, - primary_contact_email: Faker::Internet.email, - primary_contact_phone: Faker::PhoneNumber.phone_number, - primary_contact_mobile: Faker::PhoneNumber.phone_number, - pick_up_name: Faker::Name.name, - pick_up_email: Faker::Internet.email, - pick_up_phone: Faker::PhoneNumber.phone_number - }) + Partners::Profile.create!({ + essentials_bank_id: p.organization_id, + partner_id: p.id, + address1: Faker::Address.street_address, + address2: "", + city: Faker::Address.city, + state: Faker::Address.state_abbr, + zip_code: Faker::Address.zip, + website: Faker::Internet.domain_name, + zips_served: Faker::Address.zip, + executive_director_name: Faker::Name.name, + executive_director_email: p.email, + executive_director_phone: Faker::PhoneNumber.phone_number, + primary_contact_name: Faker::Name.name, + primary_contact_email: Faker::Internet.email, + primary_contact_phone: Faker::PhoneNumber.phone_number, + primary_contact_mobile: Faker::PhoneNumber.phone_number, + pick_up_name: Faker::Name.name, + pick_up_email: Faker::Internet.email, + pick_up_phone: Faker::PhoneNumber.phone_number + }) user = ::User.create!( name: Faker::Name.name, @@ -284,7 +332,7 @@ def random_record_for_org(org, klass) # Skip creating records that they would have created after # they've accepted the invitation # - next if p.status == 'uninvited' + next if p.status == "uninvited" families = (1..Faker::Number.within(range: 4..13)).to_a.map do Partners::Family.create!( @@ -359,7 +407,7 @@ def random_record_for_org(org, klass) dates_generator = DispersedPastDatesGenerator.new - Faker::Number.within(range: 32..56).times do + Faker::Number.within(range: 32..56).times do |index| date = dates_generator.next partner_request = ::Request.new( @@ -371,7 +419,7 @@ def random_record_for_org(org, klass) updated_at: date ) - pads = p.organization.items.find_by(name: 'Pads') + pads = p.organization.items.find_by(name: "Pads") new_item_request = Partners::ItemRequest.new( item_id: pads.id, quantity: Faker::Number.within(range: 10..30), @@ -380,7 +428,7 @@ def random_record_for_org(org, klass) partner_key: pads.partner_key, created_at: date, updated_at: date, - request_unit: 'pack' + request_unit: "pack" ) partner_request.item_requests << new_item_request @@ -405,6 +453,24 @@ def random_record_for_org(org, klass) } end + # Guarantee that there is a request for the items unique to the Second City Bank + if (p.organization == sc_org) && (index < 4) + unique_item = sc_org_unique_items[index] + # Make sure we don't violate item request uniqueness if the unique_item was + # randomly selected already + if !partner_request.item_requests.any? { |item_request| item_request.item_id == unique_item.id } + partner_request.item_requests << Partners::ItemRequest.new( + item_id: unique_item.id, + quantity: Faker::Number.within(range: 10..30), + children: [], + name: unique_item.name, + partner_key: unique_item.partner_key, + created_at: date, + updated_at: date + ) + end + end + partner_request.save! end end @@ -425,6 +491,18 @@ def random_record_for_org(org, klass) inventory.warehouse_type = StorageLocation::WAREHOUSE_TYPES[1] inventory.square_footage = 20_000 end +StorageLocation.find_or_create_by!(name: "Second City Bulk Storage") do |inventory| + inventory.address = "#{Faker::Address.street_address}, #{sc_org.city}, #{sc_org.state} #{sc_org.zipcode}" + inventory.organization = sc_org + inventory.warehouse_type = StorageLocation::WAREHOUSE_TYPES[0] + inventory.square_footage = 10_000 +end +StorageLocation.find_or_create_by!(name: "Second City Main Bank (Office)") do |inventory| + inventory.address = "#{Faker::Address.street_address}, #{sc_org.city}, #{sc_org.state} #{sc_org.zipcode}" + inventory.organization = sc_org + inventory.warehouse_type = StorageLocation::WAREHOUSE_TYPES[1] + inventory.square_footage = 20_000 +end inactive_storage = StorageLocation.find_or_create_by!(name: "Inactive Storage Location") do |inventory| inventory.address = "Unknown" @@ -447,79 +525,81 @@ def random_record_for_org(org, klass) ) end end -Organization.all.each { |org| SnapshotEvent.publish(org) } - -# Set minimum and recomended inventory levels for items at the Pawnee Diaper Bank Organization -half_items_count = (pdx_org.items.count/2).to_i -low_items = pdx_org.items.left_joins(:inventory_items) - .select('items.*, SUM(inventory_items.quantity) AS total_quantity') - .group('items.id') - .order('total_quantity') - .limit(half_items_count) - -min_qty = low_items.first.total_quantity -max_qty = low_items.last.total_quantity +Organization.all.find_each { |org| SnapshotEvent.publish(org) } + +# Set minimum and recomended inventory levels for the complete organizations +# Only set inventory levels for the half of each org's items with the lowest stock +complete_orgs.each do |org| + half_items_count = (org.items.count / 2).to_i + low_items = org.items.left_joins(:inventory_items) + .select("items.*, SUM(inventory_items.quantity) AS total_quantity") + .group("items.id") + .order("total_quantity") + .limit(half_items_count).to_a + + min_qty = low_items.first.total_quantity + max_qty = low_items.last.total_quantity + + # Ensure at least one of the items unqiue to the Second City Bank has minimum + # and recommended quantities set + if (org == sc_org) && !(low_items & sc_org_unique_items).any? + low_items << sc_org_unique_items.last + end -low_items.each do |item| - min_value = rand((min_qty / 10).floor..(max_qty/10).ceil) * 10 - recomended_value = rand((min_value/10).ceil..1000) * 10 - item.update(on_hand_minimum_quantity: min_value, on_hand_recommended_quantity: recomended_value) + low_items.each do |item| + min_value = rand((min_qty / 10).floor..(max_qty / 10).ceil) * 10 + recomended_value = rand((min_value / 10).ceil..1000) * 10 + item.update(on_hand_minimum_quantity: min_value, on_hand_recommended_quantity: recomended_value) + end end -# ---------------------------------------------------------------------------- -# Product Drives -# ---------------------------------------------------------------------------- - -[ - { - name: 'Pamper the Poopsies', - start_date: Time.current, - organization: pdx_org - } -].each { |drive| ProductDrive.create! drive } - -# ---------------------------------------------------------------------------- -# Product Drive Participants -# ---------------------------------------------------------------------------- - -[ - { business_name: "A Good Place to Collect Diapers", - contact_name: "fred", - email: "good@place.is", - organization: pdx_org }, - { business_name: "A Mediocre Place to Collect Diapers", - contact_name: "wilma", - email: "ok@place.is", - organization: pdx_org } -].each { |participant| ProductDriveParticipant.create! participant } - -# ---------------------------------------------------------------------------- -# Product Drives -# ---------------------------------------------------------------------------- - -[ - { name: "First Product Drive", - start_date: 3.years.ago, - end_date: 3.years.ago, - organization: sf_org }, - { name: "Best Product Drive", - start_date: 3.weeks.ago, - end_date: 2.weeks.ago, - organization: sf_org }, - { name: "Second Best Product Drive", - start_date: 2.weeks.ago, - end_date: 1.week.ago, - organization: pdx_org } -].each { |product_drive| ProductDrive.find_or_create_by! product_drive } - -# ---------------------------------------------------------------------------- -# Manufacturers -# ---------------------------------------------------------------------------- - -[ - { name: "Manufacturer 1", organization: pdx_org }, - { name: "Manufacturer 2", organization: pdx_org } -].each { |manu| Manufacturer.find_or_create_by! manu } +# Reload, since some of the items in sc_org_unique_items will have been altered +sc_org_unique_items.reload + +complete_orgs.each do |org| + # ---------------------------------------------------------------------------- + # Product Drives + # ---------------------------------------------------------------------------- + + [ + {name: "First Product Drive", + start_date: 3.years.ago, + end_date: 3.years.ago, + organization: org}, + {name: "Best Product Drive", + start_date: 3.weeks.ago, + end_date: 2.weeks.ago, + organization: org}, + {name: "Second Best Product Drive", + start_date: 2.weeks.ago, + end_date: 1.week.ago, + organization: org} + ].each { |product_drive| ProductDrive.find_or_create_by! product_drive } + + # ---------------------------------------------------------------------------- + # Product Drive Participants + # ---------------------------------------------------------------------------- + + [ + {business_name: "A Good Place to Collect Diapers", + contact_name: "fred", + email: "good@place.is", + organization: org}, + {business_name: "A Mediocre Place to Collect Diapers", + contact_name: "wilma", + email: "ok@place.is", + organization: org} + ].each { |participant| ProductDriveParticipant.create! participant } + + # ---------------------------------------------------------------------------- + # Manufacturers + # ---------------------------------------------------------------------------- + + [ + {name: "Manufacturer 1", organization: org}, + {name: "Manufacturer 2", organization: org} + ].each { |manu| Manufacturer.find_or_create_by! manu } +end # ---------------------------------------------------------------------------- # Line Items @@ -541,8 +621,8 @@ def seed_quantity(item_name, organization, storage_location, quantity) JSON.parse(File.read(Rails.root.join("db", "base_items.json"))).each do |_category, entries| entries.each do |entry| - seed_quantity(entry['name'], pdx_org, inv_arbor, entry['qty']['arbor']) - seed_quantity(entry['name'], pdx_org, inv_pdxdb, entry['qty']['pdxdb']) + seed_quantity(entry["name"], pdx_org, inv_arbor, entry["qty"]["arbor"]) + seed_quantity(entry["name"], pdx_org, inv_pdxdb, entry["qty"]["pdxdb"]) end end @@ -551,19 +631,19 @@ def seed_quantity(item_name, organization, storage_location, quantity) # ---------------------------------------------------------------------------- [ - { value: "10037867880046", name: "Kids (Size 5)", quantity: 108 }, - { value: "10037867880053", name: "Kids (Size 6)", quantity: 92 }, - { value: "10037867880039", name: "Kids (Size 4)", quantity: 124 }, - { value: "803516626364", name: "Kids (Size 1)", quantity: 40 }, - { value: "036000406535", name: "Kids (Size 1)", quantity: 44 }, - { value: "037000863427", name: "Kids (Size 1)", quantity: 35 }, - { value: "041260379000", name: "Kids (Size 3)", quantity: 160 }, - { value: "074887711700", name: "Wipes (Baby)", quantity: 8 }, - { value: "036000451306", name: "Kids Pull-Ups (4T-5T)", quantity: 56 }, - { value: "037000862246", name: "Kids (Size 4)", quantity: 92 }, - { value: "041260370236", name: "Kids (Size 4)", quantity: 68 }, - { value: "036000407679", name: "Kids (Size 4)", quantity: 24 }, - { value: "311917152226", name: "Kids (Size 4)", quantity: 82 }, + {value: "10037867880046", name: "Kids (Size 5)", quantity: 108}, + {value: "10037867880053", name: "Kids (Size 6)", quantity: 92}, + {value: "10037867880039", name: "Kids (Size 4)", quantity: 124}, + {value: "803516626364", name: "Kids (Size 1)", quantity: 40}, + {value: "036000406535", name: "Kids (Size 1)", quantity: 44}, + {value: "037000863427", name: "Kids (Size 1)", quantity: 35}, + {value: "041260379000", name: "Kids (Size 3)", quantity: 160}, + {value: "074887711700", name: "Wipes (Baby)", quantity: 8}, + {value: "036000451306", name: "Kids Pull-Ups (4T-5T)", quantity: 56}, + {value: "037000862246", name: "Kids (Size 4)", quantity: 92}, + {value: "041260370236", name: "Kids (Size 4)", quantity: 68}, + {value: "036000407679", name: "Kids (Size 4)", quantity: 24}, + {value: "311917152226", name: "Kids (Size 4)", quantity: 82} ].each do |item| BarcodeItem.find_or_create_by!(value: item[:value]) do |barcode| barcode.item = pdx_org.items.find_by(name: item[:name]) @@ -572,68 +652,90 @@ def seed_quantity(item_name, organization, storage_location, quantity) end end -# ---------------------------------------------------------------------------- -# Donations -# ---------------------------------------------------------------------------- - dates_generator = DispersedPastDatesGenerator.new -# Make some donations of all sorts -20.times.each do - source = Donation::SOURCES.values.sample - # Depending on which source it uses, additional data may need to be provided. - donation = Donation.new(source: source, - storage_location: StorageLocation.active_locations.sample, - organization: pdx_org, - issued_at: dates_generator.next) - case source - when Donation::SOURCES[:product_drive] - donation.product_drive = ProductDrive.first - donation.product_drive_participant = random_record_for_org(pdx_org, ProductDriveParticipant) - when Donation::SOURCES[:donation_site] - donation.donation_site = random_record_for_org(pdx_org, DonationSite) - when Donation::SOURCES[:manufacturer] - donation.manufacturer = random_record_for_org(pdx_org, Manufacturer) - end +complete_orgs.each do |org| + # ---------------------------------------------------------------------------- + # Donations + # ---------------------------------------------------------------------------- + + # Make some donations of all sorts + 20.times.each do |index| + source = Donation::SOURCES.values.sample + # Depending on which source it uses, additional data may need to be provided. + donation = Donation.new( + source: source, + storage_location: org.storage_locations.active_locations.sample, + organization: org, + issued_at: dates_generator.next + ) + case source + when Donation::SOURCES[:product_drive] + donation.product_drive = org.product_drives.find_by(name: "Best Product Drive") + donation.product_drive_participant = random_record_for_org(org, ProductDriveParticipant) + when Donation::SOURCES[:donation_site] + donation.donation_site = random_record_for_org(org, DonationSite) + when Donation::SOURCES[:manufacturer] + donation.manufacturer = random_record_for_org(org, Manufacturer) + end - rand(1..5).times.each do - donation.line_items.push(LineItem.new(quantity: rand(250..500), item: random_record_for_org(pdx_org, Item))) - end - DonationCreateService.call(donation) -end + rand(1..5).times.each do + donation.line_items.push(LineItem.new(quantity: rand(250..500), item: random_record_for_org(org, Item))) + end -# ---------------------------------------------------------------------------- -# Distributions -# ---------------------------------------------------------------------------- -dates_generator = DispersedPastDatesGenerator.new + # Guarantee that there are at least a few donations for the items unique to the Second City Bank + if (org == sc_org) && (index < 4) + donation.line_items.push(LineItem.new(quantity: rand(250..500), item: sc_org_unique_items[index])) + end -inventory = InventoryAggregate.inventory_for(pdx_org.id) -# Make some distributions, but don't use up all the inventory -20.times.each do - issued_at = dates_generator.next + DonationCreateService.call(donation) + end - storage_location = StorageLocation.active_locations.sample - stored_inventory_items_sample = inventory.storage_locations[storage_location.id].items.values.sample(20) - delivery_method = Distribution.delivery_methods.keys.sample - shipping_cost = delivery_method == "shipped" ? (rand(20.0..100.0)).round(2).to_s : nil - distribution = Distribution.new( - storage_location: storage_location, - partner: random_record_for_org(pdx_org, Partner), - organization: pdx_org, - issued_at: issued_at, - created_at: 3.days.ago(issued_at), - delivery_method: delivery_method, - shipping_cost: shipping_cost, - comment: 'Urgent' - ) + # ---------------------------------------------------------------------------- + # Distributions + # ---------------------------------------------------------------------------- + + inventory = InventoryAggregate.inventory_for(org.id) + # Make some distributions, but don't use up all the inventory + 20.times.each do |index| + issued_at = dates_generator.next + + storage_location = org.storage_locations.active_locations.sample + stored_inventory_items_sample = inventory.storage_locations[storage_location.id].items.values.sample(20) + delivery_method = Distribution.delivery_methods.keys.sample + shipping_cost = (delivery_method == "shipped") ? rand(20.0..100.0).round(2).to_s : nil + distribution = Distribution.new( + storage_location: storage_location, + partner: random_record_for_org(org, Partner), + organization: org, + issued_at: issued_at, + created_at: 3.days.ago(issued_at), + delivery_method: delivery_method, + shipping_cost: shipping_cost, + comment: "Urgent" + ) - stored_inventory_items_sample.each do |stored_inventory_item| - distribution_qty = rand(stored_inventory_item.quantity / 2) - if distribution_qty >= 1 - distribution.line_items.push(LineItem.new(quantity: distribution_qty, - item_id: stored_inventory_item.item_id)) + stored_inventory_items_sample.each do |stored_inventory_item| + distribution_qty = rand(stored_inventory_item.quantity / 2) + if distribution_qty >= 1 + distribution.line_items.push(LineItem.new(quantity: distribution_qty, + item_id: stored_inventory_item.item_id)) + end end + + # Guarantee that there are at least a few distributions for the items unique to the Second City Bank + if (org == sc_org) && (index < 4) + unique_item_id = sc_org_unique_items[index].id + distribution_qty = rand(storage_location.item_total(unique_item_id) / 2) + distribution.line_items.push( + LineItem.new( + quantity: distribution_qty, + item_id: unique_item_id + ) + ) + end + + DistributionCreateService.new(distribution).call end - DistributionCreateService.new(distribution).call end # ---------------------------------------------------------------------------- @@ -641,18 +743,18 @@ def seed_quantity(item_name, organization, storage_location, quantity) # ---------------------------------------------------------------------------- BroadcastAnnouncement.create( - user: User.find_by(email: 'superadmin@example.com'), + user: User.find_by(email: "superadmin@example.com"), message: "This is the staging /demo server. There may be new features here! Stay tuned!", link: "https://example.com", - expiry: Date.today + 7.days, + expiry: Time.zone.today + 7.days, organization: nil ) BroadcastAnnouncement.create( - user: User.find_by(email: 'org_admin1@example.com'), + user: User.find_by(email: "org_admin1@example.com"), message: "This is the staging /demo server. There may be new features here! Stay tuned!", link: "https://example.com", - expiry: Date.today + 10.days, + expiry: Time.zone.today + 10.days, organization: pdx_org ) @@ -661,20 +763,22 @@ def seed_quantity(item_name, organization, storage_location, quantity) # ---------------------------------------------------------------------------- # Create some Vendors so Purchases can have vendor_ids -Vendor.create( - contact_name: Faker::FunnyName.two_word_name, - email: Faker::Internet.email, - phone: Faker::PhoneNumber.cell_phone, - comment: Faker::Lorem.paragraph(sentence_count: 2), - organization_id: pdx_org.id, - address: "#{Faker::Address.street_address} #{Faker::Address.city}, #{Faker::Address.state_abbr} #{Faker::Address.zip_code}", - business_name: Faker::Company.name, - latitude: rand(-90.000000000...90.000000000), - longitude: rand(-180.000000000...180.000000000), - created_at: (Time.zone.today - rand(15).days), - updated_at: (Time.zone.today - rand(15).days), -) -4.times do +complete_orgs.each do |org| + Vendor.create( + contact_name: Faker::FunnyName.two_word_name, + email: Faker::Internet.email, + phone: Faker::PhoneNumber.cell_phone, + comment: Faker::Lorem.paragraph(sentence_count: 2), + organization_id: org.id, + address: "#{Faker::Address.street_address} #{Faker::Address.city}, #{Faker::Address.state_abbr} #{Faker::Address.zip_code}", + business_name: Faker::Company.name, + latitude: rand(-90.000000000...90.000000000), + longitude: rand(-180.000000000...180.000000000), + created_at: (Time.zone.today - rand(15).days), + updated_at: (Time.zone.today - rand(15).days) + ) +end +3.times do Vendor.create( contact_name: Faker::FunnyName.two_word_name, email: Faker::Internet.email, @@ -686,7 +790,7 @@ def seed_quantity(item_name, organization, storage_location, quantity) latitude: rand(-90.000000000...90.000000000), longitude: rand(-180.000000000...180.000000000), created_at: (Time.zone.today - rand(15).days), - updated_at: (Time.zone.today - rand(15).days), + updated_at: (Time.zone.today - rand(15).days) ) end @@ -694,8 +798,8 @@ def seed_quantity(item_name, organization, storage_location, quantity) # Purchases # ---------------------------------------------------------------------------- -suppliers = %w(Target Wegmans Walmart Walgreens) -amount_items = %w(period_supplies diapers adult_incontinence other) +suppliers = %w[Target Wegmans Walmart Walgreens] +amount_items = %w[period_supplies diapers adult_incontinence other] comments = [ "Maecenas ante lectus, vestibulum pellentesque arcu sed, eleifend lacinia elit. Cras accumsan varius nisl, a commodo ligula consequat nec. Aliquam tincidunt diam id placerat rutrum.", "Integer a molestie tortor. Duis pretium urna eget congue porta. Fusce aliquet dolor quis viverra volutpat.", @@ -704,32 +808,47 @@ def seed_quantity(item_name, organization, storage_location, quantity) dates_generator = DispersedPastDatesGenerator.new -25.times do - purchase_date = dates_generator.next - storage_location = StorageLocation.active_locations.sample - vendor = random_record_for_org(pdx_org, Vendor) - purchase = Purchase.new( - purchased_from: suppliers.sample, - comment: comments.sample, - organization_id: pdx_org.id, - storage_location_id: storage_location.id, - issued_at: purchase_date, - created_at: purchase_date, - updated_at: purchase_date, - vendor_id: vendor.id, - amount_spent_on_period_supplies_cents: rand(0..5_000), - amount_spent_on_diapers_cents: rand(0..5_000), - amount_spent_on_adult_incontinence_cents: rand(0..5_000), - amount_spent_on_other_cents: rand(0..5_000) - ) +complete_orgs.each do |org| + 25.times do |index| + purchase_date = dates_generator.next + storage_location = org.storage_locations.active_locations.sample + vendor = random_record_for_org(org, Vendor) + purchase = Purchase.new( + purchased_from: suppliers.sample, + comment: comments.sample, + organization_id: org.id, + storage_location_id: storage_location.id, + issued_at: purchase_date, + created_at: purchase_date, + updated_at: purchase_date, + vendor_id: vendor.id, + amount_spent_on_period_supplies_cents: rand(0..5_000), + amount_spent_on_diapers_cents: rand(0..5_000), + amount_spent_on_adult_incontinence_cents: rand(0..5_000), + amount_spent_on_other_cents: rand(0..5_000) + ) + + purchase.amount_spent_in_cents = amount_items.map { |i| purchase.send("amount_spent_on_#{i}_cents") }.sum + + rand(1..5).times do + purchase.line_items.push( + LineItem.new(quantity: rand(1..1000), + item_id: org.item_ids.sample) + ) + end - purchase.amount_spent_in_cents = amount_items.map{|i| purchase.send("amount_spent_on_#{i}_cents")}.sum + # Guarantee that there are at least a few purchases for the items unique to the Second City Bank + if (org == sc_org) && (index < 4) + purchase.line_items.push( + LineItem.new( + quantity: rand(1..1000), + item_id: sc_org_unique_items[index].id + ) + ) + end - rand(1..5).times do - purchase.line_items.push(LineItem.new(quantity: rand(1..1000), - item_id: pdx_org.item_ids.sample)) + PurchaseCreateService.call(purchase) end - PurchaseCreateService.call(purchase) end # ---------------------------------------------------------------------------- @@ -739,18 +858,19 @@ def seed_quantity(item_name, organization, storage_location, quantity) Flipper::Adapters::ActiveRecord::Feature.find_or_create_by(key: "new_logo") Flipper::Adapters::ActiveRecord::Feature.find_or_create_by(key: "read_events") Flipper.enable(:read_events) - +Flipper::Adapters::ActiveRecord::Feature.find_or_create_by(key: "partner_step_form") +Flipper.enable(:partner_step_form) # ---------------------------------------------------------------------------- # Account Requests # ---------------------------------------------------------------------------- # Add some Account Requests to fill up the account requests admin page -[{ organization_name: "Telluride Diaper Bank", website: "TDB.com", confirmed_at: nil }, - { organization_name: "Ouray Diaper Bank", website: "ODB.com", confirmed_at: nil }, - { organization_name: "Canon City Diaper Bank", website: "CCDB.com", confirmed_at: nil }, - { organization_name: "Golden Diaper Bank", website: "GDB.com", confirmed_at: (Time.zone.today - rand(15).days) }, - { organization_name: "Westminster Diaper Bank", website: "WDB.com", confirmed_at: (Time.zone.today - rand(15).days) }, - { organization_name: "Lakewood Diaper Bank", website: "LDB.com", confirmed_at: (Time.zone.today - rand(15).days) }].each do |account_request| +[{organization_name: "Telluride Diaper Bank", website: "TDB.com", confirmed_at: nil}, + {organization_name: "Ouray Diaper Bank", website: "ODB.com", confirmed_at: nil}, + {organization_name: "Canon City Diaper Bank", website: "CCDB.com", confirmed_at: nil}, + {organization_name: "Golden Diaper Bank", website: "GDB.com", confirmed_at: (Time.zone.today - rand(15).days)}, + {organization_name: "Westminster Diaper Bank", website: "WDB.com", confirmed_at: (Time.zone.today - rand(15).days)}, + {organization_name: "Lakewood Diaper Bank", website: "LDB.com", confirmed_at: (Time.zone.today - rand(15).days)}].each do |account_request| AccountRequest.create( name: Faker::Name.unique.name, email: Faker::Internet.unique.email, @@ -805,7 +925,7 @@ def seed_quantity(item_name, organization, storage_location, quantity) # ---------------------------------------------------------------------------- # Counties # ---------------------------------------------------------------------------- -Rake::Task['db:load_us_counties'].invoke +Rake::Task["db:load_us_counties"].invoke # ---------------------------------------------------------------------------- # Partner Counties @@ -822,16 +942,15 @@ def seed_quantity(item_name, organization, storage_location, quantity) profile = partner.profile num_counties_for_partner = Faker::Number.within(range: 1..10) remaining_percentage = 100 - share_ceiling = 100/num_counties_for_partner #arbitrary, so I can do the math easily + share_ceiling = 100 / num_counties_for_partner # arbitrary, so I can do the math easily county_index = 0 county_ids_for_this_partner = county_ids.sample(num_counties_for_partner) county_ids_for_this_partner.each do |county_id| - client_share = 0 - if county_index == num_counties_for_partner - 1 - client_share = remaining_percentage + client_share = if county_index == num_counties_for_partner - 1 + remaining_percentage else - client_share = Faker::Number.within(range:1..share_ceiling) + Faker::Number.within(range: 1..share_ceiling) end Partners::ServedArea.create( @@ -840,7 +959,7 @@ def seed_quantity(item_name, organization, storage_location, quantity) client_share: client_share ) county_index += 1 - remaining_percentage = remaining_percentage - client_share + remaining_percentage -= client_share end end @@ -869,9 +988,30 @@ def seed_quantity(item_name, organization, storage_location, quantity) # # Addresses and resolves issue #4689, which can be found in: # https://github.com/rubyforgood/human-essentials/issues/4689 -User.where(invitation_token: nil).each do |user| +User.where(invitation_token: nil).find_each do |user| user.update!( invitation_sent_at: Time.current, invitation_accepted_at: Time.current ) end + +# Guarantee that at least one of the items unique to the Second City Bank has an +# inventory less than the recommended quantity. +item_to_make_scarce = sc_org_unique_items.where("on_hand_recommended_quantity > ?", 0).first +sc_org.storage_locations.each do |location| + num_on_hand = location.item_total(item_to_make_scarce.id) + if num_on_hand > item_to_make_scarce.on_hand_recommended_quantity + num_to_remove = item_to_make_scarce.on_hand_recommended_quantity - 1 - num_on_hand + adjustment = sc_org.adjustments.create!( + comment: "Ensuring example of item below recommended inventory", + storage_location: location, + user: User.with_role(:org_admin, sc_org).first + ) + adjustment.line_items = [LineItem.new( + quantity: num_to_remove, + item: item_to_make_scarce, + itemizable: adjustment + )] + AdjustmentCreateService.new(adjustment).call + end +end diff --git a/spec/controllers/distributions_controller_spec.rb b/spec/controllers/distributions_controller_spec.rb index a04a165377..d79775da2a 100644 --- a/spec/controllers/distributions_controller_spec.rb +++ b/spec/controllers/distributions_controller_spec.rb @@ -18,6 +18,7 @@ organization_name: organization.id, distribution: { partner_id: partner.id, + issued_at: Date.yesterday, storage_location_id: first_storage_location.id, line_items_attributes: { @@ -42,6 +43,7 @@ distribution: { partner_id: partner.id, storage_location_id: second_storage_location.id, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: second_storage_location.items.first.id, quantity: 18 } @@ -66,6 +68,7 @@ distribution: { partner_id: partner.id, storage_location_id: storage_location.id, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: storage_location.items.first.id, quantity: 18 } @@ -91,6 +94,7 @@ distribution: { partner_id: partner.id, storage_location_id: storage_location.id, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: storage_location.items.first.id, quantity: 18 }, @@ -134,6 +138,7 @@ distribution: { partner_id: partner.id, storage_location_id: storage_location.id, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: item1.id, quantity: 18 }, @@ -172,6 +177,7 @@ distribution: { partner_id: partner.id, storage_location_id: storage_location.id, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: item1.id, quantity: 18 }, @@ -199,6 +205,7 @@ distribution: { partner_id: partner.id, storage_location_id: storage_location.id, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: storage_location.items.first.id, quantity: 0 } @@ -235,6 +242,7 @@ id: distribution.id, distribution: { storage_location_id: distribution.storage_location.id, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: item1.id, quantity: 18 }, @@ -309,6 +317,7 @@ id: distribution.id, distribution: { storage_location_id: distribution.storage_location.id, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: item1.id, quantity: 4 }, diff --git a/spec/controllers/donations_controller_spec.rb b/spec/controllers/donations_controller_spec.rb index 70f7bf3a27..073d8df399 100644 --- a/spec/controllers/donations_controller_spec.rb +++ b/spec/controllers/donations_controller_spec.rb @@ -34,6 +34,7 @@ donation: { storage_location_id: storage_location.id, donation_site_id: donation_site.id, source: "Donation Site", + issued_at: Date.yesterday, line_items: line_items } } expect(response).to redirect_to(donations_path) diff --git a/spec/factories/distributions.rb b/spec/factories/distributions.rb index 20fbac92e9..a6ac783d84 100644 --- a/spec/factories/distributions.rb +++ b/spec/factories/distributions.rb @@ -22,7 +22,7 @@ storage_location partner organization { Organization.try(:first) || create(:organization) } - issued_at { nil } + issued_at { Time.current } delivery_method { :pick_up } state { :scheduled } diff --git a/spec/factories/donations.rb b/spec/factories/donations.rb index 6534b28bf1..b68590c892 100644 --- a/spec/factories/donations.rb +++ b/spec/factories/donations.rb @@ -23,7 +23,7 @@ comment { "It's a fine day for diapers." } storage_location organization { Organization.try(:first) || create(:organization) } - issued_at { nil } + issued_at { Time.current } factory :manufacturer_donation do manufacturer diff --git a/spec/factories/partners.rb b/spec/factories/partners.rb index ba34f27bbe..35289fc2a8 100644 --- a/spec/factories/partners.rb +++ b/spec/factories/partners.rb @@ -24,22 +24,26 @@ send_reminders { true } organization_id { Organization.try(:first).try(:id) || create(:organization).id } + transient do + without_profile { false } + end + trait :approved do status { :approved } end trait :uninvited do status { :uninvited } - - transient do - without_profile { false } - end end trait :awaiting_review do status { :awaiting_review } end + trait :deactivated do + status { :deactivated } + end + after(:create) do |partner, evaluator| next if evaluator.try(:without_profile) diff --git a/spec/factories/purchases.rb b/spec/factories/purchases.rb index b10e9cbd2d..f6ea2a5ac8 100644 --- a/spec/factories/purchases.rb +++ b/spec/factories/purchases.rb @@ -24,7 +24,7 @@ purchased_from { "Google" } storage_location organization { Organization.try(:first) || create(:organization) } - issued_at { nil } + issued_at { Time.current } amount_spent_in_cents { 10_00 } vendor { Vendor.try(:first) || create(:vendor) } diff --git a/spec/lib/kaminari_config_spec.rb b/spec/lib/kaminari_config_spec.rb new file mode 100644 index 0000000000..e839b2d383 --- /dev/null +++ b/spec/lib/kaminari_config_spec.rb @@ -0,0 +1,48 @@ +require "rails_helper" + +RSpec.describe "Kaminari configuration" do + describe "default_per_page setting" do + after(:each) do + # Reset Kaminari configuration after each test + Kaminari.configure do |config| + config.default_per_page = 50 + end + end + + context "in development environment" do + before do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("development")) + # Reload the configuration file + load Rails.root.join("config/initializers/kaminari_config.rb") + end + + it "sets default_per_page to 5" do + expect(Kaminari.config.default_per_page).to eq(5) + end + end + + context "in staging environment" do + before do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("staging")) + # Reload the configuration file + load Rails.root.join("config/initializers/kaminari_config.rb") + end + + it "sets default_per_page to 5" do + expect(Kaminari.config.default_per_page).to eq(5) + end + end + + context "in production environment" do + before do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production")) + # Reload the configuration file + load Rails.root.join("config/initializers/kaminari_config.rb") + end + + it "sets default_per_page to 50" do + expect(Kaminari.config.default_per_page).to eq(50) + end + end + end +end diff --git a/spec/models/distribution_spec.rb b/spec/models/distribution_spec.rb index b03c485e16..b75ae51c9f 100644 --- a/spec/models/distribution_spec.rb +++ b/spec/models/distribution_spec.rb @@ -139,6 +139,34 @@ end end + describe "in_last_12_months >" do + context "when the current date is December 31, 2023" do + before do + travel_to Time.zone.local(2023, 12, 31) + end + + after do + travel_back + end + + it "includes distributions issued within the last 12 months" do + included_distribution = create(:distribution, organization: organization, issued_at: Time.zone.local(2023, 1, 1)) + excluded_distribution = create(:distribution, organization: organization, issued_at: Time.zone.local(2022, 12, 30)) + distributions = Distribution.in_last_12_months + expect(distributions).to include(included_distribution) + expect(distributions).not_to include(excluded_distribution) + end + + it "includes distributions up to the current date and excludes future ones" do + current_distribution = create(:distribution, organization: organization, issued_at: Time.zone.local(2023, 12, 31)) + future_distribution = create(:distribution, organization: organization, issued_at: Time.zone.local(2024, 1, 1)) + distributions = Distribution.in_last_12_months + expect(distributions).to include(current_distribution) + expect(distributions).not_to include(future_distribution) + end + end + end + describe "by_item_id >" do it "only returns distributions with given item id" do # create 2 items with unique ids @@ -176,20 +204,42 @@ expect(Distribution.by_location(location_1.id)).not_to include(dist2) end end - end - context "Callbacks >" do - it "initializes the issued_at field to default to midnight if it wasn't explicitly set" do - yesterday = 1.day.ago - today = Time.zone.today + describe "with_diapers >" do + let(:disposable_item) { create(:item, base_item: create(:base_item, category: "Diapers - Childrens")) } + let(:cloth_diaper_item) { create(:item, base_item: create(:base_item, category: "Diapers - Cloth (Kids)")) } + let(:non_diaper_item) { create(:item, base_item: create(:base_item, category: "Menstrual Supplies/Items")) } + + it "only includes distributions with disposable or cloth_diaper items" do + dist1 = create(:distribution, :with_items, item: disposable_item) + dist2 = create(:distribution, :with_items, item: cloth_diaper_item) + dist3 = create(:distribution, :with_items, item: non_diaper_item) + + distributions = Distribution.with_diapers + expect(distributions.count).to eq(2) + expect(distributions).to include(dist1) + expect(distributions).to include(dist2) + expect(distributions).not_to include(dist3) + end + end + + describe "with_period_supplies >" do + let(:period_supplies_item) { create(:item, base_item: create(:base_item, category: "Menstrual Supplies/Items")) } + let(:non_period_supplies_item) { create(:item, base_item: create(:base_item, category: "Diapers - Childrens")) } - distribution = create(:distribution, created_at: yesterday, issued_at: today) - expect(distribution.issued_at.to_date).to eq(today) + it "only includes distributions with period supplies items" do + dist1 = create(:distribution, :with_items, item: period_supplies_item) + dist2 = create(:distribution, :with_items, item: non_period_supplies_item) - distribution = create(:distribution, created_at: yesterday) - expect(distribution.issued_at).to eq(distribution.created_at.end_of_day) + distributions = Distribution.with_period_supplies + expect(distributions.count).to eq(1) + expect(distributions).to include(dist1) + expect(distributions).not_to include(dist2) + end end + end + context "Callbacks >" do context "#before_save" do context "#reset_shipping_cost" do context "when delivery_method is other then shipped" do diff --git a/spec/models/donation_spec.rb b/spec/models/donation_spec.rb index cbd4fec1a5..2e66edc10b 100644 --- a/spec/models/donation_spec.rb +++ b/spec/models/donation_spec.rb @@ -61,17 +61,6 @@ end context "Callbacks >" do - it "inititalizes the issued_at field to default to midnight if it wasn't explicitly set" do - yesterday = 1.day.ago - today = Time.zone.today - - donation = create(:donation, created_at: yesterday, issued_at: today) - expect(donation.issued_at.to_date).to eq(today) - - donation = create(:donation, created_at: yesterday) - expect(donation.issued_at).to eq(donation.created_at.end_of_day) - end - it "automatically combines duplicate line_item records when they're created" do donation = build(:donation) item = create(:item) diff --git a/spec/models/item_spec.rb b/spec/models/item_spec.rb index d726bed05e..5436ab0b0c 100644 --- a/spec/models/item_spec.rb +++ b/spec/models/item_spec.rb @@ -36,7 +36,6 @@ it { should validate_presence_of(:name) } it { should belong_to(:organization) } - it { should belong_to(:base_item).counter_cache(:item_count).with_primary_key(:partner_key).with_foreign_key(:partner_key).inverse_of(:items) } it { should validate_numericality_of(:distribution_quantity).is_greater_than(0) } it { should validate_numericality_of(:on_hand_minimum_quantity).is_greater_than_or_equal_to(0) } it { should validate_numericality_of(:on_hand_recommended_quantity).is_greater_than_or_equal_to(0) } @@ -481,4 +480,47 @@ describe "versioning" do it { is_expected.to be_versioned } end + + describe "kit items" do + context "with kit and regular items" do + let(:organization) { create(:organization) } + let(:kit) { create(:kit, organization: organization) } + let(:kit_item) { create(:item, kit: kit, organization: organization) } + let(:regular_item) { create(:item, organization: organization) } + + describe "#can_delete?" do + it "returns false for kit items" do + expect(kit_item.can_delete?).to be false + end + + it "returns true for regular items" do + expect(regular_item.can_delete?).to be true + end + end + + describe "#deactivate!" do + it "deactivates both the kit item and its associated kit" do + kit_item.deactivate! + expect(kit_item.reload.active).to be false + expect(kit.reload.active).to be false + end + + it "only deactivates regular items" do + regular_item.deactivate! + expect(regular_item.reload.active).to be false + end + end + + describe "#validate_destroy" do + it "prevents deletion of kit items" do + expect { kit_item.destroy! }.to raise_error(ActiveRecord::RecordNotDestroyed) + expect(kit_item.errors[:base]).to include("Cannot delete item - it has already been used!") + end + + it "allows deletion of regular items" do + expect { regular_item.destroy! }.not_to raise_error + end + end + end + end end diff --git a/spec/models/partner_spec.rb b/spec/models/partner_spec.rb index c632759004..a3e0a06060 100644 --- a/spec/models/partner_spec.rb +++ b/spec/models/partner_spec.rb @@ -299,6 +299,8 @@ let(:agency_type) { Partner::AGENCY_TYPES["OTHER"] } let(:other_agency_type) { "Another Agency Name" } let(:notes) { "Some notes" } + let(:providing_diapers) { {value: "N", index: 13} } + let(:providing_period_supplies) { {value: "N", index: 14} } before do partner.profile.update({ @@ -317,7 +319,16 @@ partner.update(notes: notes) end - it "should has the info in the columns order" do + it "should have the expected info in the columns order" do + county_1 = create(:county, name: "High County, Maine", region: "Maine") + county_2 = create(:county, name: "laRue County, Louisiana", region: "Louisiana") + county_3 = create(:county, name: "Ste. Anne County, Louisiana", region: "Louisiana") + create(:partners_served_area, partner_profile: partner.profile, county: county_1, client_share: 50) + create(:partners_served_area, partner_profile: partner.profile, county: county_2, client_share: 40) + create(:partners_served_area, partner_profile: partner.profile, county: county_3, client_share: 10) + partner.profile.reload # not sure if this is needed + # county ordering is a bit esoteric -- it is human alphabetical by county within region (region is state) + correctly_ordered_counties = "laRue County, Louisiana; Ste. Anne County, Louisiana; High County, Maine" expect(partner.csv_export_attributes).to eq([ partner.name, partner.email, @@ -330,9 +341,77 @@ contact_name, contact_phone, contact_email, - notes + notes, + correctly_ordered_counties, + providing_diapers[:value], + providing_period_supplies[:value] ]) end + + context "when partner has a distribution in the last 12 months" do + let(:distribution) { create(:distribution, partner: partner) } + + shared_examples "providing_diapers check" do |scope| + before do + providing_diapers[:value] = "Y" + + case scope + when :disposable + item = create(:item, base_item: create(:base_item, category: "Diapers - Childrens")) + when :cloth_diapers + item = create(:item, base_item: create(:base_item, category: "Diapers - Cloth (Kids)")) + end + + create(:line_item, item: item, itemizable: distribution) + end + + it "should have Y as providing_diapers" do + expect(partner.csv_export_attributes[providing_diapers[:index]]).to eq(providing_diapers[:value]) + end + end + + context "with a disposable item" do + include_examples "providing_diapers check", :disposable + end + + context "with a cloth diaper item" do + include_examples "providing_diapers check", :cloth_diapers + end + + context "with a period supplies item" do + before do + providing_period_supplies[:value] = "Y" + + item = create(:item, base_item: create(:base_item, category: "Menstrual Supplies/Items")) + create(:line_item, item: item, itemizable: distribution) + end + + it "should have Y as providing_period_supplies" do + expect(partner.csv_export_attributes[providing_period_supplies[:index]]).to eq(providing_period_supplies[:value]) + end + end + end + + context "when partner only has distribution older than a 12 months" do + let(:distribution) { create(:distribution, issued_at: (12.months.ago.beginning_of_day - 1.day), partner: partner) } + let(:disposable_diapers_item) { create(:item, base_item: create(:base_item, category: "Diapers - Childrens")) } + let(:cloth_diapers_item) { create(:item, base_item: create(:base_item, category: "Diapers - Cloth (Kids)")) } + let(:period_supplies_item) { create(:item, base_item: create(:base_item, category: "Menstrual Supplies/Items")) } + + before do + create(:line_item, item: disposable_diapers_item, itemizable: distribution) + create(:line_item, item: cloth_diapers_item, itemizable: distribution) + create(:line_item, item: period_supplies_item, itemizable: distribution) + end + + it "should have N as providing_diapers" do + expect(partner.csv_export_attributes[providing_diapers[:index]]).to eq(providing_diapers[:value]) + end + + it "should have N as providing_period_supplies" do + expect(partner.csv_export_attributes[providing_period_supplies[:index]]).to eq(providing_period_supplies[:value]) + end + end end describe '#quantity_year_to_date' do diff --git a/spec/models/partners/profile_spec.rb b/spec/models/partners/profile_spec.rb index 80a6cb0c05..f82cb7e976 100644 --- a/spec/models/partners/profile_spec.rb +++ b/spec/models/partners/profile_spec.rb @@ -307,6 +307,25 @@ end end + describe "county_list" do + it "provides a county list in human-alpha by county within region order" do + county_1 = create(:county, name: "High County, Maine", region: "Maine") + county_2 = create(:county, name: "laRue County, Louisiana", region: "Louisiana") + county_3 = create(:county, name: "Ste. Anne County, Louisiana", region: "Louisiana") + county_4 = create(:county, name: "Other County, Louisiana", region: "Louisiana") + profile = create(:partner_profile) + profile_2 = create(:partner_profile) + create(:partners_served_area, partner_profile: profile, county: county_1, client_share: 50) + create(:partners_served_area, partner_profile: profile, county: county_2, client_share: 40) + create(:partners_served_area, partner_profile: profile, county: county_3, client_share: 10) + create(:partners_served_area, partner_profile: profile_2, county: county_4) + profile.reload + profile_2.reload + ## This is human-alpha by county within region (i.e. state) + expect(profile.county_list_by_region).to eq("laRue County, Louisiana; Ste. Anne County, Louisiana; High County, Maine") + end + end + describe "versioning" do it { is_expected.to be_versioned } end diff --git a/spec/models/purchase_spec.rb b/spec/models/purchase_spec.rb index 8961142a68..655a5999e1 100644 --- a/spec/models/purchase_spec.rb +++ b/spec/models/purchase_spec.rb @@ -87,17 +87,6 @@ end context "Callbacks >" do - it "inititalizes the issued_at field to default to midnight if it wasn't explicitly set" do - yesterday = 1.day.ago - today = Time.zone.today - - purchase = create(:purchase, created_at: yesterday, issued_at: today) - expect(purchase.issued_at.to_date).to eq(today) - - purchase = create(:purchase, created_at: yesterday) - expect(purchase.issued_at).to eq(purchase.created_at.end_of_day) - end - it "automatically combines duplicate line_item records when they're created" do purchase = build(:purchase) item = create(:item) diff --git a/spec/pdfs/donation_pdf_spec.rb b/spec/pdfs/donation_pdf_spec.rb index 21a8ee2174..5ceac9d3eb 100644 --- a/spec/pdfs/donation_pdf_spec.rb +++ b/spec/pdfs/donation_pdf_spec.rb @@ -5,6 +5,17 @@ create(:donation, organization: organization, donation_site: donation_site, source: Donation::SOURCES[:donation_site], comment: "A donation comment") end + let(:product_drive) { create(:product_drive, name: "Second Best Product Drive") } + let(:product_drive_participant) { + create(:product_drive_participant, business_name: "A Good Place to Collect Diapers", address: "1500 Remount Road, Front Royal, VA 22630", email: "good@place.is") + } + let(:product_drive_donation) do + create(:donation, organization: organization, product_drive: product_drive, source: Donation::SOURCES[:product_drive], + product_drive_participant: product_drive_participant, comment: "A product drive donation") + end + let(:product_drive_donation_without_participant) do + create(:donation, organization: organization, product_drive: product_drive, source: Donation::SOURCES[:product_drive], comment: "A product drive donation without participant") + end 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) } @@ -65,4 +76,22 @@ expect(pdf_test.page(1).text).to include("Total Items Received") end end + + context "product drive donation" do + it "renders correctly" do + pdf = described_class.new(organization, product_drive_donation) + pdf_test = PDF::Reader.new(StringIO.new(pdf.compute_and_render)) + expect(pdf_test.page(1).text).to include("A Good Place to Collect Diapers") + expect(pdf_test.page(1).text).to include("good@place.is") + expect(pdf_test.page(1).text).to include("1500 Remount Road, Front Royal, VA 22630") + expect(pdf_test.page(1).text).to include("A product drive donation") + end + + it "renders correctly without a product drive participant" do + pdf = described_class.new(organization, product_drive_donation_without_participant) + pdf_test = PDF::Reader.new(StringIO.new(pdf.compute_and_render)) + expect(pdf_test.page(1).text).to include("Product Drive -- Second Best Product Drive") + expect(pdf_test.page(1).text).to include("A product drive donation") + end + end end diff --git a/spec/queries/distribution_summary_by_county_query_spec.rb b/spec/queries/distribution_summary_by_county_query_spec.rb new file mode 100644 index 0000000000..ec34cf0e6f --- /dev/null +++ b/spec/queries/distribution_summary_by_county_query_spec.rb @@ -0,0 +1,67 @@ +RSpec.describe DistributionSummaryByCountyQuery do + let(:year) { Time.current.year } + let(:issued_at_last_year) { Time.current.change(year: year - 1).to_datetime } + let(:distributions) { [] } + let(:organization_id) { organization.id } + let(:start_date) { nil } + let(:end_date) { nil } + let(:params) { {organization_id:, start_date:, end_date:} } + + include_examples "distribution_by_county" + + before do + create(:storage_location, organization: organization) + end + + describe "get_breakdown" do + it "will have 100% unspecified shows if no served_areas" do + create(:distribution, :with_items, item: item_1, organization: user.organization) + breakdown = DistributionSummaryByCountyQuery.call(**params) + expect(breakdown.size).to eq(1) + expect(breakdown[0].quantity).to eq(100) + expect(breakdown[0].value).to be_within(0.01).of(105000.0) + end + + 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) + 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) + 3.times do |i| + expect(breakdown[i].quantity).to eq(25) + expect(breakdown[i].value).to be_within(0.01).of(26250.0) + end + end + + it "handles multiple partners with overlapping service areas properly" do + create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1, issued_at: issued_at_present) + create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_2, issued_at: issued_at_present) + breakdown = DistributionSummaryByCountyQuery.call(**params) + num_with_45 = 0 + num_with_20 = 0 + num_with_0 = 0 + # The result will have at least 1 45 and at least 1 20, and 1 0. Anything else will be either 45 or 25 or 20 + breakdown.each do |sa| + if sa.quantity == 45 + expect(sa.value).to be_within(0.01).of(47250.0) + num_with_45 += 1 + end + + if sa.quantity == 25 + expect(sa.value).to be_within(0.01).of(26250.0) + end + if sa.quantity == 20 + expect(sa.value).to be_within(0.01).of(21000.0) + num_with_20 += 1 + end + if sa.quantity == 0 + expect(sa.value).to be_within(0.01).of(0) + end + end + expect(num_with_45).to be > 0 + expect(num_with_20).to be > 0 + expect(num_with_0).to eq 0 + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index bcfb3b3fb6..375094b245 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -107,7 +107,7 @@ def self.capybara_tmp_path config.use_transactional_fixtures = true # Location for fixtures (logo, etc) - config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.fixture_paths = ["#{::Rails.root}/spec/fixtures"] # Persistence for failures config.example_status_persistence_file_path = "spec/example_failures.txt" diff --git a/spec/requests/audits_requests_spec.rb b/spec/requests/audits_requests_spec.rb index 80d21de719..61ad5a38a8 100644 --- a/spec/requests/audits_requests_spec.rb +++ b/spec/requests/audits_requests_spec.rb @@ -50,6 +50,21 @@ get new_audit_path expect(response).to be_successful end + + it 'only includes active items in the line item select dropdown' do + create(:item, name: "TestActiveItem", organization: organization) + create(:item, name: "TestInactiveItem", organization: organization, active: false) + + get new_audit_path + expect(response).to have_http_status(:ok) + + html = Nokogiri::HTML(response.body) + options = html.css('select[name="audit[line_items_attributes][0][item_id]"] option') + option_values = options.map { |option| option.text.strip } + + expect(option_values).to include('TestActiveItem') + expect(option_values).not_to include('TestInactiveItem') + end end describe "GET #edit" do diff --git a/spec/requests/distributions_requests_spec.rb b/spec/requests/distributions_requests_spec.rb index 68431b1a9b..66315b1d47 100644 --- a/spec/requests/distributions_requests_spec.rb +++ b/spec/requests/distributions_requests_spec.rb @@ -104,6 +104,24 @@ end end + context "with filters" do + it "shows all active partners in dropdown filter unrestricted by current filter" do + inactive_partner_name = create(:partner, :deactivated, organization:).name + active_partner_name = distribution.partner.name + + # Filter by date with no distributions + params = { filters: { date_range: "January 1,9999 - January 1,9999"} } + + get distributions_path, params: params + page = Nokogiri::HTML(response.body) + partner_select = page.at_css("select[name='filters[by_partner]']") + + expect(partner_select).to be_present + expect(partner_select.text).to include(active_partner_name) + expect(partner_select.text).not_to include(inactive_partner_name) + end + end + context "when filtering by item id" do let!(:item_2) { create(:item, value_in_cents: 100, organization: organization) } let(:params) { { filters: { by_item_id: item.id } } } @@ -193,8 +211,9 @@ describe "POST #create" do let!(:storage_location) { create(:storage_location, organization: organization) } let!(:partner) { create(:partner, organization: organization) } + let(:issued_at) { Time.current } let(:distribution) do - { storage_location_id: storage_location.id, partner_id: partner.id, delivery_method: :delivery } + { storage_location_id: storage_location.id, partner_id: partner.id, issued_at:, delivery_method: :delivery } end it "redirects to #show on success" do @@ -225,6 +244,20 @@ expect(response.body).to include("Item 1 (0)") end + + it "renders #new on failure with only active items in dropdown" do + create(:item, organization: organization, name: 'Active Item') + create(:item, :inactive, organization: organization, name: 'Inactive Item') + + post distributions_path(distribution: { comment: nil, partner_id: nil, storage_location_id: nil }, format: :turbo_stream) + expect(response).to have_http_status(400) + + page = Nokogiri::HTML(response.body) + selectable_items = page.at_css("select.line_item_name").text.split("\n") + + expect(selectable_items).to include("Active Item") + expect(selectable_items).not_to include("Inactive Item") + end context "Deactivated partners should not be displayed in partner dropdown" do before do @@ -240,6 +273,17 @@ expect(response.body).to include("Active Partner") end end + + context "with missing issued_at field" do + let(:issued_at) { "" } + + it "fails and returns validation error message" do + post distributions_path(distribution:, format: :turbo_stream) + + expect(response).to have_http_status(400) + expect(flash[:error]).to include("Distribution date and time can't be blank") + end + end end describe "GET #new" do @@ -268,6 +312,18 @@ expect(page.css('#distribution_storage_location_id option[selected]')).to be_empty end + it "should only show active items in item dropdown" do + create(:item, :inactive, organization: organization, name: 'Inactive Item') + + get new_distribution_path(default_params) + + page = Nokogiri::HTML(response.body) + selectable_items = page.at_css("select#barcode_item_barcodeable_id").text.split("\n") + + expect(selectable_items).to include("Item 1", "Item 2") + expect(selectable_items).not_to include("Inactive Item") + end + context "with org default but no partner default" do it "selects org default" do organization.update!(default_storage_location: storage_location.id) @@ -459,9 +515,10 @@ include_examples "requiring authorization" end - describe "POST #update" do + describe "PATCH #update" do + let(:partner_name) { "Patrick" } let(:location) { create(:storage_location, organization: organization) } - let(:partner) { create(:partner, organization: organization) } + let(:partner) { create(:partner, name: partner_name, organization: organization) } let(:distribution) { create(:distribution, partner: partner, organization: organization) } let(:issued_at) { distribution.issued_at } @@ -481,6 +538,35 @@ expect(response.status).to redirect_to(distribution_path(distribution.to_param)) end + context "with invalid issued_at field" do + let(:distribution_params) do + { id: distribution.id, + distribution: { + partner_id: partner.id, + storage_location_id: location.id, + 'issued_at(1i)' => issued_at.to_date.year, + 'issued_at(2i)' => issued_at.to_date.month, + 'issued_at(3i)' => nil # day part of date missing + }} + end + + it "fails and returns validation error message" do + patch distribution_path(distribution_params) + + expect(flash[:error]).to include("Distribution date and time can't be blank") + expect(response).not_to redirect_to(anything) + end + + it "renders storage location dropdowns" do + patch distribution_path(distribution_params) + + page = Nokogiri::HTML(response.body) + selectable_partners = page.at_css("select#distribution_partner_id").text.split("\n") + + expect(selectable_partners).to include("Patrick") + end + end + describe "when changing storage location" do let(:item) { create(:item, organization: organization) } it "updates storage quantity correctly" do @@ -565,6 +651,19 @@ expect(response.body).to include("Active Partner") end + it "should only show active items in item dropdown" do + create(:item, organization: organization, name: 'Active Item') + create(:item, :inactive, organization: organization, name: 'Inactive Item') + + get edit_distribution_path(id: distribution.id) + + page = Nokogiri::HTML(response.body) + selectable_items = page.at_css("select#barcode_item_barcodeable_id").text.split("\n") + + expect(selectable_items).to include("Active Item") + expect(selectable_items).not_to include("Inactive Item") + end + context 'with units' do let!(:request) { create(:request, diff --git a/spec/requests/donations_requests_spec.rb b/spec/requests/donations_requests_spec.rb index 60c3a1ae90..648fe54b4e 100644 --- a/spec/requests/donations_requests_spec.rb +++ b/spec/requests/donations_requests_spec.rb @@ -100,6 +100,74 @@ end end + describe "POST #create" do + let(:product_drive) { create(:product_drive, organization:) } + let(:storage_location) { create(:storage_location, organization:) } + let(:manufacturer) { create(:manufacturer, organization:) } + let(:source) { Donation::SOURCES[:manufacturer] } + let(:issued_at) { Date.yesterday } + + let(:params) do + { + donation: { + source: Donation::SOURCES[:manufacturer], + manufacturer_id: manufacturer.id, + product_drive_id: product_drive.id, + storage_location_id: storage_location.id, + money_raised_in_dollars: 5, + product_drive_participant_id: nil, + comment: "", + issued_at: issued_at, + line_items_attributes: {} + } + } + end + + it "flashes a success message" do + post donations_path(params) + + expect(flash[:notice]).to eq("Donation created and logged!") + end + + it "redirects to the index page" do + post donations_path(params) + + expect(response).to redirect_to(donations_path) + end + + context "with invalid issued_at param" do + let(:issued_at) { "" } + + it "flashes the correct validation error" do + post donations_path(params) + + expect(flash[:error]).to include("Issue date can't be blank") + end + end + end + + describe "PATCH #update" do + let(:donation) { create(:donation, organization:) } + let(:item) { create(:item, organization:) } + let(:params) { { id: donation.id, donation: donation_params } } + let(:donation_params) do + { + line_items_attributes: { + "0": { item_id: item.id, quantity: 5 } + } + } + end + + context "with invalid issued_at param" do + it "flashes the correct validation error" do + donation_params[:issued_at] = "" + put donation_path(params) + + expect(flash[:alert]).to include("Issue date can't be blank") + end + end + end + describe "GET #print" do let(:item) { create(:item) } let!(:donation) { create(:donation, :with_items, item: item) } @@ -268,7 +336,7 @@ expect(response.body).to include("
  • \n Editing #{original_source}") expect(response.body).to include("") expect(response.body).to include("") - expect(response.body).to include("") + expect(response.body).to include("") expect(response.body).to include("") expect(response.body).to include(edited_comment) expect(response.body).to include("value=\"#{edited_money}\" type=\"text\" name=\"donation[money_raised_in_dollars]") diff --git a/spec/requests/partners_requests_spec.rb b/spec/requests/partners_requests_spec.rb index 9084b6251d..ef086fddfb 100644 --- a/spec/requests/partners_requests_spec.rb +++ b/spec/requests/partners_requests_spec.rb @@ -100,7 +100,7 @@ families_served: 3, children_served: 4, family_zipcodes: 2, - family_zipcodes_list: %w(45612-123 45612-126) + family_zipcodes_list: contain_exactly("45612-126", "45612-123") # order of zipcodes not guaranteed } end @@ -127,7 +127,7 @@ context "when the partner is invited" do it "includes impact metrics" do subject - expect(assigns[:impact_metrics]).to eq(expected_impact_metrics) + expect(assigns[:impact_metrics]).to match(expected_impact_metrics) end end diff --git a/spec/requests/purchases_requests_spec.rb b/spec/requests/purchases_requests_spec.rb index f1167cf9b2..ea44a33ce8 100644 --- a/spec/requests/purchases_requests_spec.rb +++ b/spec/requests/purchases_requests_spec.rb @@ -37,6 +37,27 @@ expect(subject.body).to include("Comments") expect(subject.body).to include("Purchase Comment") end + + describe "pagination" do + around do |ex| + Kaminari.config.default_per_page = 2 + ex.run + Kaminari.config.default_per_page = 50 + end + before do + item = create(:item, organization: organization) + purchase_1 = create(:purchase, organization: organization, comment: "Singleton", issued_at: 1.day.ago) + create(:line_item, item: item, itemizable: purchase_1, quantity: 2) + purchase_2 = create(:purchase, organization: organization, comment: "Twins", issued_at: 2.days.ago) + create(:line_item, item: item, itemizable: purchase_2, quantity: 2) + purchase_3 = create(:purchase, organization: organization, comment: "Fates", issued_at: 3.days.ago) + create(:line_item, item: item, itemizable: purchase_3, quantity: 2) + end + + it "puts the right number of purchases on the page" do + expect(subject.body).to include(" View").twice + end + end end context "csv" do @@ -64,16 +85,16 @@ let!(:storage_location) { create(:storage_location, organization: organization) } let(:line_items) { [attributes_for(:line_item)] } let(:vendor) { create(:vendor, organization: organization) } + let(:purchase) do + { storage_location_id: storage_location.id, + purchased_from: "Google", + vendor_id: vendor.id, + amount_spent: 10, + issued_at: Time.current, + line_items: line_items } + end context "on success" do - let(:purchase) do - { storage_location_id: storage_location.id, - purchased_from: "Google", - vendor_id: vendor.id, - amount_spent: 10, - line_items: line_items } - end - it "redirects to GET#edit" do expect { post purchases_path(purchase: purchase) } .to change { Purchase.count }.by(1) @@ -101,6 +122,15 @@ expect(response).to be_successful # Will render :new expect(response.body).to include('Failed to create purchase due to') end + + context "with invalid issued_at param" do + it "flashes the correct validation error" do + issued_at = "" + post purchases_path(purchase: purchase.merge(issued_at:)) + + expect(flash[:error]).to include("Purchase date can't be blank") + end + end end end @@ -131,6 +161,15 @@ }.by(5) end + context "with invalid issued_at" do + it "redirects to index after update" do + purchase = create(:purchase, purchased_from: "Google") + put purchase_path(id: purchase.id, purchase: { issued_at: "" }) + + expect(flash[:alert]).to include("Purchase date can't be blank") + end + end + describe "when removing a line item" do it "updates storage invetory item quantity correctly" do purchase = create(:purchase, :with_items, item_quantity: 10) diff --git a/spec/requests/vendors_requests_spec.rb b/spec/requests/vendors_requests_spec.rb index 024644f4b6..7625ee7766 100644 --- a/spec/requests/vendors_requests_spec.rb +++ b/spec/requests/vendors_requests_spec.rb @@ -104,7 +104,8 @@ describe "DELETE #destroy" do subject { delete vendor_path(id: create(:vendor)) } it "does not have a route for this" do - expect { subject }.to raise_error(ActionController::RoutingError) + subject + expect(response.code).to eq('404') end end diff --git a/spec/services/distribution_create_service_spec.rb b/spec/services/distribution_create_service_spec.rb index 32fb7d1ab8..cae63cec6c 100644 --- a/spec/services/distribution_create_service_spec.rb +++ b/spec/services/distribution_create_service_spec.rb @@ -12,6 +12,7 @@ partner_id: partner.id, storage_location_id: storage_location.id, delivery_method: :delivery, + issued_at: Time.current, line_items_attributes: { "0": { item_id: storage_location.items.first.id, quantity: 5 } }) @@ -95,6 +96,7 @@ partner_id: partner.id, storage_location_id: storage_location.id, delivery_method: :delivery, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: storage_location.items.first.id, quantity: 500 } } ) } @@ -106,6 +108,27 @@ end end + context "when missing issued_at attribute" do + let(:distribution) { + Distribution.new( + organization_id: organization.id, + partner_id: partner.id, + storage_location_id: storage_location.id, + delivery_method: :delivery, + issued_at: "", + line_items_attributes: { "0": { item_id: storage_location.items.first.id, quantity: 5 } } + ) + } + + it "preserves validation error and is unsuccessful" do + result = subject.new(distribution).call + + expect(result).not_to be_success + expect(result.error).to be_instance_of(ActiveRecord::RecordInvalid) + expect(result.error.message).to include("Distribution date and time can't be blank") + end + end + context "when there's multiple line items and one has insufficient inventory" do let(:too_much_dist) do Distribution.new( @@ -113,6 +136,7 @@ partner_id: partner.id, storage_location_id: storage_location.id, delivery_method: :delivery, + issued_at: Date.yesterday, line_items_attributes: { "0": { item_id: storage_location.items.first.id, quantity: 2 }, diff --git a/spec/services/distribution_update_service_spec.rb b/spec/services/distribution_update_service_spec.rb index 6722e1d978..f7b95845f4 100644 --- a/spec/services/distribution_update_service_spec.rb +++ b/spec/services/distribution_update_service_spec.rb @@ -8,6 +8,16 @@ DistributionUpdateService.new(distribution, new_attributes).call end.to change { distribution.storage_location.size }.by(8) end + + context "when missing issued_at attribute" do + it "preserves validation error and is unsuccessful" do + result = DistributionUpdateService.new(distribution, {issued_at: ""}).call + + expect(result).not_to be_success + expect(result.error).to be_instance_of(ActiveRecord::RecordInvalid) + expect(result.error.message).to include("Distribution date and time can't be blank") + end + end end describe "resend_notification?" do diff --git a/spec/services/distributions_by_county_report_service_spec.rb b/spec/services/distributions_by_county_report_service_spec.rb deleted file mode 100644 index 2a7a20e657..0000000000 --- a/spec/services/distributions_by_county_report_service_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -RSpec.describe DistributionByCountyReportService, type: :service do - let(:year) { Time.current.year } - let(:issued_at_last_year) { Time.current.change(year: year - 1).to_datetime } - let(:distributions) { [] } - - include_examples "distribution_by_county" - - before do - create(:storage_location, organization: organization) - end - - describe "get_breakdown" do - it "will have 100% unspecified shows if no served_areas" do - distribution_1 = create(:distribution, :with_items, item: item_1, organization: user.organization) - breakdown = DistributionByCountyReportService.new.get_breakdown([distribution_1]) - expect(breakdown.size).to eq(1) - expect(breakdown[0].num_items).to eq(100) - expect(breakdown[0].amount).to be_within(0.01).of(105000.0) - end - - it "divides the item numbers and values according to the partner profile" do - distribution_1 = create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1) - breakdown = DistributionByCountyReportService.new.get_breakdown([distribution_1]) - expect(breakdown.size).to eq(5) - expect(breakdown[4].num_items).to eq(0) - expect(breakdown[4].amount).to be_within(0.01).of(0) - 3.times do |i| - expect(breakdown[i].num_items).to eq(25) - expect(breakdown[i].amount).to be_within(0.01).of(26250.0) - end - end - - it "handles multiple partners with overlapping service areas properly" do - distribution_p1 = create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_1, issued_at: issued_at_present) - distribution_p2 = create(:distribution, :with_items, item: item_1, organization: user.organization, partner: partner_2, issued_at: issued_at_present) - breakdown = DistributionByCountyReportService.new.get_breakdown([distribution_p1, distribution_p2]) - num_with_45 = 0 - num_with_20 = 0 - num_with_0 = 0 - # The result will have at least 1 45 and at least 1 20, and 1 0. Anything else will be either 45 or 25 or 20 - breakdown.each do |sa| - if sa.num_items == 45 - expect(sa.amount).to be_within(0.01).of(47250.0) - num_with_45 += 1 - end - - if sa.num_items == 25 - expect(sa.amount).to be_within(0.01).of(26250.0) - end - if sa.num_items == 20 - expect(sa.amount).to be_within(0.01).of(21000.0) - num_with_20 += 1 - end - if sa.num_items == 0 - expect(sa.amount).to be_within(0.01).of(0) - end - end - expect(num_with_45).to be > 0 - expect(num_with_20).to be > 0 - expect(num_with_0).to eq 0 - end - end -end diff --git a/spec/services/donation_create_service_spec.rb b/spec/services/donation_create_service_spec.rb index eace51cb59..76bafc18f4 100644 --- a/spec/services/donation_create_service_spec.rb +++ b/spec/services/donation_create_service_spec.rb @@ -7,5 +7,13 @@ .to change { Donation.count }.by(1) .and change { DonationEvent.count }.by(1) end + + context "when missing issued_at attribute" do + before { donation.issued_at = "" } + + it "raises a validation error" do + expect { described_class.call(donation) }.to raise_error("Issue date can't be blank") + end + end end end diff --git a/spec/services/exports/export_request_service_spec.rb b/spec/services/exports/export_request_service_spec.rb index fb91b5e5d1..cf88dba96d 100644 --- a/spec/services/exports/export_request_service_spec.rb +++ b/spec/services/exports/export_request_service_spec.rb @@ -237,7 +237,7 @@ end it "has expected data for the 3T Diapers request" do - expect(subject[1]).to eq([ + expect(subject).to include([ request_3t.created_at.strftime("%m/%d/%Y").to_s, "Howdy Partner", "Child", @@ -250,7 +250,7 @@ end it "has expected data for the 2T Diapers request" do - expect(subject[2]).to eq([ + expect(subject).to include([ request_2t.created_at.strftime("%m/%d/%Y").to_s, "Howdy Partner", "Individual", @@ -263,7 +263,7 @@ end it "has expected data for the request with deleted items" do - expect(subject[3]).to eq([ + expect(subject).to include([ request_with_deleted_items.created_at.strftime("%m/%d/%Y").to_s, "Howdy Partner", nil, @@ -276,7 +276,7 @@ end it "has expected data for the request with multiple items" do - expect(subject[4]).to eq([ + expect(subject).to include([ request_with_multiple_items.created_at.strftime("%m/%d/%Y").to_s, "Howdy Partner", nil, @@ -289,7 +289,7 @@ end it "has expected data for the request with 4T diapers without pack unit" do - expect(subject[5]).to eq([ + expect(subject).to include([ request_4t.created_at.strftime("%m/%d/%Y").to_s, "Howdy Partner", "Quantity", @@ -302,7 +302,7 @@ end it "has expected data for the request with 4T diapers with pack unit" do - expect(subject[6]).to eq([ + expect(subject).to include([ request_4t_pack.created_at.strftime("%m/%d/%Y").to_s, "Howdy Partner", "Quantity", diff --git a/spec/services/itemizable_update_service_spec.rb b/spec/services/itemizable_update_service_spec.rb index 77ba98fee1..ab8da001c7 100644 --- a/spec/services/itemizable_update_service_spec.rb +++ b/spec/services/itemizable_update_service_spec.rb @@ -62,6 +62,15 @@ expect(UpdateExistingEvent.count).to eq(1) end + it "fails with a validation message for donation events with invalid issued_at" do + attributes[:issued_at] = "" + + expect { subject }.to raise_error do |e| + expect(e).to be_a(ActiveRecord::RecordInvalid) + expect(e.message).to eq("Validation failed: Issue date can't be blank") + end + end + context "when storage location changes" do context "when there is no intervening audit" do it "should update quantity in different locations" do @@ -127,6 +136,15 @@ expect(itemizable.issued_at).to eq(2.days.ago) end + it "fails with a validation message for distribution events with invalid issued_at" do + attributes[:issued_at] = "" + + expect { subject }.to raise_error do |e| + expect(e).to be_a(ActiveRecord::RecordInvalid) + expect(e.message).to eq("Validation failed: Distribution date and time can't be blank") + end + end + context "when storage location changes" do context "when there is no intervening audit" do it "should update quantity in different locations" do diff --git a/spec/services/partners/family_request_create_service_spec.rb b/spec/services/partners/family_request_create_service_spec.rb index 1cbe16d5bf..1ae4348f18 100644 --- a/spec/services/partners/family_request_create_service_spec.rb +++ b/spec/services/partners/family_request_create_service_spec.rb @@ -5,7 +5,7 @@ { partner_user_id: partner_user.id, comments: comments, - for_families: for_families, + request_type: request_type, family_requests_attributes: family_requests_attributes } end @@ -13,7 +13,7 @@ let(:partner) { create(:partner, organization: organization) } let(:partner_user) { partner.primary_user } let(:comments) { Faker::Lorem.paragraph } - let(:for_families) { false } + let(:request_type) { "individual" } context 'when the arguments are incorrect' do context 'because no family_requests_attributes or comments were defined' do @@ -97,8 +97,8 @@ expect(second_item_request.quantity.to_i).to eq(second_item_request.item.default_quantity * 4) end - context "with for_families false" do - let(:for_families) { false } + context "with request_type as individual" do + let(:request_type) { "individual" } it "creates a request of type individual" do expect { subject }.to change { Request.count }.by(1) @@ -108,8 +108,8 @@ end end - context "with for_families true" do - let(:for_families) { true } + context "with request_type as child" do + let(:request_type) { "child" } it "creates a request of type child" do expect { subject }.to change { Request.count }.by(1) diff --git a/spec/system/admin/users_system_spec.rb b/spec/system/admin/users_system_spec.rb index f5a1d727a4..7393dd02ce 100644 --- a/spec/system/admin/users_system_spec.rb +++ b/spec/system/admin/users_system_spec.rb @@ -47,10 +47,7 @@ expect(page).to have_content('User 123') select "Partner", from: "resource_type" find("div.input-group:has(.select2-container)").click - find('.select2-search__field', wait: 5).set("Partner ABC") - find(:xpath, - "//li[contains(@class, 'select2-results__option') and contains(., 'Partner ABC')]", - wait: 5).click + find("li.select2-results__option", text: "Partner ABC").click click_on 'Add Role' expect(page.find('.alert')).to have_content('Role added') diff --git a/spec/system/audit_system_spec.rb b/spec/system/audit_system_spec.rb index 92df751ae5..69a0218ece 100644 --- a/spec/system/audit_system_spec.rb +++ b/spec/system/audit_system_spec.rb @@ -68,6 +68,36 @@ end end + it "allows auditing items that are not in a storage location", :js do + item = create(:item, name: "TestItemNotInStorageLocation", organization: organization) + audit_quantity = 1234 + visit new_audit_path + + await_select2("#audit_line_items_attributes_0_item_id") do + select storage_location.name, from: "Storage location" + end + select item.name, from: "audit_line_items_attributes_0_item_id" + fill_in "audit_line_items_attributes_0_quantity", with: audit_quantity + + accept_confirm do + click_button "Confirm Audit" + end + expect(page.find(".alert-info")).to have_content "Audit is confirmed" + expect(page).to have_content(item.name) + expect(page).to have_content(audit_quantity) + + accept_confirm do + click_link "Finalize Audit" + end + expect(page.find(".alert-info")).to have_content "Audit is Finalized" + + event = Event.last + expect(event.type).to eq "AuditEvent" + event_line_item = Event.last.data.items.first + expect(event_line_item.item_id).to eq item.id + expect(event_line_item.quantity).to eq audit_quantity + end + it "allows user to add items that do not yet have a barcode", :js do item_without_barcode = create(:item) new_barcode = "00000000" diff --git a/spec/system/distribution_system_spec.rb b/spec/system/distribution_system_spec.rb index 5a98ee1ba0..649149773c 100644 --- a/spec/system/distribution_system_spec.rb +++ b/spec/system/distribution_system_spec.rb @@ -558,9 +558,9 @@ context "when editing that distribution" do before do - click_on "Distributions", match: :first - click_on "Edit", match: :first @distribution = Distribution.last + expect(page).to have_current_path(distribution_path(@distribution.id)) + click_on "Make a Correction" end it "User creates a distribution from a donation then edits it" do diff --git a/spec/system/donation_system_spec.rb b/spec/system/donation_system_spec.rb index b38710011a..749ad5c207 100644 --- a/spec/system/donation_system_spec.rb +++ b/spec/system/donation_system_spec.rb @@ -312,20 +312,6 @@ end.to change { Donation.count }.by(1) end - it "Allows User to create a donation for Purchased Supplies" do - select Donation::SOURCES[:misc], from: "donation_source" - expect(page).not_to have_xpath("//select[@id='donation_donation_site_id']") - expect(page).not_to have_xpath("//select[@id='donation_product_drive_participant_id']") - expect(page).not_to have_xpath("//select[@id='donation_manufacturer_id']") - select StorageLocation.first.name, from: "donation_storage_location_id" - select Item.alphabetized.first.name, from: "donation_line_items_attributes_0_item_id" - fill_in "donation_line_items_attributes_0_quantity", with: "5" - - expect do - click_button "Save" - end.to change { Donation.count }.by(1) - end - it "Allows User to create a donation with a Miscellaneous source" do select Donation::SOURCES[:misc], from: "donation_source" expect(page).not_to have_xpath("//select[@id='donation_donation_site_id']") diff --git a/spec/system/partners/children_system_spec.rb b/spec/system/partners/children_system_spec.rb index 25b2531138..8498f5778f 100644 --- a/spec/system/partners/children_system_spec.rb +++ b/spec/system/partners/children_system_spec.rb @@ -30,7 +30,7 @@ expect(page).to have_text("Child Last Name") expect(page).to have_text("01234") expect(page).to have_text("Some Comment") - expect(page).to have_text("Item 1, Item 2") + expect(page).to have_text(/Item 1, Item 2|Item 2, Item 1/) # order of items requested not guaranteed end end end diff --git a/spec/system/partners/family_requests_system_spec.rb b/spec/system/partners/family_requests_system_spec.rb index f6c5642e4c..71e3ddcb19 100644 --- a/spec/system/partners/family_requests_system_spec.rb +++ b/spec/system/partners/family_requests_system_spec.rb @@ -29,13 +29,13 @@ within("table tbody tr", text: "Main Items1") do |row| expect(row).to have_css("td", text: "Main Family") expect(row).to have_css("td", text: "Main Items1") - expect(row).to have_css("td", text: "Item 1, Item 2") + expect(row).to have_css("td", text: /Item 1, Item 2|Item 2, Item 1/) # order of items requested not guaranteed end within("table tbody tr", text: "Main Items2") do |row| expect(row).to have_css("td", text: "Main Family") expect(row).to have_css("td", text: "Main Items2") - expect(row).to have_css("td", text: "Item 2, Item 3") + expect(row).to have_css("td", text: /Item 2, Item 3|Item 3, Item 2/) # order of items requested not guaranteed end within("table tbody tr", text: "Main No Items") do |row| @@ -47,7 +47,7 @@ within("table tbody tr", text: "Other Items") do |row| expect(row).to have_css("td", text: "Other Family") expect(row).to have_css("td", text: "Other Items") - expect(row).to have_css("td", text: "Item 1, Item 2") + expect(row).to have_css("td", text: /Item 1, Item 2|Item 2, Item 1/) # order of items requested not guaranteed end within("table tbody tr", text: "Other No Items") do |row|
  • <%= link_to item.created_at.strftime("%m/%d/%Y"), item %><%= item.partner.name %><%= item.partner_user&.formatted_email %><%= item.comments %><%= link_to request.created_at.strftime("%m/%d/%Y"), request %><%= request.partner.name %><%= request.partner_user&.formatted_email %><%= request.comments %>
    <%= bd.name %><%= number_with_delimiter(bd.num_items) %><%= dollar_presentation(bd.amount) %><%= number_with_delimiter(bd.quantity) %><%= dollar_presentation(bd.value) %>