Merge branch 'master' into 3-0-stable-Apr28

This commit is contained in:
Luis Ramos
2020-04-28 13:40:19 +01:00
631 changed files with 15133 additions and 10891 deletions

View File

@@ -33,7 +33,6 @@ Layout/LineLength:
- app/controllers/admin/inventory_items_controller.rb
- app/controllers/admin/manager_invitations_controller.rb
- app/controllers/admin/product_import_controller.rb
- app/controllers/admin/proxy_orders_controller.rb
- app/controllers/admin/schedules_controller.rb
- app/controllers/admin/subscriptions_controller.rb
- app/controllers/admin/variant_overrides_controller.rb
@@ -89,7 +88,6 @@ Layout/LineLength:
- app/models/spree/tax_rate_decorator.rb
- app/models/spree/taxon_decorator.rb
- app/models/spree/user.rb
- app/models/spree/variant_decorator.rb
- app/models/subscription.rb
- app/models/variant_override.rb
- app/models/variant_override_set.rb
@@ -98,7 +96,6 @@ Layout/LineLength:
- app/services/embedded_page_service.rb
- app/services/order_cycle_form.rb
- app/services/order_factory.rb
- app/services/subscriptions_count.rb
- app/services/variants_stock_levels.rb
- engines/web/app/helpers/web/cookies_policy_helper.rb
- lib/discourse/single_sign_on.rb
@@ -158,7 +155,6 @@ Layout/LineLength:
- spec/controllers/spree/admin/orders/customer_details_controller_spec.rb
- spec/controllers/spree/admin/orders_controller_spec.rb
- spec/controllers/spree/admin/payment_methods_controller_spec.rb
- spec/controllers/spree/admin/payments_controller_spec.rb
- spec/controllers/spree/admin/products_controller_spec.rb
- spec/controllers/spree/admin/reports_controller_spec.rb
- spec/controllers/spree/admin/variants_controller_spec.rb
@@ -183,7 +179,6 @@ Layout/LineLength:
- spec/features/admin/image_settings_spec.rb
- spec/features/admin/multilingual_spec.rb
- spec/features/admin/order_cycles_spec.rb
- spec/features/admin/orders_spec.rb
- spec/features/admin/overview_spec.rb
- spec/features/admin/payment_method_spec.rb
- spec/features/admin/product_import_spec.rb
@@ -239,10 +234,7 @@ Layout/LineLength:
- spec/lib/open_food_network/packing_report_spec.rb
- spec/lib/open_food_network/permissions_spec.rb
- spec/lib/open_food_network/products_and_inventory_report_spec.rb
- spec/lib/open_food_network/proxy_order_syncer_spec.rb
- spec/lib/open_food_network/scope_variant_to_hub_spec.rb
- spec/lib/open_food_network/subscription_payment_updater_spec.rb
- spec/lib/open_food_network/subscription_summarizer_spec.rb
- spec/lib/open_food_network/tag_rule_applicator_spec.rb
- spec/lib/open_food_network/users_and_enterprises_report_spec.rb
- spec/lib/open_food_network/xero_invoices_report_spec.rb
@@ -317,10 +309,6 @@ Layout/LineLength:
- spec/services/permissions/order_spec.rb
- spec/services/product_tag_rules_filterer_spec.rb
- spec/services/products_renderer_spec.rb
- spec/services/subscription_estimator_spec.rb
- spec/services/subscription_form_spec.rb
- spec/services/subscription_validator_spec.rb
- spec/services/subscription_variants_service_spec.rb
- spec/spec_helper.rb
- spec/support/cancan_helper.rb
- spec/support/delayed_job_helper.rb
@@ -386,7 +374,6 @@ Metrics/AbcSize:
- app/helpers/spree/admin/base_helper.rb
- app/helpers/spree/admin/zones_helper.rb
- app/helpers/spree/orders_helper.rb
- app/mailers/producer_mailer.rb
- app/models/calculator/flat_percent_per_item.rb
- app/models/column_preference.rb
- app/models/enterprise.rb
@@ -411,7 +398,7 @@ Metrics/AbcSize:
- app/services/cart_service.rb
- app/services/create_order_cycle.rb
- app/services/order_syncer.rb
- app/services/subscription_validator.rb
- engines/order_management/app/services/order_management/subscriptions/validator.rb
- lib/active_merchant/billing/gateways/stripe_decorator.rb
- lib/active_merchant/billing/gateways/stripe_payment_intents.rb
- lib/discourse/single_sign_on.rb
@@ -477,7 +464,6 @@ Metrics/BlockLength:
- spec/factories/shipping_method_factory.rb
- spec/factories/subscription_factory.rb
- spec/factories/variant_factory.rb
- spec/features/admin/orders_spec.rb
- spec/features/consumer/shopping/embedded_shopfronts_spec.rb
- spec/lib/open_food_network/group_buy_report_spec.rb
- spec/models/tag_rule/discount_order_spec.rb
@@ -583,7 +569,6 @@ Metrics/MethodLength:
- app/helpers/spree/admin/navigation_helper.rb
- app/helpers/spree/admin/base_helper.rb
- app/jobs/subscription_placement_job.rb
- app/mailers/producer_mailer.rb
- app/models/column_preference.rb
- app/models/enterprise.rb
- app/models/enterprise_relationship.rb
@@ -686,6 +671,12 @@ Metrics/ModuleLength:
- app/helpers/injection_helper.rb
- app/helpers/spree/admin/navigation_helper.rb
- app/helpers/spree/admin/base_helper.rb
- engines/order_management/spec/services/order_management/subscriptions/estimator_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/form_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/proxy_order_syncer_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/summarizer_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/validator_spec.rb
- engines/order_management/spec/services/order_management/subscriptions/variants_list_spec.rb
- lib/open_food_network/column_preference_defaults.rb
- spec/controllers/admin/enterprises_controller_spec.rb
- spec/controllers/admin/order_cycles_controller_spec.rb
@@ -701,9 +692,7 @@ Metrics/ModuleLength:
- spec/lib/open_food_network/order_grouper_spec.rb
- spec/lib/open_food_network/permissions_spec.rb
- spec/lib/open_food_network/products_and_inventory_report_spec.rb
- spec/lib/open_food_network/proxy_order_syncer_spec.rb
- spec/lib/open_food_network/scope_variant_to_hub_spec.rb
- spec/lib/open_food_network/subscription_payment_updater_spec.rb
- spec/lib/open_food_network/tag_rule_applicator_spec.rb
- spec/lib/open_food_network/users_and_enterprises_report_spec.rb
- spec/models/spree/ability_spec.rb

View File

@@ -107,7 +107,6 @@ Lint/DuplicateHashKey:
Lint/DuplicateMethods:
Exclude:
- 'lib/discourse/single_sign_on.rb'
- 'lib/open_food_network/subscription_summary.rb'
# Offense count: 10
Lint/IneffectiveAccessModifier:
@@ -209,7 +208,7 @@ Naming/MemoizedInstanceVariableName:
Naming/MethodParameterName:
Exclude:
- 'app/helpers/spree/base_helper_decorator.rb'
- 'app/services/subscription_validator.rb'
- 'engines/order_management/app/services/order_management/subscriptions/validator.rb'
- 'lib/open_food_network/reports/bulk_coop_report.rb'
- 'lib/open_food_network/xero_invoices_report.rb'
- 'spec/lib/open_food_network/reports/report_spec.rb'
@@ -224,7 +223,6 @@ Naming/MethodParameterName:
Naming/PredicateName:
Exclude:
- 'spec/**/*'
- 'app/mailers/producer_mailer.rb'
- 'app/models/enterprise.rb'
- 'app/models/enterprise_relationship.rb'
- 'app/models/order_cycle.rb'
@@ -861,7 +859,6 @@ Style/FrozenStringLiteralComment:
- 'app/jobs/subscription_placement_job.rb'
- 'app/jobs/welcome_enterprise_job.rb'
- 'app/mailers/enterprise_mailer.rb'
- 'app/mailers/producer_mailer.rb'
- 'app/mailers/spree/base_mailer_decorator.rb'
- 'app/mailers/spree/order_mailer_decorator.rb'
- 'app/mailers/spree/user_mailer.rb'
@@ -1078,11 +1075,6 @@ Style/FrozenStringLiteralComment:
- 'app/services/reset_order_service.rb'
- 'app/services/restart_checkout.rb'
- 'app/services/search_orders.rb'
- 'app/services/subscription_estimator.rb'
- 'app/services/subscription_form.rb'
- 'app/services/subscription_validator.rb'
- 'app/services/subscription_variants_service.rb'
- 'app/services/subscriptions_count.rb'
- 'app/services/tax_rate_finder.rb'
- 'app/services/upload_sanitizer.rb'
- 'app/services/variant_deleter.rb'
@@ -1090,7 +1082,6 @@ Style/FrozenStringLiteralComment:
- 'app/validators/date_time_string_validator.rb'
- 'app/validators/distributors_validator.rb'
- 'app/validators/integer_array_validator.rb'
- 'app/views/spree/admin/taxons/search.rabl'
- 'config.ru'
- 'engines/order_management/app/controllers/order_management/application_controller.rb'
- 'engines/order_management/app/services/order_management/reports/enterprise_fee_summary/authorizer.rb'
@@ -1121,6 +1112,7 @@ Style/FrozenStringLiteralComment:
- 'engines/order_management/lib/order_management/engine.rb'
- 'engines/order_management/lib/order_management/version.rb'
- 'engines/order_management/order_management.gemspec'
- 'engines/order_management/spec/performance/order_management/subscriptions/proxy_order_syncer_spec.rb'
- 'engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/authorizer_spec.rb'
- 'engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/parameters_spec.rb'
- 'engines/order_management/spec/services/order_management/reports/enterprise_fee_summary/permissions_spec.rb'
@@ -1177,7 +1169,6 @@ Style/FrozenStringLiteralComment:
- 'lib/open_food_network/products_and_inventory_report.rb'
- 'lib/open_food_network/products_and_inventory_report_base.rb'
- 'lib/open_food_network/property_merge.rb'
- 'lib/open_food_network/proxy_order_syncer.rb'
- 'lib/open_food_network/rack_request_blocker.rb'
- 'lib/open_food_network/referer_parser.rb'
- 'lib/open_food_network/reports/bulk_coop_allocation_report.rb'
@@ -1194,8 +1185,6 @@ Style/FrozenStringLiteralComment:
- 'lib/open_food_network/scope_variants_for_search.rb'
- 'lib/open_food_network/spree_api_key_loader.rb'
- 'lib/open_food_network/subscription_payment_updater.rb'
- 'lib/open_food_network/subscription_summarizer.rb'
- 'lib/open_food_network/subscription_summary.rb'
- 'lib/open_food_network/tag_rule_applicator.rb'
- 'lib/open_food_network/user_balance_calculator.rb'
- 'lib/open_food_network/users_and_enterprises_report.rb'
@@ -1435,7 +1424,6 @@ Style/FrozenStringLiteralComment:
- 'spec/lib/open_food_network/permissions_spec.rb'
- 'spec/lib/open_food_network/products_and_inventory_report_spec.rb'
- 'spec/lib/open_food_network/property_merge_spec.rb'
- 'spec/lib/open_food_network/proxy_order_syncer_spec.rb'
- 'spec/lib/open_food_network/referer_parser_spec.rb'
- 'spec/lib/open_food_network/reports/report_spec.rb'
- 'spec/lib/open_food_network/reports/row_spec.rb'
@@ -1444,8 +1432,6 @@ Style/FrozenStringLiteralComment:
- 'spec/lib/open_food_network/scope_variant_to_hub_spec.rb'
- 'spec/lib/open_food_network/scope_variants_to_search_spec.rb'
- 'spec/lib/open_food_network/subscription_payment_updater_spec.rb'
- 'spec/lib/open_food_network/subscription_summarizer_spec.rb'
- 'spec/lib/open_food_network/subscription_summary_spec.rb'
- 'spec/lib/open_food_network/tag_rule_applicator_spec.rb'
- 'spec/lib/open_food_network/user_balance_calculator_spec.rb'
- 'spec/lib/open_food_network/users_and_enterprises_report_spec.rb'
@@ -1529,7 +1515,6 @@ Style/FrozenStringLiteralComment:
- 'spec/models/variant_override_spec.rb'
- 'spec/performance/injection_helper_spec.rb'
- 'spec/performance/orders_controller_spec.rb'
- 'spec/performance/proxy_order_syncer_spec.rb'
- 'spec/performance/shop_controller_spec.rb'
- 'spec/requests/checkout/failed_checkout_spec.rb'
- 'spec/requests/checkout/paypal_spec.rb'
@@ -1581,11 +1566,6 @@ Style/FrozenStringLiteralComment:
- 'spec/services/reset_order_service_spec.rb'
- 'spec/services/restart_checkout_spec.rb'
- 'spec/services/search_orders_spec.rb'
- 'spec/services/subscription_estimator_spec.rb'
- 'spec/services/subscription_form_spec.rb'
- 'spec/services/subscription_validator_spec.rb'
- 'spec/services/subscription_variants_service_spec.rb'
- 'spec/services/subscriptions_count_spec.rb'
- 'spec/services/tax_rate_finder_spec.rb'
- 'spec/services/upload_sanitizer_spec.rb'
- 'spec/services/variants_stock_levels_spec.rb'
@@ -1778,7 +1758,6 @@ Style/Send:
- 'spec/lib/open_food_network/products_and_inventory_report_spec.rb'
- 'spec/lib/open_food_network/sales_tax_report_spec.rb'
- 'spec/lib/open_food_network/subscription_payment_updater_spec.rb'
- 'spec/lib/open_food_network/subscription_summarizer_spec.rb'
- 'spec/lib/open_food_network/tag_rule_applicator_spec.rb'
- 'spec/lib/open_food_network/xero_invoices_report_spec.rb'
- 'spec/lib/stripe/webhook_handler_spec.rb'

View File

@@ -35,13 +35,20 @@ Download the Docker images and build the containers:
$ docker-compose build
```
Run the app with all the required containers:
Setup the database and seed it with sample data:
```sh
$ docker-compose run web bundle exec rake db:reset
$ docker-compose run web bundle exec rake db:test:prepare
$ docker-compose run web bundle exec rake ofn:sample_data
```
Finally, run the app with all the required containers:
```sh
$ docker-compose up
```
This command will setup the database and seed it with sample data. The default admin user is 'ofn@example.com' with 'ofn123' password.
The default admin user is 'ofn@example.com' with 'ofn123' password.
Check the app in the browser at `http://localhost:3000`.
You will then get the trace of the containers in the terminal. You can stop the containers using Ctrl-C in the terminal.

View File

@@ -12,7 +12,7 @@ ENV BUNDLE_PATH /bundles
WORKDIR /usr/src/app
COPY .ruby-version .
# Rbenv & Ruby part
# Install Rbenv & Ruby
RUN git clone https://github.com/rbenv/rbenv.git ${RBENV_ROOT} && \
git clone https://github.com/rbenv/ruby-build.git ${RBENV_ROOT}/plugins/ruby-build && \
${RBENV_ROOT}/plugins/ruby-build/install.sh && \
@@ -21,7 +21,7 @@ RUN git clone https://github.com/rbenv/rbenv.git ${RBENV_ROOT} && \
rbenv global $(cat .ruby-version) && \
gem install bundler --version=1.17.2
# Postgres
# Install Postgres
RUN sh -c "echo 'deb https://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main' > /etc/apt/sources.list.d/pgdg.list" && \
wget --quiet -O - https://apt.postgresql.org/pub/repos/apt/ACCC4CF8.asc | apt-key add - && \
apt-get update && \
@@ -38,4 +38,6 @@ RUN wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.z
unzip chromedriver_linux64.zip -d /usr/bin && \
chmod u+x /usr/bin/chromedriver
# Copy code and install app dependencies
COPY . /usr/src/app/
RUN bundle install

View File

@@ -118,7 +118,7 @@ $ createdb open_food_network_test --owner=ofn
If these commands succeed, you should be able to [continue the setup process](#get-it-running).
[developer-wiki]: https://github.com/openfoodfoundation/openfoodnetwork/wiki
[sierra]: https://github.com/openfoodfoundation/openfoodnetwork/wiki/Development-Environment-Setup%3A-macOS-%28Sierra%2C-HighSierra-and-Mojave%29
[sierra]: https://github.com/openfoodfoundation/openfoodnetwork/wiki/Development-Environment-Setup%3A-macOS-%28Sierra%2C-HighSierra%2C-Mojave-and-Catalina%29
[el-capitan]: https://github.com/openfoodfoundation/openfoodnetwork/wiki/Development-Environment-Setup:-OS-X-(El-Capitan)
[ubuntu]: https://github.com/openfoodfoundation/openfoodnetwork/wiki/Development-Environment-Setup:-Ubuntu
[wiki]: https://github.com/openfoodfoundation/openfoodnetwork/wiki

13
Gemfile
View File

@@ -9,7 +9,6 @@ gem 'rails-i18n', '~> 4.0'
gem 'rails_safe_tasks', '~> 1.0'
gem "activerecord-import"
gem 'nokogiri', '~> 1.6.8.1'
gem "catalog", path: "./engines/catalog"
gem "order_management", path: "./engines/order_management"
@@ -44,10 +43,6 @@ gem 'daemons'
gem 'delayed_job_active_record'
gem 'delayed_job_web'
# Fix bug in simple_form preventing collection_check_boxes usage within form_for block
# When merged, revert to upstream gem
# gem 'simple_form', github: 'RohanM/simple_form'
# Spree's default pagination gem (locked to the current version used by Spree)
# We use it's methods in OFN code as well, so this is a direct dependency
gem 'kaminari', '~> 0.14.1'
@@ -58,7 +53,6 @@ gem 'aws-sdk'
gem 'bugsnag'
gem 'db2fog'
gem 'haml'
gem 'rabl'
gem 'redcarpet'
gem 'sass'
gem 'sass-rails'
@@ -101,7 +95,7 @@ gem 'test-unit', '~> 3.3'
gem 'coffee-rails', '~> 4.0.0'
gem 'compass-rails'
gem 'mini_racer', '0.2.9'
gem 'mini_racer', '0.2.10'
gem 'uglifier', '>= 1.0.3'
@@ -120,16 +114,17 @@ gem 'ofn-qz', github: 'openfoodfoundation/ofn-qz', branch: 'ofn-rails-4'
group :production, :staging do
gem 'ddtrace'
gem 'unicorn-worker-killer'
end
group :test, :development do
# Pretty printed test output
gem 'atomic'
gem 'awesome_print'
gem 'capybara', '>= 2.18.0' # 3.0 requires nokogiri 1.8
gem 'capybara', '>= 2.18.0' # 3.0 requires rack 1.6 that only works with Rails 4.2
gem 'database_cleaner', require: false
gem "factory_bot_rails", '4.10.0', require: false
gem 'fuubar', '~> 2.4.1'
gem 'fuubar', '~> 2.5.0'
gem 'json_spec', '~> 1.1.4'
gem 'knapsack'
gem 'letter_opener', '>= 1.4.1'

View File

@@ -31,7 +31,6 @@ GIT
awesome_nested_set (~> 3.0.0.rc.1)
aws-sdk (= 1.11.1)
cancan (~> 1.6.10)
deface (>= 1.0.0.rc3)
ffaker (~> 1.16)
highline (= 1.6.18)
httparty (~> 0.11)
@@ -169,7 +168,6 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.12.2)
colorize (0.8.1)
combine_pdf (1.0.16)
ruby-rc4 (>= 0.1.5)
compass (1.0.3)
@@ -200,14 +198,9 @@ GEM
activerecord (>= 3.2.0, < 5.0)
fog (~> 1.0)
rails (>= 3.2.0, < 5.0)
ddtrace (0.34.0)
ddtrace (0.34.2)
msgpack
debugger-linecache (1.2.0)
deface (1.0.2)
colorize (>= 0.5.8)
nokogiri (~> 1.6.0)
polyglot
rails (>= 3.1)
delayed_job (4.1.8)
activesupport (>= 3.0, < 6.1)
delayed_job_active_record (4.1.4)
@@ -406,6 +399,8 @@ GEM
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
geocoder (1.5.2)
get_process_mem (0.2.5)
ffi (~> 1.0)
gmaps4rails (2.1.2)
haml (5.1.2)
temple (>= 0.8.0)
@@ -452,9 +447,9 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
mini_mime (1.0.2)
mini_portile2 (2.1.0)
mini_racer (0.2.9)
libv8 (>= 6.9.411)
mini_portile2 (2.4.0)
mini_racer (0.2.10)
libv8 (> 7.3)
minitest (4.7.5)
momentjs-rails (2.20.1)
railties (>= 3.1)
@@ -465,15 +460,15 @@ GEM
multi_xml (0.6.0)
multipart-post (2.1.1)
newrelic_rpm (3.18.1.330)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
oauth2 (1.4.4)
faraday (>= 0.8, < 2.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.10.5)
oj (3.10.6)
optimist (3.0.0)
orm_adapter (0.5.0)
paper_trail (5.2.3)
@@ -488,7 +483,7 @@ GEM
parallel (1.19.1)
paranoia (2.4.2)
activerecord (>= 4.0, < 6.1)
parser (2.7.0.5)
parser (2.7.1.0)
ast (~> 2.4.0)
paypal-sdk-core (0.2.10)
multi_json (~> 1.0)
@@ -594,7 +589,7 @@ GEM
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-rails (2.5.0)
rubocop-rails (2.5.2)
activesupport
rack (>= 1.1)
rubocop (>= 0.72.0)
@@ -660,6 +655,9 @@ GEM
unicorn-rails (2.2.1)
rack
unicorn
unicorn-worker-killer (0.4.4)
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uuidtools (2.1.5)
warden (1.2.7)
rack (>= 1.0)
@@ -739,10 +737,9 @@ DEPENDENCIES
kaminari (~> 0.14.1)
knapsack
letter_opener (>= 1.4.1)
mini_racer (= 0.2.9)
mini_racer (= 0.2.10)
momentjs-rails
newrelic_rpm (~> 3.0)
nokogiri (~> 1.6.8.1)
oauth2 (~> 1.4.4)
ofn-qz!
oj
@@ -751,7 +748,6 @@ DEPENDENCIES
paperclip (~> 3.4.1)
pg (~> 0.21.0)
pry-byebug (>= 3.4.3)
rabl
rack-mini-profiler (< 3.0.0)
rack-rewrite
rack-ssl
@@ -784,6 +780,7 @@ DEPENDENCIES
uglifier (>= 1.0.3)
unicorn
unicorn-rails
unicorn-worker-killer
web!
webdrivers
webmock

View File

@@ -2,19 +2,24 @@ angular.module("admin.indexUtils").factory "PagedFetcher", (dataFetcher) ->
new class PagedFetcher
# Given a URL like http://example.com/foo?page=::page::&per_page=20
# And the response includes an attribute pages with the number of pages to fetch
# Fetch each page async, and call the processData callback with the resulting data
fetch: (url, processData, onLastPageComplete) ->
dataFetcher(@urlForPage(url, 1)).then (data) =>
processData data
# Fetch each page async, and call the pageCallback callback with the resulting data
# Developer note: this class should not be re-used!
page: 1
last_page: 1
if data.pages > 1
for page in [2..data.pages]
lastPromise = dataFetcher(@urlForPage(url, page)).then (data) ->
processData data
onLastPageComplete && lastPromise.then onLastPageComplete
return
else
onLastPageComplete && onLastPageComplete()
fetch: (url, pageCallback) ->
@fetchPages(url, @page, pageCallback)
urlForPage: (url, page) ->
url.replace("::page::", page)
fetchPages: (url, page, pageCallback) ->
dataFetcher(@urlForPage(url, page)).then (data) =>
@page++
@last_page = data.pages
pageCallback(data) if pageCallback
if @page <= @last_page
@fetchPages(url, @page, pageCallback)

View File

@@ -67,7 +67,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, $timeout, Reque
return unless sort && sort.predicate != ""
$scope.sorting = sort.getSortingExpr()
$scope.fetchProducts()
$scope.fetchResults()
, true
$scope.capturePayment = (order) ->

View File

@@ -43,12 +43,11 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl",
$scope.fetchProducts = ->
url = "/api/products/overridable?page=::page::;per_page=100"
PagedFetcher.fetch url, (data) => $scope.addProducts data.products
PagedFetcher.fetch url, $scope.addProducts
$scope.addProducts = (products) ->
$scope.products = $scope.products.concat products
VariantOverrides.ensureDataFor hubs, products
$scope.addProducts = (data) ->
$scope.products = $scope.products.concat data.products
VariantOverrides.ensureDataFor hubs, data.products
$scope.displayDirty = ->
if DirtyVariantOverrides.count() > 0

View File

@@ -9,8 +9,13 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, $location
$scope.show_closed = false
$scope.filtersActive = false
$scope.distanceMatchesShown = false
$scope.closed_shops_loading = false
$scope.closed_shops_loaded = false
$scope.$watch "query", (query)->
$scope.resetSearch(query)
$scope.resetSearch = (query) ->
Enterprises.flagMatching query
Search.search query
$rootScope.$broadcast 'enterprisesChanged'
@@ -19,6 +24,7 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, $location
$timeout ->
Enterprises.calculateDistance query, $scope.firstNameMatch()
$rootScope.$broadcast 'enterprisesChanged'
$scope.closed_shops_loading = false
$timeout ->
if $location.search()['show_closed']?
@@ -73,6 +79,12 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, $location
undefined
$scope.showClosedShops = ->
unless $scope.closed_shops_loaded
$scope.closed_shops_loading = true
$scope.closed_shops_loaded = true
Enterprises.loadClosedEnterprises().then ->
$scope.resetSearch($scope.query)
$scope.show_closed = true
$location.search('show_closed', '1')

View File

@@ -24,7 +24,7 @@ Darkswarm.controller "HubNodeCtrl", ($scope, HashNavigation, CurrentHub, $http,
$scope.shopfront_loading = true
$scope.toggle_tab(event)
$http.get("/api/enterprises/" + $scope.hub.id + "/shopfront")
$http.get("/api/shops/" + $scope.hub.id)
.success (data) ->
$scope.shopfront_loading = false
$scope.hub = data

View File

@@ -24,7 +24,7 @@ Darkswarm.controller "ProducerNodeCtrl", ($scope, HashNavigation, $anchorScroll,
$scope.shopfront_loading = true
$scope.toggle_tab(event)
$http.get("/api/enterprises/" + $scope.producer.id + "/shopfront")
$http.get("/api/shops/" + $scope.producer.id)
.success (data) ->
$scope.shopfront_loading = false
$scope.producer = data

View File

@@ -14,15 +14,28 @@ Darkswarm.factory 'Checkout', ($injector, CurrentOrder, ShippingMethods, StripeE
submit: =>
Loading.message = t 'submitting_order'
$http.put('/checkout.json', {order: @preprocess()}).success (data, status)=>
Navigation.go data.path
.error (response, status)=>
if response.path
Navigation.go response.path
else
Loading.clear()
@errors = response.errors
RailsFlashLoader.loadFlash(response.flash)
$http.put('/checkout.json', {order: @preprocess()})
.then (response) =>
Navigation.go response.data.path
.catch (response) =>
try
@handle_checkout_error_response(response)
catch error
@loadFlash(error: t("checkout.failed")) # inform the user about the unexpected error
throw error # generate a BugsnagJS alert
handle_checkout_error_response: (response) =>
if response.data.path
Navigation.go response.data.path
else
throw response unless response.data.flash
@errors = response.data.errors
@loadFlash(response.data.flash)
loadFlash: (flash) =>
Loading.clear()
RailsFlashLoader.loadFlash(flash)
# Rails wants our Spree::Address data to be provided with _attributes
preprocess: ->

View File

@@ -5,7 +5,7 @@ Darkswarm.factory "EnterpriseModal", ($modal, $rootScope, $http)->
scope = $rootScope.$new(true) # Spawn an isolate to contain the enterprise
scope.embedded_layout = window.location.search.indexOf("embedded_shopfront=true") != -1
$http.get("/api/enterprises/" + enterprise.id + "/shopfront").success (data) ->
$http.get("/api/shops/" + enterprise.id).success (data) ->
scope.enterprise = data
$modal.open(templateUrl: "enterprise_modal.html", scope: scope)
.error (data) ->

View File

@@ -1,27 +1,30 @@
Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer, Matcher, Geo, $rootScope) ->
Darkswarm.factory 'Enterprises', (enterprises, ShopsResource, CurrentHub, Taxons, Dereferencer, Matcher, Geo, $rootScope) ->
new class Enterprises
enterprises: []
enterprises_by_id: {}
constructor: ->
# Populate Enterprises.enterprises from json in page.
@enterprises = enterprises
@initEnterprises(enterprises)
initEnterprises: (enterprises) ->
# Map enterprises to id/object pairs for lookup.
for enterprise in enterprises
@enterprises.push enterprise
@enterprises_by_id[enterprise.id] = enterprise
# Replace enterprise and taxons ids with actual objects.
@dereferenceEnterprises()
@dereferenceEnterprises(enterprises)
@producers = @enterprises.filter (enterprise)->
enterprise.category in ["producer_hub", "producer_shop", "producer"]
@hubs = @enterprises.filter (enterprise)->
enterprise.category in ["hub", "hub_profile", "producer_hub", "producer_shop"]
dereferenceEnterprises: ->
dereferenceEnterprises: (enteprises) ->
if CurrentHub.hub?.id
CurrentHub.hub = @enterprises_by_id[CurrentHub.hub.id]
for enterprise in @enterprises
for enterprise in enterprises
@dereferenceEnterprise enterprise
dereferenceEnterprise: (enterprise) ->
@@ -42,6 +45,12 @@ Darkswarm.factory 'Enterprises', (enterprises, CurrentHub, Taxons, Dereferencer,
for enterprise in new_enterprises
@enterprises_by_id[enterprise.id] = enterprise
loadClosedEnterprises: ->
request = ShopsResource.closed_shops {}, (data) =>
@initEnterprises(data)
request.$promise
flagMatching: (query) ->
for enterprise in @enterprises
enterprise.matches_name_query = if query? && query.length > 0

View File

@@ -1,6 +1,6 @@
Darkswarm.factory 'OrderCycle', ($resource, orderCycleData) ->
class OrderCycle
@order_cycle = orderCycleData # Object or {} due to RABL
@order_cycle = orderCycleData # Object or {}
@push_order_cycle: (callback) ->
new $resource("/shop/order_cycle").save {order_cycle_id: @order_cycle.order_cycle_id}, (order_data)->
OrderCycle.order_cycle.orders_close_at = order_data.orders_close_at

View File

@@ -0,0 +1,7 @@
Darkswarm.factory 'ShopsResource', ($resource) ->
$resource('/api/shops/:id.json', {}, {
'closed_shops':
method: 'GET'
isArray: true
url: '/api/shops/closed_shops.json'
})

View File

@@ -8,4 +8,4 @@
%hr
%div.menu_item.text-center
%input.fullwidth.orange{ type: "button", ng: { value: "saved() ? 'Saved': 'Saving'", show: "saved() || saving", disabled: "saved()" } }
%input.fullwidth.red{ type: "button", value: 'Save As Default', ng: { show: "!saved() && !saving", click: "saveColumnPreferences(action)"} }
%input.fullwidth.red{ type: "button", :value => t('admin.column_save_as_default').html_safe, ng: { show: "!saved() && !saving", click: "saveColumnPreferences(action)"} }

View File

@@ -9,6 +9,9 @@
input#search {
@include medium-input(rgba(0, 0, 0, 0.3), #777, $clr-brick);
// avoid zoom on iphone, see issue #4535
font-size: 1rem;
}
// ordering

View File

@@ -54,6 +54,7 @@ $teal-400: #4cb5c5;
$teal-500: #0096ad;
$orange-400: #ff9466;
$orange-450: #f4704c;
$orange-500: #f27052;
$orange-600: #d7583a;

View File

@@ -14,5 +14,10 @@
.more-controls {
text-align: center;
.spinner {
height: 2.25em;
margin-right: 0.5em;
}
}
}

View File

@@ -115,3 +115,14 @@
height: 36px;
}
}
.links {
.button {
padding: 1.125rem 0 1.1875rem;
width: 210px;
@media all and (max-width: 480px) {
width: 100%;
}
}
}

View File

@@ -64,13 +64,13 @@
.button.primary, button.primary {
font-family: $body-font;
background: $clr-brick;
background: $orange-450;
color: white;
}
.button.primary:hover, .button.primary:active, .button.primary:focus, button.primary:hover, button.primary:active, button.primary:focus {
background: $clr-brick-bright;
text-shadow: 0 1px 0 $clr-brick;
background: $orange-400;
text-shadow: 0 1px 0 $orange-450;
}
button.success, .button.success {

View File

@@ -17,8 +17,9 @@ module Admin
respond_to do |format|
format.html
format.json do
tag_rule_mapping = TagRule.mapping_for(Enterprise.where(id: params[:enterprise_id]))
render_as_json @collection, tag_rule_mapping: tag_rule_mapping
render_as_json @collection,
tag_rule_mapping: tag_rule_mapping,
customer_tags: customer_tags_by_id
end
end
end
@@ -64,8 +65,13 @@ module Admin
def collection
return Customer.where("1=0") unless json_request? && params[:enterprise_id].present?
enterprise = Enterprise.managed_by(spree_current_user).find_by(id: params[:enterprise_id])
Customer.of(enterprise)
Customer.of(managed_enterprise_id).
includes(:bill_address, :ship_address, user: :credit_cards)
end
def managed_enterprise_id
@managed_enterprise_id ||= Enterprise.managed_by(spree_current_user).
select('enterprises.id').find_by(id: params[:enterprise_id])
end
def load_managed_shops
@@ -93,5 +99,28 @@ module Admin
def permitted_resource_params
customer_params
end
def tag_rule_mapping
TagRule.mapping_for(Enterprise.where(id: managed_enterprise_id))
end
# Fetches tags for all customers of the enterprise and returns a hash indexed by customer_id
def customer_tags_by_id
customer_tags = ::ActsAsTaggableOn::Tag.
joins(:taggings).
includes(:taggings).
where(taggings:
{ taggable_type: 'Customer',
taggable_id: Customer.of(managed_enterprise_id),
context: 'tags' })
customer_tags.each_with_object({}) do |tag, indexed_hash|
tag.taggings.each do |tagging|
customer_id = tagging.taggable_id
indexed_hash[customer_id] ||= []
indexed_hash[customer_id] << tag.name
end
end
end
end
end

View File

@@ -16,7 +16,7 @@ module Admin
render_as_json @collection,
ams_prefix: params[:ams_prefix],
current_user: spree_current_user,
subscriptions_count: SubscriptionsCount.new(@collection)
subscriptions_count: OrderManagement::Subscriptions::Count.new(@collection)
end
end
end
@@ -74,7 +74,7 @@ module Admin
render_as_json @order_cycles,
ams_prefix: 'index',
current_user: spree_current_user,
subscriptions_count: SubscriptionsCount.new(@collection)
subscriptions_count: OrderManagement::Subscriptions::Count.new(@collection)
else
order_cycle = order_cycle_set.collection.find{ |oc| oc.errors.present? }
render json: { errors: order_cycle.errors.full_messages }, status: :unprocessable_entity

View File

@@ -9,25 +9,19 @@ module Admin
def cancel
if @proxy_order.cancel
respond_with(@proxy_order) do |format|
format.json { render_as_json @proxy_order }
end
render_as_json @proxy_order
else
respond_with(@proxy_order) do |format|
format.json { render json: { errors: [t('admin.proxy_orders.cancel.could_not_cancel_the_order')] }, status: :unprocessable_entity }
end
render json: { errors: [t('admin.proxy_orders.cancel.could_not_cancel_the_order')] },
status: :unprocessable_entity
end
end
def resume
if @proxy_order.resume
respond_with(@proxy_order) do |format|
format.json { render_as_json @proxy_order }
end
render_as_json @proxy_order
else
respond_with(@proxy_order) do |format|
format.json { render json: { errors: [t('admin.proxy_orders.resume.could_not_resume_the_order')] }, status: :unprocessable_entity }
end
render json: { errors: [t('admin.proxy_orders.resume.could_not_resume_the_order')] },
status: :unprocessable_entity
end
end
end

View File

@@ -1,5 +1,5 @@
require 'open_food_network/permissions'
require 'open_food_network/proxy_order_syncer'
require 'order_management/subscriptions/proxy_order_syncer'
module Admin
class SchedulesController < ResourceController
@@ -90,7 +90,7 @@ module Admin
return unless removed_ids.any? || new_ids.any?
subscriptions = Subscription.where(schedule_id: @schedule)
syncer = OpenFoodNetwork::ProxyOrderSyncer.new(subscriptions)
syncer = OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions)
syncer.sync!
end

View File

@@ -56,7 +56,7 @@ module Admin
end
def variant_if_eligible(variant_id)
SubscriptionVariantsService.eligible_variants(@shop).find_by(id: variant_id)
OrderManagement::Subscriptions::VariantsList.eligible_variants(@shop).find_by(id: variant_id)
end
def subscription_line_item_params

View File

@@ -1,5 +1,4 @@
require 'open_food_network/permissions'
require 'open_food_network/proxy_order_syncer'
module Admin
class SubscriptionsController < ResourceController
@@ -65,7 +64,7 @@ module Admin
private
def save_form_and_render(render_issues = true)
form = SubscriptionForm.new(@subscription, subscription_params)
form = OrderManagement::Subscriptions::Form.new(@subscription, subscription_params)
unless form.save
render json: { errors: form.json_errors }, status: :unprocessable_entity
return

View File

@@ -74,10 +74,10 @@ module Admin
def collection
@variant_overrides = VariantOverride.
includes(:variant).
includes(variant: :product).
for_hubs(params[:hub_id] || @hubs).
references(:variant)
@variant_overrides.select { |vo| vo.variant.present? }
references(:variant).
select { |vo| vo.variant.present? }
end
def collection_actions

View File

@@ -5,7 +5,6 @@ module Api
before_filter :override_sells, only: [:create, :update]
before_filter :override_visible, only: [:create, :update]
respond_to :json
skip_authorization_check only: [:shopfront]
def create
authorize! :create, Enterprise
@@ -46,12 +45,6 @@ module Api
end
end
def shopfront
enterprise = Enterprise.find_by(id: params[:id])
render text: Api::EnterpriseShopfrontSerializer.new(enterprise).to_json, status: :ok
end
private
def override_owner

View File

@@ -69,12 +69,12 @@ module Api
end
def overridable
producers = OpenFoodNetwork::Permissions.new(current_api_user).
variant_override_producers.by_name
producer_ids = OpenFoodNetwork::Permissions.new(current_api_user).
variant_override_producers.by_name.select('enterprises.id')
@products = paged_products_for_producers producers
@products = paged_products_for_producers producer_ids
render_paged_products @products
render_paged_products @products, ::Api::Admin::ProductSimpleSerializer
end
# POST /api/products/:product_id/clone
@@ -118,19 +118,20 @@ module Api
]
end
def paged_products_for_producers(producers)
def paged_products_for_producers(producer_ids)
Spree::Product.where(nil).
merge(product_scope).
where(supplier_id: producers).
includes(variants: [:product, :default_price, :stock_items]).
where(supplier_id: producer_ids).
by_producer.by_name.
ransack(params[:q]).result.
page(params[:page]).per(params[:per_page])
end
def render_paged_products(products)
def render_paged_products(products, product_serializer = ::Api::Admin::ProductSerializer)
serializer = ActiveModel::ArraySerializer.new(
products,
each_serializer: ::Api::Admin::ProductSerializer
each_serializer: product_serializer
)
render text: {

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
module Api
class ShopsController < BaseController
respond_to :json
skip_authorization_check only: [:show, :closed_shops]
def show
enterprise = Enterprise.find_by_id(params[:id])
render text: Api::EnterpriseShopfrontSerializer.new(enterprise).to_json, status: :ok
end
def closed_shops
@active_distributor_ids = []
@earliest_closing_times = []
serialized_closed_shops = ActiveModel::ArraySerializer.new(
ShopsListService.new.closed_shops,
each_serializer: Api::EnterpriseSerializer,
data: OpenFoodNetwork::EnterpriseInjectionData.new
)
render json: serialized_closed_shops
end
end
end

View File

@@ -14,10 +14,6 @@ class BaseController < ApplicationController
helper 'spree/base'
# Spree::Core::ControllerHelpers declares helper_method get_taxonomies, so we need to
# include Spree::ProductsHelper so that method is available on the controller
include Spree::ProductsHelper
before_filter :set_locale
before_filter :check_order_cycle_expiry

View File

@@ -133,13 +133,6 @@ class CheckoutController < Spree::StoreController
@order.ship_address = finder.ship_address
end
def before_delivery
return if params[:order].present?
packages = @order.shipments.map(&:to_package)
@differentiator = Spree::Stock::Differentiator.new(@order, packages)
end
def before_payment
current_order.payments.destroy_all if request.put?
end
@@ -153,10 +146,12 @@ class CheckoutController < Spree::StoreController
end
def valid_payment_intent_provided?
params["payment_intent"]&.starts_with?("pi_") &&
@order.state == "payment" &&
@order.payments.last.state == "pending" &&
@order.payments.last.response_code == params["payment_intent"]
return false unless params["payment_intent"]&.starts_with?("pi_")
last_payment = OrderPaymentFinder.new(@order).last_payment
@order.state == "payment" &&
last_payment&.state == "pending" &&
last_payment&.response_code == params["payment_intent"]
end
def handle_redirect_from_stripe

View File

@@ -4,13 +4,6 @@ class ShopsController < BaseController
before_filter :enable_embedded_shopfront
def index
@enterprises = Enterprise
.activated
.visible
.is_distributor
.includes(address: [:state, :country])
.includes(:properties)
.includes(supplied_products: :properties)
.all
@enterprises = ShopsListService.new.open_shops
end
end

View File

@@ -29,6 +29,8 @@ module Spree
return
end
authorize_stripe_sca_payment
if @order.completed?
@payment.process!
flash[:success] = flash_message_for(@payment, :successfully_created)
@@ -93,7 +95,7 @@ module Spree
available(:back_end).
select{ |pm| pm.has_distributor? @order.distributor }
@payment_method = if @payment && @payment.payment_method
@payment_method = if @payment&.payment_method
@payment.payment_method
else
@payment_methods.first
@@ -124,6 +126,13 @@ module Spree
def load_payment
@payment = Payment.find(params[:id])
end
def authorize_stripe_sca_payment
return unless @payment.payment_method.class == Spree::Gateway::StripeSCA
@payment.authorize!
raise Spree::Core::GatewayError, I18n.t('authorization_failure') unless @payment.pending?
end
end
end
end

View File

@@ -1,66 +0,0 @@
module Spree
module Admin
module Reports
class EnterpriseFeeSummariesController < BaseController
before_filter :load_report_parameters
before_filter :load_permissions
def new; end
def create
return respond_to_invalid_parameters unless @report_parameters.valid?
@report_parameters.authorize!(@permissions)
@report = report_klass::ReportService.new(@permissions, @report_parameters)
renderer.render(self)
rescue ::Reports::Authorizer::ParameterNotAllowedError => e
flash[:error] = e.message
render_report_form
end
private
def respond_to_invalid_parameters
flash[:error] = I18n.t("invalid_filter_parameters", scope: i18n_scope)
render_report_form
end
def i18n_scope
"order_management.reports.enterprise_fee_summary"
end
def render_report_form
render action: :new
end
def report_klass
OrderManagement::Reports::EnterpriseFeeSummary
end
def load_report_parameters
@report_parameters = report_klass::Parameters.new(params[:report] || {})
end
def load_permissions
@permissions = report_klass::Permissions.new(spree_current_user)
end
def report_renderer_klass
case params[:report_format]
when "csv"
report_klass::Renderers::CsvRenderer
when nil, "", "html"
report_klass::Renderers::HtmlRenderer
else
raise Reports::UnsupportedReportFormatException
end
end
def renderer
@renderer ||= report_renderer_klass.new(@report)
end
end
end
end
end

View File

@@ -298,11 +298,20 @@ module Spree
end
def url_for_report(report)
public_send("#{report}_admin_reports_url".to_sym)
if report_in_order_management_engine?(report)
main_app.public_send("new_order_management_reports_#{report}_url".to_sym)
else
public_send("#{report}_admin_reports_url".to_sym)
end
rescue NoMethodError
url_for([:new, :admin, :reports, report.to_s.singularize])
end
# List of reports that have been moved to the Order Management engine
def report_in_order_management_engine?(report)
report == :enterprise_fee_summary
end
def timestamp
Time.zone.now.strftime("%Y%m%d")
end

View File

@@ -3,14 +3,6 @@ module Spree
class TaxonsController < Spree::Admin::BaseController
respond_to :html, :json, :js
def search
@taxons = if params[:ids]
Spree::Taxon.where(id: params[:ids].split(','))
else
Spree::Taxon.limit(20).search(name_cont: params[:q]).result
end
end
def create
@taxonomy = Taxonomy.find(params[:taxonomy_id])
@taxon = @taxonomy.taxons.build(params[:taxon])

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
module OrderHelper
def last_payment_method(order)
OrderPaymentFinder.new(order).last_payment&.payment_method
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module Spree
module ProductsHelper
def product_has_variant_unit_option_type?(product)
product.option_types.any? { |option_type| variant_unit_option_type? option_type }
end
def variant_unit_option_type?(option_type)
Spree::Product.all_variant_unit_option_types.include? option_type
end
end
end

View File

@@ -1,23 +0,0 @@
module Spree
ProductsHelper.class_eval do
# Return the price of the variant, overriding sprees price diff capability.
# This will allways return the variant price as if the show_variant_full_price is set.
def variant_price_diff(variant)
"(#{Spree::Money.new(variant.price)})"
end
def product_has_variant_unit_option_type?(product)
product.option_types.any? { |option_type| variant_unit_option_type? option_type }
end
def variant_unit_option_type?(option_type)
Spree::Product.all_variant_unit_option_types.include? option_type
end
def product_variant_unit_options
[[I18n.t(:weight), 'weight'],
[I18n.t(:volume), 'volume'],
[I18n.t(:items), 'items']]
end
end
end

View File

@@ -1,17 +1,9 @@
require 'open_food_network/subscription_payment_updater'
require 'open_food_network/subscription_summarizer'
require 'order_management/subscriptions/summarizer'
# Confirms orders of unconfirmed proxy orders in recently closed Order Cycles
class SubscriptionConfirmJob
def perform
ids = proxy_orders.pluck(:id)
proxy_orders.update_all(confirmed_at: Time.zone.now)
ProxyOrder.where(id: ids).each do |proxy_order|
Rails.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
@order = proxy_order.order
process!
end
send_confirmation_summary_emails
confirm_proxy_orders!
end
private
@@ -20,10 +12,26 @@ class SubscriptionConfirmJob
delegate :record_and_log_error, :send_confirmation_summary_emails, to: :summarizer
def summarizer
@summarizer ||= OpenFoodNetwork::SubscriptionSummarizer.new
@summarizer ||= OrderManagement::Subscriptions::Summarizer.new
end
def proxy_orders
def confirm_proxy_orders!
# Fetch all unconfirmed proxy orders
unconfirmed_proxy_orders_ids = unconfirmed_proxy_orders.pluck(:id)
# Mark these proxy orders as confirmed
unconfirmed_proxy_orders.update_all(confirmed_at: Time.zone.now)
# Confirm these proxy orders
ProxyOrder.where(id: unconfirmed_proxy_orders_ids).each do |proxy_order|
Rails.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
confirm_order!(proxy_order.order)
end
send_confirmation_summary_emails
end
def unconfirmed_proxy_orders
ProxyOrder.not_canceled.where('confirmed_at IS NULL AND placed_at IS NOT NULL')
.joins(:order_cycle).merge(recently_closed_order_cycles)
.joins(:order).merge(Spree::Order.complete.not_state('canceled'))
@@ -33,30 +41,55 @@ class SubscriptionConfirmJob
OrderCycle.closed.where('order_cycles.orders_close_at BETWEEN (?) AND (?) OR order_cycles.updated_at BETWEEN (?) AND (?)', 1.hour.ago, Time.zone.now, 1.hour.ago, Time.zone.now)
end
def process!
record_order(@order)
update_payment! if @order.payment_required?
return send_failed_payment_email if @order.errors.present?
# It sets up payments, processes payments and sends confirmation emails
def confirm_order!(order)
record_order(order)
@order.process_payments! if @order.payment_required?
return send_failed_payment_email if @order.errors.present?
send_confirm_email
if process_payment!(order)
send_confirmation_email(order)
else
send_failed_payment_email(order)
end
end
def update_payment!
OpenFoodNetwork::SubscriptionPaymentUpdater.new(@order).update!
def process_payment!(order)
return false if order.errors.present?
return true unless order.payment_required?
setup_payment!(order)
return false if order.errors.any?
authorize_payment!(order)
return false if order.errors.any?
order.process_payments!
return false if order.errors.any?
true
end
def send_confirm_email
@order.update!
record_success(@order)
SubscriptionMailer.confirmation_email(@order).deliver
def setup_payment!(order)
OrderManagement::Subscriptions::PaymentSetup.new(order).call!
return if order.errors.any?
OrderManagement::Subscriptions::StripePaymentSetup.new(order).call!
end
def send_failed_payment_email
@order.update!
record_and_log_error(:failed_payment, @order)
SubscriptionMailer.failed_payment_email(@order).deliver
def authorize_payment!(order)
return if order.subscription.payment_method.class != Spree::Gateway::StripeSCA
OrderManagement::Subscriptions::StripeScaPaymentAuthorize.new(order).call!
end
def send_confirmation_email(order)
order.update!
record_success(order)
SubscriptionMailer.confirmation_email(order).deliver
end
def send_failed_payment_email(order)
order.update!
record_and_log_error(:failed_payment, order)
SubscriptionMailer.failed_payment_email(order).deliver
end
end

View File

@@ -1,4 +1,4 @@
require 'open_food_network/subscription_summarizer'
require 'order_management/subscriptions/summarizer'
class SubscriptionPlacementJob
def perform
@@ -17,7 +17,7 @@ class SubscriptionPlacementJob
delegate :record_and_log_error, :send_placement_summary_emails, to: :summarizer
def summarizer
@summarizer ||= OpenFoodNetwork::SubscriptionSummarizer.new
@summarizer ||= OrderManagement::Subscriptions::Summarizer.new
end
def proxy_orders

View File

@@ -1,41 +1,58 @@
# frozen_string_literal: true
class ProducerMailer < Spree::BaseMailer
include I18nHelper
def order_cycle_report(producer, order_cycle)
@producer = producer
@coordinator = order_cycle.coordinator
@order_cycle = order_cycle
line_items = line_items_from(@order_cycle, @producer)
@grouped_line_items = line_items.group_by(&:product_and_full_name)
@receival_instructions = @order_cycle.receival_instructions_for @producer
@total = total_from_line_items(line_items)
@tax_total = tax_total_from_line_items(line_items)
I18n.with_locale valid_locale(@producer.owner) do
order_cycle_subject = I18n.t('producer_mailer.order_cycle.subject', producer: producer.name)
subject = "[#{Spree::Config.site_name}] #{order_cycle_subject}"
with_unscoped_products_and_variants do
load_data
return unless has_orders?(order_cycle, producer)
I18n.with_locale(owner_locale) do
return unless orders?(order_cycle, producer)
mail(
to: @producer.contact.email,
from: from_address,
subject: subject,
reply_to: @coordinator.contact.email,
cc: @coordinator.contact.email
)
mail(
to: @producer.contact.email,
from: from_address,
subject: subject,
reply_to: @coordinator.contact.email,
cc: @coordinator.contact.email
)
end
end
end
private
def has_orders?(order_cycle, producer)
def owner_locale
valid_locale(@producer.owner)
end
def load_data
@coordinator = @order_cycle.coordinator
line_items = line_items_from(@order_cycle, @producer)
@grouped_line_items = line_items.group_by(&:product_and_full_name)
@receival_instructions = @order_cycle.receival_instructions_for(@producer)
@total = total_from_line_items(line_items)
@tax_total = tax_total_from_line_items(line_items)
end
def subject
order_cycle_subject = I18n.t('producer_mailer.order_cycle.subject', producer: @producer.name)
"[#{Spree::Config.site_name}] #{order_cycle_subject}"
end
def orders?(order_cycle, producer)
line_items_from(order_cycle, producer).any?
end
def line_items_from(order_cycle, producer)
Spree::LineItem.
includes(variant: { option_values: :option_type }).
@line_items ||= Spree::LineItem.
includes(:option_values, variant: [:product, { option_values: :option_type }]).
from_order_cycle(order_cycle).
sorted_by_name_and_unit_value.
merge(Spree::Product.in_supplier(producer)).
@@ -49,4 +66,22 @@ class ProducerMailer < Spree::BaseMailer
def tax_total_from_line_items(line_items)
Spree::Money.new line_items.sum(&:included_tax)
end
# This hack makes ActiveRecord skip the default_scope (deleted_at IS NULL)
# when eager loading associations. Further details:
# https://github.com/rails/rails/issues/11036
def with_unscoped_products_and_variants
variant_default_scopes = Spree::Variant.default_scopes
product_default_scopes = Spree::Product.default_scopes
Spree::Variant.default_scopes = []
Spree::Product.default_scopes = []
return_value = yield
Spree::Variant.default_scopes = variant_default_scopes
Spree::Product.default_scopes = product_default_scopes
return_value
end
end

View File

@@ -2,6 +2,7 @@ Spree::OrderMailer.class_eval do
helper HtmlHelper
helper CheckoutHelper
helper SpreeCurrencyHelper
helper OrderHelper
include I18nHelper
def cancel_email(order_or_order_id, resend = false)

View File

@@ -1,6 +1,7 @@
class SubscriptionMailer < Spree::BaseMailer
helper CheckoutHelper
helper ShopMailHelper
helper OrderHelper
include I18nHelper
def confirmation_email(order)

View File

@@ -17,7 +17,7 @@ class OrderCycle < ActiveRecord::Base
has_many :distributors, -> { uniq }, source: :receiver, through: :cached_outgoing_exchanges
has_and_belongs_to_many :schedules, join_table: 'order_cycle_schedules'
has_paper_trail meta: { custom_data: :schedule_ids }
has_paper_trail meta: { custom_data: proc { |order_cycle| order_cycle.schedule_ids.to_s } }
attr_accessor :incoming_exchanges, :outgoing_exchanges

View File

@@ -4,18 +4,6 @@ class ProducerProperty < ActiveRecord::Base
default_scope { order("#{table_name}.position") }
scope :ever_sold_by, ->(shop) {
joins(producer: { supplied_products: { variants: { exchanges: :order_cycle } } }).
merge(Exchange.outgoing).
merge(Exchange.to_enterprise(shop)).
select('DISTINCT producer_properties.*')
}
scope :currently_sold_by, ->(shop) {
ever_sold_by(shop).
merge(OrderCycle.active)
}
def property_name
property.name if property
end

View File

@@ -1,7 +1,7 @@
class Schedule < ActiveRecord::Base
has_paper_trail meta: { custom_data: :order_cycle_ids }
has_and_belongs_to_many :order_cycles, join_table: 'order_cycle_schedules'
has_paper_trail meta: { custom_data: proc { |schedule| schedule.order_cycle_ids.to_s } }
has_many :coordinators, -> { uniq }, through: :order_cycles
validates :order_cycles, presence: true

View File

@@ -17,9 +17,9 @@ module Spree
order_amount = line_items_for(object).map { |x| x.price * x.quantity }.sum
if order_amount < min
cost = preferred_normal_amount.to_i
cost = preferred_normal_amount.to_f
elsif order_amount >= min
cost = preferred_discount_amount.to_i
cost = preferred_discount_amount.to_f
end
cost

View File

@@ -1,6 +0,0 @@
Spree::Money.class_eval do
# return the currency symbol (on it's own) for the current default currency
def self.currency_symbol
Money.new(0, Spree::Config[:currency]).symbol
end
end

View File

@@ -2,9 +2,14 @@ module Spree
class Property < ActiveRecord::Base
has_many :product_properties, dependent: :destroy
has_many :products, through: :product_properties
has_many :producer_properties
validates :name, :presentation, presence: true
scope :sorted, -> { order(:name) }
def property
self
end
end
end

View File

@@ -1,27 +0,0 @@
module Spree
Property.class_eval do
has_many :producer_properties
scope :applied_by, ->(enterprise) {
select('DISTINCT spree_properties.*').
joins(:product_properties).
where('spree_product_properties.product_id IN (?)', enterprise.supplied_product_ids)
}
scope :ever_sold_by, ->(shop) {
joins(products: { variants: { exchanges: :order_cycle } }).
merge(Exchange.outgoing).
merge(Exchange.to_enterprise(shop)).
select('DISTINCT spree_properties.*')
}
scope :currently_sold_by, ->(shop) {
ever_sold_by(shop).
merge(OrderCycle.active)
}
def property
self
end
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Spree
module Stock
class Quantifier
attr_reader :stock_items
def initialize(variant)
@variant = variant
@stock_items = fetch_stock_items
end
def total_on_hand
stock_items.sum(&:count_on_hand)
end
def backorderable?
stock_items.any?(&:backorderable)
end
def can_supply?(required)
total_on_hand >= required || backorderable?
end
private
def fetch_stock_items
# Don't re-fetch associated stock items from the DB if we've already eager-loaded them
return @variant.stock_items.to_a if @variant.stock_items.loaded?
Spree::StockItem.joins(:stock_location).
where(:variant_id => @variant, Spree::StockLocation.table_name => { active: true })
end
end
end
end

View File

@@ -104,7 +104,12 @@ module Spree
end
def default_card
credit_cards.where(is_default: true).first
# Don't re-fetch associated cards from the DB if they're already eager-loaded
if credit_cards.loaded?
credit_cards.to_a.find(&:is_default)
else
credit_cards.where(is_default: true).first
end
end
# Checks whether the specified user is a superadmin, with full control of the

View File

@@ -52,13 +52,23 @@ Spree::Variant.class_eval do
}
scope :visible_for, lambda { |enterprise|
joins(:inventory_items).where('inventory_items.enterprise_id = (?) AND inventory_items.visible = (?)', enterprise, true)
joins(:inventory_items).
where(
'inventory_items.enterprise_id = (?) AND inventory_items.visible = (?)',
enterprise,
true
)
}
scope :not_hidden_for, lambda { |enterprise|
return where("1=0") if enterprise.blank?
joins("LEFT OUTER JOIN (SELECT * from inventory_items WHERE enterprise_id = #{sanitize enterprise.andand.id}) AS o_inventory_items ON o_inventory_items.variant_id = spree_variants.id")
joins("
LEFT OUTER JOIN (SELECT *
FROM inventory_items
WHERE enterprise_id = #{sanitize enterprise.andand.id})
AS o_inventory_items
ON o_inventory_items.variant_id = spree_variants.id")
.where("o_inventory_items.id IS NULL OR o_inventory_items.visible = (?)", true)
}
@@ -67,7 +77,8 @@ Spree::Variant.class_eval do
scope :stockable_by, lambda { |enterprise|
return where("1=0") if enterprise.blank?
joins(:product).where(spree_products: { id: Spree::Product.stockable_by(enterprise).pluck(:id) })
joins(:product).
where(spree_products: { id: Spree::Product.stockable_by(enterprise).pluck(:id) })
}
# Define sope as class method to allow chaining with other scopes filtering id.
@@ -84,7 +95,19 @@ Spree::Variant.class_eval do
]
end
# We override in_stock? to avoid depending on the non-overridable method Spree::Stock::Quantifier.can_supply?
def self.active(currency = nil)
# "where(id:" is necessary so that the returned relation has no includes
# The relation without includes will not be readonly and allow updates on it
where("spree_variants.id in (?)", joins(:prices).
where(deleted_at: nil).
where('spree_prices.currency' =>
currency || Spree::Config[:currency]).
where('spree_prices.amount IS NOT NULL').
select("spree_variants.id"))
end
# We override in_stock? to avoid depending
# on the non-overridable method Spree::Stock::Quantifier.can_supply?
# VariantStock implements can_supply? itself which depends on overridable methods
def in_stock?(quantity = 1)
can_supply?(quantity)

View File

@@ -15,8 +15,10 @@ class VariantOverrideSet < ModelSet
tag_list.empty?
end
# Override of ModelSet method to allow us to check presence of a tag_list (which is not an attribute)
# This method will delete VariantOverrides that have no values (see deletable? above)
# If the user sets all values to nil in the UI the VO will be deleted from the DB
def collection_to_delete
# Override of ModelSet method to allow us to check presence of a tag_list (which is not an attribute)
deleted = []
collection.delete_if { |e| deleted << e if @delete_if.andand.call(e.attributes, e.tag_list) }
deleted

View File

@@ -6,7 +6,7 @@ class Api::Admin::CustomerSerializer < ActiveModel::Serializer
has_one :bill_address, serializer: Api::AddressSerializer
def tag_list
object.tag_list.join(",")
customer_tag_list.join(",")
end
def name
@@ -14,7 +14,7 @@ class Api::Admin::CustomerSerializer < ActiveModel::Serializer
end
def tags
object.tag_list.map do |tag|
customer_tag_list.map do |tag|
tag_rule_map = options[:tag_rule_mapping].andand[tag]
tag_rule_map || { text: tag, rules: nil }
end
@@ -25,4 +25,12 @@ class Api::Admin::CustomerSerializer < ActiveModel::Serializer
object.user.default_card.present?
end
private
def customer_tag_list
return object.tag_list unless options[:customer_tags]
options[:customer_tags].andand[object.id] || []
end
end

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module Api
module Admin
class ProductSimpleSerializer < ActiveModel::Serializer
attributes :id, :name, :producer_id
has_many :variants, key: :variants, serializer: Api::Admin::VariantSimpleSerializer
def producer_id
object.supplier_id
end
end
end
end

View File

@@ -13,9 +13,9 @@ module Api
end
def in_open_and_upcoming_order_cycles
SubscriptionVariantsService.in_open_and_upcoming_order_cycles?(option_or_assigned_shop,
option_or_assigned_schedule,
object.variant)
OrderManagement::Subscriptions::VariantsList.in_open_and_upcoming_order_cycles?(option_or_assigned_shop,
option_or_assigned_schedule,
object.variant)
end
private

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Api
module Admin
class VariantSimpleSerializer < ActiveModel::Serializer
attributes :id, :name, :import_date,
:options_text, :unit_value, :unit_description, :unit_to_display,
:display_as, :display_name, :name_to_display,
:price, :on_demand, :on_hand
has_many :variant_overrides
def name
if object.full_name.present?
"#{object.name} - #{object.full_name}"
else
object.name
end
end
def on_hand
return 0 if object.on_hand.nil?
object.on_hand
end
def price
object.price.nil? ? 0.to_f : object.price
end
end
end
end

View File

@@ -73,12 +73,16 @@ module Api
# This results in 3 queries per enterprise
def distributed_properties
return [] unless active
(distributed_product_properties + distributed_producer_properties).uniq do |property_object|
property_object.property.presentation
end
end
def distributed_product_properties
return [] unless active
properties = Spree::Property
.joins(products: { variants: { exchanges: :order_cycle } })
.merge(Exchange.outgoing)
@@ -91,6 +95,8 @@ module Api
end
def distributed_producer_properties
return [] unless active
properties = Spree::Property
.joins(
producer_properties: {

View File

@@ -12,11 +12,8 @@ module Checkout
def path
return unless stripe_payment_method?
payment = @order.pending_payments.last
return unless payment&.checkout?
payment.authorize!
raise unless payment.pending?
payment = OrderManagement::Subscriptions::StripeScaPaymentAuthorize.new(@order).call!
raise if @order.errors.any?
field_with_url(payment) if url?(field_with_url(payment))
end

View File

@@ -1,6 +1,6 @@
require 'open_food_network/permissions'
require 'open_food_network/proxy_order_syncer'
require 'open_food_network/order_cycle_form_applicator'
require 'order_management/subscriptions/proxy_order_syncer'
class OrderCycleForm
def initialize(order_cycle, order_cycle_params, user)
@@ -58,7 +58,7 @@ class OrderCycleForm
return unless schedule_ids?
return unless schedule_sync_required?
OpenFoodNetwork::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
end
def schedule_sync_required?

View File

@@ -1,14 +1,28 @@
# frozen_string_literal: true
module OrderPaymentFinder
def self.last_payment_method(order)
# `max_by` avoids additional database queries when payments are loaded
# already. There is usually only one payment and this shouldn't cause
# any overhead compared to `order(:created_at).last`. Using `last`
# without order is not deterministic.
#
# We are not using `updated_at` because all payments are touched when the
# order is updated and then all payments have the same `updated_at` value.
order.payments.max_by(&:created_at)&.payment_method
class OrderPaymentFinder
def initialize(order)
@order = order
end
def last_payment
last(@order.payments)
end
def last_pending_payment
last(@order.pending_payments)
end
private
# `max_by` avoids additional database queries when payments are loaded
# already. There is usually only one payment and this shouldn't cause
# any overhead compared to `order(:created_at).last`. Using `last`
# without order is not deterministic.
#
# We are not using `updated_at` because all payments are touched when the
# order is updated and then all payments have the same `updated_at` value.
def last(payments)
payments.max_by(&:created_at)
end
end

View File

@@ -40,7 +40,7 @@ class ProductsRenderer
end
def product_scoper
OpenFoodNetwork::ScopeProductToHub.new(distributor)
@product_scoper ||= OpenFoodNetwork::ScopeProductToHub.new(distributor)
end
def enterprise_fee_calculator

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
class ShopsListService
def open_shops
shops_list.ready_for_checkout.all
end
def closed_shops
shops_list.not_ready_for_checkout.all
end
private
def shops_list
Enterprise
.activated
.visible
.is_distributor
.includes(address: [:state, :country])
.includes(:properties)
.includes(supplied_products: :properties)
end
end

View File

@@ -1,63 +0,0 @@
require 'open_food_network/scope_variant_to_hub'
# Responsible for estimating prices and fees for subscriptions
# Used by SubscriptionForm as part of the create/update process
# The values calculated here are intended to be persisted in the db
class SubscriptionEstimator
def initialize(subscription)
@subscription = subscription
end
def estimate!
assign_price_estimates
assign_fee_estimates
end
private
attr_accessor :subscription
delegate :subscription_line_items, :shipping_method, :payment_method, :shop, to: :subscription
def assign_price_estimates
subscription_line_items.each do |item|
item.price_estimate =
price_estimate_for(item.variant, item.price_estimate_was)
end
end
def price_estimate_for(variant, fallback)
return fallback unless fee_calculator && variant
scoper.scope(variant)
fees = fee_calculator.indexed_fees_for(variant)
(variant.price + fees).to_d
end
def fee_calculator
return @fee_calculator unless @fee_calculator.nil?
next_oc = subscription.schedule.andand.current_or_next_order_cycle
return nil unless shop && next_oc
@fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(shop, next_oc)
end
def scoper
OpenFoodNetwork::ScopeVariantToHub.new(shop)
end
def assign_fee_estimates
subscription.shipping_fee_estimate = shipping_fee_estimate
subscription.payment_fee_estimate = payment_fee_estimate
end
def shipping_fee_estimate
shipping_method.calculator.compute(subscription)
end
def payment_fee_estimate
payment_method.calculator.compute(subscription)
end
end

View File

@@ -1,34 +0,0 @@
require 'open_food_network/proxy_order_syncer'
class SubscriptionForm
attr_accessor :subscription, :subscription_params, :order_update_issues, :validator, :order_syncer, :estimator
delegate :json_errors, :valid?, to: :validator
delegate :order_update_issues, to: :order_syncer
def initialize(subscription, subscription_params = {})
@subscription = subscription
@subscription_params = subscription_params
@estimator = SubscriptionEstimator.new(subscription)
@validator = SubscriptionValidator.new(subscription)
@order_syncer = OrderSyncer.new(subscription)
end
def save
subscription.assign_attributes(subscription_params)
return false unless valid?
subscription.transaction do
estimator.estimate!
proxy_order_syncer.sync!
order_syncer.sync!
subscription.save!
end
end
private
def proxy_order_syncer
OpenFoodNetwork::ProxyOrderSyncer.new(subscription)
end
end

View File

@@ -1,127 +0,0 @@
# Encapsulation of all of the validation logic required for subscriptions
# Public interface consists of #valid? method provided by ActiveModel::Validations
# and #json_errors which compiles a serializable hash of errors
class SubscriptionValidator
include ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :subscription
validates :shop, :customer, :schedule, :shipping_method, :payment_method, presence: true
validates :bill_address, :ship_address, :begins_at, presence: true
validate :shipping_method_allowed?
validate :payment_method_allowed?
validate :payment_method_type_allowed?
validate :ends_at_after_begins_at?
validate :customer_allowed?
validate :schedule_allowed?
validate :credit_card_ok?
validate :subscription_line_items_present?
validate :requested_variants_available?
delegate :shop, :customer, :schedule, :shipping_method, :payment_method, to: :subscription
delegate :bill_address, :ship_address, :begins_at, :ends_at, to: :subscription
delegate :subscription_line_items, to: :subscription
def initialize(subscription)
@subscription = subscription
end
def json_errors
errors.messages.each_with_object({}) do |(k, v), errors|
errors[k] = v.map { |msg| build_msg_from(k, msg) }
end
end
private
def shipping_method_allowed?
return unless shipping_method
return if shipping_method.distributors.include?(shop)
errors.add(:shipping_method, :not_available_to_shop, shop: shop.name)
end
def payment_method_allowed?
return unless payment_method
return if payment_method.distributors.include?(shop)
errors.add(:payment_method, :not_available_to_shop, shop: shop.name)
end
def payment_method_type_allowed?
return unless payment_method
return if Subscription::ALLOWED_PAYMENT_METHOD_TYPES.include? payment_method.type
errors.add(:payment_method, :invalid_type)
end
def ends_at_after_begins_at?
# Only validates ends_at if it is present
return if begins_at.blank? || ends_at.blank?
return if ends_at > begins_at
errors.add(:ends_at, :after_begins_at)
end
def customer_allowed?
return unless customer
return if customer.enterprise == shop
errors.add(:customer, :does_not_belong_to_shop, shop: shop.name)
end
def schedule_allowed?
return unless schedule
return if schedule.coordinators.include?(shop)
errors.add(:schedule, :not_coordinated_by_shop, shop: shop.name)
end
def credit_card_ok?
return unless customer && payment_method
return unless stripe_payment_method?(payment_method)
return errors.add(:payment_method, :charges_not_allowed) unless customer.allow_charges
return if customer.user.andand.default_card.present?
errors.add(:payment_method, :no_default_card)
end
def stripe_payment_method?(payment_method)
payment_method.type == "Spree::Gateway::StripeConnect" ||
payment_method.type == "Spree::Gateway::StripeSCA"
end
def subscription_line_items_present?
return if subscription_line_items.reject(&:marked_for_destruction?).any?
errors.add(:subscription_line_items, :at_least_one_product)
end
def requested_variants_available?
subscription_line_items.each { |sli| verify_availability_of(sli.variant) }
end
def verify_availability_of(variant)
return if available_variant_ids.include? variant.id
name = "#{variant.product.name} - #{variant.full_name}"
errors.add(:subscription_line_items, :not_available, name: name)
end
def available_variant_ids
return @available_variant_ids if @available_variant_ids.present?
subscription_variant_ids = subscription_line_items.map(&:variant_id)
@available_variant_ids = SubscriptionVariantsService.eligible_variants(shop)
.where(id: subscription_variant_ids).pluck(:id)
end
def build_msg_from(k, msg)
return msg[1..-1] if msg.starts_with?("^")
errors.full_message(k, msg)
end
end

View File

@@ -1,39 +0,0 @@
class SubscriptionVariantsService
# Includes the following variants:
# - Variants of permitted producers
# - Variants of hub
# - Variants that are in outgoing exchanges where the hub is receiver
def self.eligible_variants(distributor)
variant_conditions = ["spree_products.supplier_id IN (?)", permitted_producer_ids(distributor)]
exchange_variant_ids = outgoing_exchange_variant_ids(distributor)
if exchange_variant_ids.present?
variant_conditions[0] << " OR spree_variants.id IN (?)"
variant_conditions << exchange_variant_ids
end
Spree::Variant.joins(:product).where(is_master: false).where(*variant_conditions)
end
def self.in_open_and_upcoming_order_cycles?(distributor, schedule, variant)
scope = ExchangeVariant.joins(exchange: { order_cycle: :schedules })
.where(variant_id: variant, exchanges: { incoming: false, receiver_id: distributor })
.merge(OrderCycle.not_closed)
scope = scope.where(schedules: { id: schedule })
scope.any?
end
def self.permitted_producer_ids(distributor)
other_permitted_producer_ids = EnterpriseRelationship.joins(:parent)
.permitting(distributor.id).with_permission(:add_to_order_cycle)
.merge(Enterprise.is_primary_producer)
.pluck(:parent_id)
other_permitted_producer_ids | [distributor.id]
end
def self.outgoing_exchange_variant_ids(distributor)
ExchangeVariant.select("DISTINCT exchange_variants.variant_id").joins(:exchange)
.where(exchanges: { incoming: false, receiver_id: distributor.id })
.pluck(:variant_id)
end
end

View File

@@ -1,20 +0,0 @@
class SubscriptionsCount
def initialize(order_cycles)
@order_cycles = order_cycles
end
def for(order_cycle_id)
active[order_cycle_id] || 0
end
private
attr_accessor :order_cycles
def active
return @active unless @active.nil?
return @active = [] if order_cycles.blank?
@active ||= ProxyOrder.not_canceled.group(:order_cycle_id).where(order_cycle_id: order_cycles).count
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
# Produces mappings of variant overrides by distributor id and variant id
# The primary use case for data structured in this way is for injection into
# the initializer of the OpenFoodNetwork::ScopeVariantToHub class
class VariantOverridesIndexed
def initialize(variant_ids, distributor_ids)
@variant_ids = variant_ids
@distributor_ids = distributor_ids
end
def indexed
scoped_variant_overrides.each_with_object(hash_of_hashes) do |variant_override, indexed|
indexed[variant_override.hub_id][variant_override.variant] = variant_override
end
end
private
attr_reader :variant_ids, :distributor_ids
def scoped_variant_overrides
VariantOverride
.joins(:variant)
.preload(:variant)
.where(
hub_id: distributor_ids,
variant_id: variant_ids,
)
end
def hash_of_hashes
Hash.new { |hash, key| hash[key] = {} }
end
end

View File

@@ -8,10 +8,13 @@ class VariantsStockLevels
variant_stock_levels = variant_stock_levels(order.line_items)
order_variant_ids = variant_stock_levels.keys
missing_variant_ids = requested_variant_ids - order_variant_ids
missing_variant_ids.each do |variant_id|
variant = scoped_variant(order.distributor, Spree::Variant.find(variant_id))
variant_stock_levels[variant_id] = { quantity: 0, max_quantity: 0, on_hand: variant.on_hand, on_demand: variant.on_demand }
missing_variants = Spree::Variant.includes(:stock_items).
where(id: (requested_variant_ids - order_variant_ids))
missing_variants.each do |missing_variant|
variant = scoped_variant(order.distributor, missing_variant)
variant_stock_levels[variant.id] =
{ quantity: 0, max_quantity: 0, on_hand: variant.on_hand, on_demand: variant.on_demand }
end
variant_stock_levels

View File

@@ -6,6 +6,7 @@
%td
%ul
%li{"ng-repeat" => "permission in enterprise_relationship.permissions"}
to {{ EnterpriseRelationships.permission_presentation(permission.name) }}
= t 'admin_enterprise_relationships_to'
{{ EnterpriseRelationships.permission_presentation(permission.name) }}
%td.actions
%a.delete-enterprise-relationship.icon-trash.no-text{'ng-click' => 'delete(enterprise_relationship)'}

View File

@@ -13,7 +13,8 @@
%div{"ng-repeat" => "permission in EnterpriseRelationships.all_permissions"}
%label
%input{type: "checkbox", "ng-model" => "permissions[permission]"}
to {{ EnterpriseRelationships.permission_presentation(permission) }}
= t 'admin_enterprise_relationships_to'
{{ EnterpriseRelationships.permission_presentation(permission) }}
%td.actions
%input{type: "button", value: t(:admin_enterprise_relationships_button_create), "ng-click" => "create()"}
.errors {{ EnterpriseRelationships.create_errors }}

View File

@@ -31,5 +31,4 @@
//= f.submit "Purchase", class: "button", "ofn-focus" => "accordion['payment']"
%a.button.secondary{href: main_app.cart_url}
%i.ofn-i_008-caret-left
= t :checkout_back_to_cart

View File

@@ -26,8 +26,11 @@
%a{href: "", "ng-click" => "showDistanceMatches()"}
= t :hubs_distance_filter, location: "{{ nameMatchesFiltered[0].name }}"
.more-controls
%a.button{href: "", ng: {click: "showClosedShops()", show: "!show_closed"}}
= t '.show_closed_shops'
%a.button{href: "", ng: {click: "hideClosedShops()", show: "show_closed"}}
= t '.hide_closed_shops'
%img.spinner.text-center{ng: {show: "closed_shops_loading"}, src: "/assets/spinning-circles.svg" }
%span{ng: {if: "!show_closed", cloak: true}}
%a.button{href: "", ng: {click: "showClosedShops()"}}
= t '.show_closed_shops'
%span{ng: {if: "show_closed", cloak: true}}
%a.button{href: "", ng: {click: "hideClosedShops()"}}
= t '.hide_closed_shops'
%a.button{href: main_app.map_path}= t '.show_on_map'

View File

@@ -1,5 +1,6 @@
- shipment.manifest.each do |item|
- line_item = order.find_line_item_by_variant(item.variant)
- break if line_item.blank?
%tr.stock-item{ "data-item-quantity" => "#{item.quantity}" }
%td.item-image

View File

@@ -1,4 +1,5 @@
.three.columns.omega{ "ng-if" => "product.variant_unit_with_scale != 'items'" }
= f.field_container :display_as do
= f.label :product_display_as, t('.display_as')
%span.required *
%input#product_display_as.fullwidth{name: "product[display_as]", placeholder: "{{ placeholder_text }}", type: "text"}

View File

@@ -16,11 +16,6 @@
= sanitize(@product.description)
= f.error_message_on :description
= f.field_container :taxons do
= f.label :taxon_ids, t(:taxons)
%br
= f.hidden_field :taxon_ids, :value => @product.taxon_ids.join(',')
.right.four.columns.omega
.variant_units_form{ 'ng-app' => 'admin.products', 'ng-controller' => 'editUnitsCtrl' }

View File

@@ -38,6 +38,7 @@
.three.columns.omega{ 'ng-show' => "product.variant_unit_with_scale == 'items'" }
= f.field_container :unit_name do
= f.label :product_variant_unit_name, t(".unit_name")
%span.required *
%input.fullwidth{ id: 'product_variant_unit_name','ng-model' => 'product.variant_unit_name', :name => 'product[variant_unit_name]', :placeholder => t('admin.products.unit_name_placeholder'), :type => 'text' }
.twelve.columns.alpha
.six.columns.alpha

View File

@@ -1,4 +0,0 @@
object false
child(@taxons => :taxons) do
attributes :name, :pretty_name, :id
end

View File

@@ -8,7 +8,7 @@
= t :email_payment_summary
%h4
= t :email_payment_method
%strong= OrderPaymentFinder.last_payment_method(@order)&.name
%strong= last_payment_method(@order)&.name
%p
%em= OrderPaymentFinder.last_payment_method(@order)&.description
%em= last_payment_method(@order)&.description
%p &nbsp;

View File

@@ -1,9 +1,5 @@
.row.links{'data-hook' => "cart_buttons"}
.columns.large-8{"data-hook" => ""}
%a.button.large.secondary{href: current_shop_products_path}
%i.ofn-i_008-caret-left
= t :orders_edit_continue
.columns.large-4.text-right
%a#checkout-link.button.large.primary{href: main_app.checkout_path}
= t :orders_edit_checkout
%i.ofn-i_007-caret-right
%a.button.large.secondary{href: current_shop_products_path}
= t :orders_edit_continue
%a#checkout-link.button.large.primary.right{href: main_app.checkout_path}
= t :orders_edit_checkout

View File

@@ -3,11 +3,9 @@
- if current_order.nil? || current_order.distributor.nil? || current_order.distributor == @order.distributor
- if current_order&.line_items.present?
= link_to main_app.cart_path, :class => "button expand" do
%i.ofn-i_008-caret-left
= t(:order_back_to_cart)
- else
= link_to "#{main_app.enterprise_shop_path(@order.distributor)}#/shop", class: "button expand" do
%i.ofn-i_008-caret-left
= t(:order_back_to_store)
- else
&nbsp;

View File

@@ -13,9 +13,9 @@
.pad
.text-big
= t :order_payment
%strong= OrderPaymentFinder.last_payment_method(order)&.name
%strong= last_payment_method(order)&.name
%p.text-small.text-skinny.pre-line
%em= OrderPaymentFinder.last_payment_method(order)&.description
%em= last_payment_method(order)&.description
.order-summary.text-small
%strong

View File

@@ -13,7 +13,8 @@
%tr.order-row
%td.order1
%a{"ng-href" => "{{::order.path}}", "ng-bind" => "::order.number"}
%td.order2{"ng-bind" => "::Orders.shopsByID[order.shop_id].name"}
%td.order2
%a{"ng-href" => "{{::Orders.shopsByID[order.shop_id].hash}}#{main_app.shop_path}", "ng-bind" => "::Orders.shopsByID[order.shop_id].name"}
%td.order3.show-for-large-up{"ng-bind" => "::order.changes_allowed_until"}
%td.order4.show-for-large-up{"ng-bind" => "::order.item_count"}
%td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","ng-bind" => "::order.total | localizeCurrency"}

View File

@@ -13,7 +13,8 @@
%tr.order-row
%td.order1
%a{"ng-href" => "{{::order.path}}", "ng-bind" => "::order.number"}
%td.order2{"ng-bind" => "::Orders.shopsByID[order.shop_id].name"}
%td.order2
%a{"ng-href" => "{{::Orders.shopsByID[order.shop_id].hash}}#{main_app.shop_path}", "ng-bind" => "::Orders.shopsByID[order.shop_id].name"}
%td.order3.show-for-large-up{"ng-bind" => "::order.completed_at"}
%td.order4.show-for-large-up{"ng-bind" => "::order.item_count"}
%td.order5.text-right{"ng-class" => "{'debit': order.payment_state != 'paid', 'credit': order.payment_state == 'paid'}","ng-bind" => "::order.total | localizeCurrency"}

View File

@@ -1,4 +1,17 @@
# This file is used by Rack-based servers to start the application.
if ENV.fetch('KILL_UNICORNS', false) && ['production', 'staging'].include?(ENV['RAILS_ENV'])
# Gracefully restart individual unicorn workers if they have:
# - performed between 25000 and 30000 requests
# - grown in memory usage to between 700 and 850 MB
require 'unicorn/worker_killer'
use Unicorn::WorkerKiller::MaxRequests,
ENV.fetch('UWK_REQS_MIN', 25_000).to_i,
ENV.fetch('UWK_REQS_MAX', 30_000).to_i
use Unicorn::WorkerKiller::Oom,
( ENV.fetch('UWK_MEM_MIN', 700).to_i * (1024**2) ),
( ENV.fetch('UWK_MEM_MAX', 850).to_i * (1024**2) )
end
require ::File.expand_path('../config/environment', __FILE__)
run Openfoodnetwork::Application

View File

@@ -1,7 +1,7 @@
defaults: &defaults
adapter: postgresql
encoding: unicode
pool: 5
pool: <%= ENV.fetch('OFN_DB_POOL', 5) %>
host: <%= ENV.fetch('OFN_DB_HOST', 'localhost') %>
username: <%= ENV.fetch('OFN_DB_USERNAME', 'ofn') %>
password: <%= ENV.fetch('OFN_DB_PASSWORD', 'f00d') %>

View File

@@ -4,5 +4,6 @@ if ENV['DATADOG_RAILS_APM']
c.use :delayed_job, service_name: 'delayed_job'
c.use :dalli, service_name: 'memcached'
c.analytics_enabled = true
c.runtime_metrics_enabled = true
end
end

View File

@@ -55,7 +55,7 @@ ar:
messages:
inclusion: "غير مدرجة في القائمة"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^ الرجاء إضافة منتج واحد على الأقل"
@@ -2725,6 +2725,8 @@ ar:
location: "الموقع"
count_on_hand: "الاعتماد على المتوفر"
quantity: "الكمية"
on_demand: "على الطلب"
on_hand: "متوفر"
package_from: "التعبئة من"
item_description: "وصف السلعة"
price: "السعر"

View File

@@ -31,6 +31,10 @@ ca:
taken: "Ja hi ha un compte per a aquest correu electrònic. Si us plau, inicia sessió o restableix la contrasenya."
spree/order:
no_card: No hi ha targetes de crèdit autoritzades disponibles per carregar
spree/credit_card:
attributes:
base:
card_expired: "Ha expirat"
order_cycle:
attributes:
orders_close_at:
@@ -55,7 +59,7 @@ ca:
messages:
inclusion: "no està inclòs a la llista"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Afegiu com a mínim un producte"
@@ -665,9 +669,9 @@ ca:
primary_producer: Productora principal?
primary_producer_tip: Selecciona "Productora" si ets productora principal d'aliments.
producer: Productora
any: Cap
none: No productora
own: Propi
any: Qualsevol
none: Cap
own: Propis
sells: Ven
sells_tip: "Cap: l'organització no ven als clients directament. <br /> Propietari: l'organització ven productes propis als clients. <br /> Qualsevol: l'organització pot vendre productes propis o d'altres empreses. <br />"
visible_in_search: Visible a la cerca?
@@ -1452,13 +1456,13 @@ ca:
email_payment_summary: Resum del pagament
email_payment_method: "Pagament a través de:"
email_so_placement_intro_html: "Tens una nova comanda amb <strong> %{distributor} </ strong>"
email_so_placement_details_html: "Aquests són els detalls de la comanda de <strong> %{distributor} </ strong>:"
email_so_placement_details_html: "Aquests són els detalls de la comanda de <strong>%{distributor}</strong> :"
email_so_placement_changes: "Malauradament, no tots els productes que has demanat estaven disponibles. Les quantitats originals que has sol·licitat apareixen ratllades a sota."
email_so_payment_success_intro_html: "S'ha processat un pagament automàtic per a la vostra comanda des de <strong> %{distributor} </ strong>."
email_so_placement_explainer_html: "Aquesta comanda s'ha creat automàticament per tu."
email_so_edit_true_html: "Potd <a href='%{order_url}'> fer canvis </ a> fins que les comandes es tanquin el %{orders_close_at}."
email_so_edit_true_html: "Pots <a href='%{order_url}'>fer canvis</a> fins que les comandes es tanquin el %{orders_close_at}."
email_so_edit_false_html: "Pots <a href='%{order_url}'> veure detalls d'aquesta comanda </a> en qualsevol moment."
email_so_contact_distributor_html: "Si tens alguna pregunta pots contactar amb <strong> %{distributor} </ strong> a través d'%{email}."
email_so_contact_distributor_html: "Si tens alguna pregunta pots contactar amb <strong>%{distributor}</strong> a través d'%{email}."
email_so_contact_distributor_to_change_order_html: "Aquesta comanda s'ha creat automàticament per a vostè. Podeu fer canvis fins que les comandes es tanquin a %{orders_close_at} contactant a <strong> %{distributor} </ strong> a través d'%{email}."
email_so_confirmation_intro_html: "La teva comanda amb <strong> %{distributor} </ strong> ja està confirmada"
email_so_confirmation_explainer_html: "Vas realitzar aquesta comanda automàticament i ara s'ha finalitzat."
@@ -2762,6 +2766,8 @@ ca:
location: "Ubicació"
count_on_hand: "Compte disponible"
quantity: "Quantitat"
on_demand: "Sota demanda"
on_hand: "Disponibles"
package_from: "perfil de"
item_description: "Descripció de l'article"
price: "Preu"

View File

@@ -59,7 +59,7 @@ de_DE:
messages:
inclusion: "ist in der Liste nicht enthalten"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^ Bitte fügen Sie mindestens ein Produkt hinzu"
@@ -2765,6 +2765,8 @@ de_DE:
location: "Ort"
count_on_hand: "Zählen Sie zur Hand"
quantity: "Menge"
on_demand: "Unbegrenzt"
on_hand: "Verfügbar"
package_from: "Paket von"
item_description: "Artikelbeschreibung"
price: "Preis"

View File

@@ -80,7 +80,7 @@ en:
messages:
inclusion: "is not included in the list"
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"
@@ -362,6 +362,7 @@ en:
choose: "Choose..."
please_select: Please select...
column_save_as_default: Save As Default
columns: Columns
actions: Actions
viewing: "Viewing: %{current_view_name}"
@@ -2053,6 +2054,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
admin_enterprise_relationships_permits: "permits"
admin_enterprise_relationships_seach_placeholder: "Search"
admin_enterprise_relationships_button_create: "Create"
admin_enterprise_relationships_to: "to"
admin_enterprise_groups: "Enterprise Groups"
admin_enterprise_groups_name: "Name"
admin_enterprise_groups_owner: "Owner"
@@ -2828,6 +2830,14 @@ See the %{link} to find out more about %{sitename}'s features and to start using
order_management:
reports:
enterprise_fee_summaries:
filters:
date_range: "Date Range"
report_format_csv: "Download as CSV"
generate_report: "Generate Report"
report:
none: "None"
select_and_search: "Select filters and click on GENERATE REPORT to access your data."
enterprise_fee_summary:
date_end_before_start_error: "must be after start"
parameter_not_allowed_error: "You are not authorized to use one or more selected filters for this report."
@@ -2910,6 +2920,8 @@ See the %{link} to find out more about %{sitename}'s features and to start using
location: "Location"
count_on_hand: "Count On Hand"
quantity: "Quantity"
on_demand: "On Demand"
on_hand: "On Hand"
package_from: "package from"
item_description: "Item Description"
price: "Price"
@@ -3331,14 +3343,6 @@ See the %{link} to find out more about %{sitename}'s features and to start using
bulk_coop_allocation: 'Bulk Co-op - Allocation'
bulk_coop_packing_sheets: 'Bulk Co-op - Packing Sheets'
bulk_coop_customer_payments: 'Bulk Co-op - Customer Payments'
enterprise_fee_summaries:
filters:
date_range: "Date Range"
report_format_csv: "Download as CSV"
generate_report: "Generate Report"
report:
none: "None"
select_and_search: "Select filters and click on GENERATE REPORT to access your data."
users:
index:
listing_users: "Listing Users"
@@ -3384,6 +3388,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using
producer_name: "Producer"
unit: "Unit"
general_settings:
shared:
sortable_header:
name: "Name"
edit:
legal_settings: "Legal Settings"
cookies_consent_banner_toggle: "Display cookies consent banner"

View File

@@ -53,7 +53,7 @@ en_AU:
payment_method_ids: "Payment Methods"
errors:
models:
subscription_validator:
order_management/subscriptions/validator:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"
@@ -2676,6 +2676,8 @@ en_AU:
location: "Location"
count_on_hand: "Count On Hand"
quantity: "Quantity"
on_demand: "On Demand"
on_hand: "On Hand"
package_from: "package from"
item_description: "Item Description"
price: "Price"

Some files were not shown because too many files have changed in this diff Show More