mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-12 18:36:49 +00:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eafffa2c23 | ||
|
|
82a4753eec | ||
|
|
b92e858448 | ||
|
|
b5ba2acb21 | ||
|
|
e97a16cb40 | ||
|
|
57d87a8042 | ||
|
|
0ca87580e8 | ||
|
|
59e0f3d9f4 | ||
|
|
fc5aff8c79 | ||
|
|
dd717fe8ac | ||
|
|
6341c5fd80 | ||
|
|
47ac6c1491 | ||
|
|
6afda141a1 | ||
|
|
5bb2614f9d | ||
|
|
b3c968856b | ||
|
|
b0a7497f2a | ||
|
|
f959e632ea | ||
|
|
f9cf826f1c | ||
|
|
ececbce596 | ||
|
|
1b7ac1a252 | ||
|
|
d31b24786a | ||
|
|
374bf04118 | ||
|
|
3aff7f62e3 | ||
|
|
fc5e346a06 | ||
|
|
29bbf2fa74 | ||
|
|
64c66ddedc | ||
|
|
e0e2c32d9f | ||
|
|
003341ef7a | ||
|
|
94f8ea2f93 | ||
|
|
eb64112b22 | ||
|
|
3e14b62b46 | ||
|
|
3244650932 | ||
|
|
b6753a2593 | ||
|
|
1b119805b4 | ||
|
|
edde7689a9 | ||
|
|
8060977786 | ||
|
|
837a345958 | ||
|
|
7c5e511fde | ||
|
|
d18a06a0f7 | ||
|
|
d23b4fd307 | ||
|
|
2cb4c6bec2 | ||
|
|
924e816a5b | ||
|
|
109da43905 | ||
|
|
33ca6a2096 | ||
|
|
e7b780f963 | ||
|
|
13cba3d244 | ||
|
|
ce45e7cf71 | ||
|
|
ba5a56db14 | ||
|
|
276dcf4a3b | ||
|
|
dcfb1aec6d | ||
|
|
fde2aac366 | ||
|
|
63138aef30 | ||
|
|
7612415991 | ||
|
|
53d901b41b | ||
|
|
bc859cf9f7 | ||
|
|
af48cac140 | ||
|
|
59bb956677 | ||
|
|
55b3f4d54f | ||
|
|
452ab3a842 | ||
|
|
a049e7a433 | ||
|
|
97063bf47e | ||
|
|
5b4dd57380 | ||
|
|
904e89e325 | ||
|
|
e5e9325499 | ||
|
|
2e4f8003b6 | ||
|
|
45b5e838b7 | ||
|
|
f8a4f00d52 | ||
|
|
29377bbff9 | ||
|
|
f68d0c2a0f | ||
|
|
3901c49af9 | ||
|
|
ae0ceb61a1 | ||
|
|
fb1c825fbc | ||
|
|
e36b0249b9 | ||
|
|
34fa2d7ad6 | ||
|
|
3aefea9f04 | ||
|
|
15231a9128 | ||
|
|
25e3f72934 | ||
|
|
523d819575 | ||
|
|
bc0a1d9bae | ||
|
|
a53dc3a8c1 |
@@ -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
|
||||
@@ -98,7 +97,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
|
||||
@@ -239,10 +237,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 +312,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
|
||||
@@ -411,7 +402,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
|
||||
@@ -686,6 +677,13 @@ 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/payment_setup_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 +699,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
|
||||
|
||||
@@ -71,7 +71,6 @@ Lint/DuplicateHashKey:
|
||||
Lint/DuplicateMethods:
|
||||
Exclude:
|
||||
- 'lib/discourse/single_sign_on.rb'
|
||||
- 'lib/open_food_network/subscription_summary.rb'
|
||||
|
||||
# Offense count: 10
|
||||
Lint/IneffectiveAccessModifier:
|
||||
@@ -159,7 +158,7 @@ Naming/MethodParameterName:
|
||||
Exclude:
|
||||
- 'app/helpers/spree/admin/base_helper_decorator.rb'
|
||||
- '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'
|
||||
@@ -849,11 +848,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'
|
||||
@@ -892,6 +886,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'
|
||||
@@ -949,7 +944,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'
|
||||
@@ -966,8 +960,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'
|
||||
@@ -1209,7 +1201,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'
|
||||
@@ -1218,8 +1209,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'
|
||||
@@ -1304,7 +1293,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'
|
||||
@@ -1355,11 +1343,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'
|
||||
@@ -1557,7 +1540,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'
|
||||
|
||||
11
DOCKER.md
11
DOCKER.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
5
Gemfile
5
Gemfile
@@ -45,10 +45,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'
|
||||
@@ -122,6 +118,7 @@ gem 'ofn-qz', github: 'openfoodfoundation/ofn-qz', ref: '60da2ae4c44cbb4c8d602f5
|
||||
|
||||
group :production, :staging do
|
||||
gem 'ddtrace'
|
||||
gem 'unicorn-worker-killer'
|
||||
end
|
||||
|
||||
group :test, :development do
|
||||
|
||||
21
Gemfile.lock
21
Gemfile.lock
@@ -1,11 +1,3 @@
|
||||
GIT
|
||||
remote: https://github.com/RohanM/simple_form.git
|
||||
revision: 45f08a213b40f3d4bda5f5398db841137587160a
|
||||
specs:
|
||||
simple_form (2.0.2)
|
||||
actionpack (~> 3.0)
|
||||
activemodel (~> 3.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/jeremydurham/custom-err-msg.git
|
||||
revision: 3a8ec9dddc7a5b0aab7c69a6060596de300c68f4
|
||||
@@ -206,7 +198,7 @@ GEM
|
||||
activerecord (>= 3.2.0, < 5.0)
|
||||
fog (~> 1.0)
|
||||
rails (>= 3.2.0, < 5.0)
|
||||
ddtrace (0.34.0)
|
||||
ddtrace (0.34.1)
|
||||
msgpack
|
||||
debugger-linecache (1.2.0)
|
||||
deface (1.0.2)
|
||||
@@ -411,6 +403,8 @@ GEM
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
geocoder (1.1.8)
|
||||
get_process_mem (0.2.5)
|
||||
ffi (~> 1.0)
|
||||
gmaps4rails (1.5.6)
|
||||
haml (4.0.7)
|
||||
tilt
|
||||
@@ -475,7 +469,7 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
oj (3.10.5)
|
||||
oj (3.10.6)
|
||||
orm_adapter (0.5.0)
|
||||
paper_trail (5.2.3)
|
||||
activerecord (>= 3.0, < 6.0)
|
||||
@@ -602,7 +596,7 @@ GEM
|
||||
rexml
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-rails (2.5.0)
|
||||
rubocop-rails (2.5.1)
|
||||
activesupport
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.72.0)
|
||||
@@ -668,6 +662,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)
|
||||
@@ -776,7 +773,6 @@ DEPENDENCIES
|
||||
select2-rails (~> 3.4.7)
|
||||
selenium-webdriver
|
||||
shoulda-matchers
|
||||
simple_form!
|
||||
simplecov
|
||||
spinjs-rails
|
||||
spree_core!
|
||||
@@ -792,6 +788,7 @@ DEPENDENCIES
|
||||
uglifier (>= 1.0.3)
|
||||
unicorn
|
||||
unicorn-rails
|
||||
unicorn-worker-killer
|
||||
web!
|
||||
webdrivers
|
||||
webmock
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
@@ -14,5 +14,10 @@
|
||||
|
||||
.more-controls {
|
||||
text-align: center;
|
||||
|
||||
.spinner {
|
||||
height: 2.25em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -81,7 +81,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
|
||||
end
|
||||
|
||||
@@ -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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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, params[:subscription])
|
||||
form = OrderManagement::Subscriptions::Form.new(@subscription, params[:subscription])
|
||||
unless form.save
|
||||
render json: { errors: form.json_errors }, status: :unprocessable_entity
|
||||
return
|
||||
|
||||
@@ -73,8 +73,10 @@ module Admin
|
||||
end
|
||||
|
||||
def collection
|
||||
@variant_overrides = VariantOverride.includes(:variant).for_hubs(params[:hub_id] || @hubs)
|
||||
@variant_overrides.select { |vo| vo.variant.present? }
|
||||
@variant_overrides = VariantOverride.
|
||||
includes(variant: :product).
|
||||
for_hubs(params[:hub_id] || @hubs).
|
||||
select { |vo| vo.variant.present? }
|
||||
end
|
||||
|
||||
def collection_actions
|
||||
|
||||
@@ -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
|
||||
@@ -42,12 +41,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
|
||||
|
||||
@@ -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.scoped.
|
||||
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: {
|
||||
|
||||
27
app/controllers/api/shops_controller.rb
Normal file
27
app/controllers/api/shops_controller.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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,43 @@ 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.present?
|
||||
|
||||
order.process_payments!
|
||||
return false if order.errors.present?
|
||||
|
||||
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!
|
||||
end
|
||||
|
||||
def send_failed_payment_email
|
||||
@order.update!
|
||||
record_and_log_error(:failed_payment, @order)
|
||||
SubscriptionMailer.failed_payment_email(@order).deliver
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@ class OrderCycle < ActiveRecord::Base
|
||||
has_many :distributors, source: :receiver, through: :cached_outgoing_exchanges, uniq: true
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class Schedule < ActiveRecord::Base
|
||||
has_and_belongs_to_many :order_cycles, join_table: 'order_cycle_schedules'
|
||||
has_paper_trail meta: { custom_data: :order_cycle_ids }
|
||||
has_paper_trail meta: { custom_data: proc { |schedule| schedule.order_cycle_ids.to_s } }
|
||||
|
||||
has_many :coordinators, uniq: true, through: :order_cycles
|
||||
|
||||
|
||||
36
app/models/spree/stock/quantifier.rb
Normal file
36
app/models/spree/stock/quantifier.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
15
app/serializers/api/admin/product_simple_serializer.rb
Normal file
15
app/serializers/api/admin/product_simple_serializer.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
32
app/serializers/api/admin/variant_simple_serializer.rb
Normal file
32
app/serializers/api/admin/variant_simple_serializer.rb
Normal 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
|
||||
@@ -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: {
|
||||
|
||||
@@ -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, 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?
|
||||
|
||||
@@ -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
|
||||
|
||||
23
app/services/shops_list_service.rb
Normal file
23
app/services/shops_list_service.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -1,34 +0,0 @@
|
||||
require 'open_food_network/proxy_order_syncer'
|
||||
|
||||
class SubscriptionForm
|
||||
attr_accessor :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, params = {})
|
||||
@subscription = subscription
|
||||
@params = params
|
||||
@estimator = SubscriptionEstimator.new(subscription)
|
||||
@validator = SubscriptionValidator.new(subscription)
|
||||
@order_syncer = OrderSyncer.new(subscription)
|
||||
end
|
||||
|
||||
def save
|
||||
subscription.assign_attributes(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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
13
config.ru
13
config.ru
@@ -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
|
||||
|
||||
@@ -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') %>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
# Use this setup block to configure all options available in SimpleForm.
|
||||
SimpleForm.setup do |config|
|
||||
# Wrappers are used by the form builder to generate a
|
||||
# complete input. You can remove any component from the
|
||||
# wrapper, change the order or even add your own to the
|
||||
# stack. The options given below are used to wrap the
|
||||
# whole input.
|
||||
config.wrappers :default, :class => :input,
|
||||
:hint_class => :field_with_hint, :error_class => :field_with_errors do |b|
|
||||
## Extensions enabled by default
|
||||
# Any of these extensions can be disabled for a
|
||||
# given input by passing: `f.input EXTENSION_NAME => false`.
|
||||
# You can make any of these extensions optional by
|
||||
# renaming `b.use` to `b.optional`.
|
||||
|
||||
# Determines whether to use HTML5 (:email, :url, ...)
|
||||
# and required attributes
|
||||
b.use :html5
|
||||
|
||||
# Calculates placeholders automatically from I18n
|
||||
# You can also pass a string as f.input :placeholder => "Placeholder"
|
||||
b.use :placeholder
|
||||
|
||||
## Optional extensions
|
||||
# They are disabled unless you pass `f.input EXTENSION_NAME => :lookup`
|
||||
# to the input. If so, they will retrieve the values from the model
|
||||
# if any exists. If you want to enable the lookup for any of those
|
||||
# extensions by default, you can change `b.optional` to `b.use`.
|
||||
|
||||
# Calculates maxlength from length validations for string inputs
|
||||
b.optional :maxlength
|
||||
|
||||
# Calculates pattern from format validations for string inputs
|
||||
b.optional :pattern
|
||||
|
||||
# Calculates min and max from length validations for numeric inputs
|
||||
b.optional :min_max
|
||||
|
||||
# Calculates readonly automatically from readonly attributes
|
||||
b.optional :readonly
|
||||
|
||||
## Inputs
|
||||
b.use :label_input
|
||||
b.use :hint, :wrap_with => { :tag => :span, :class => :hint }
|
||||
b.use :error, :wrap_with => { :tag => :span, :class => :error }
|
||||
end
|
||||
|
||||
# The default wrapper to be used by the FormBuilder.
|
||||
config.default_wrapper = :default
|
||||
|
||||
# Define the way to render check boxes / radio buttons with labels.
|
||||
# Defaults to :nested for bootstrap config.
|
||||
# :inline => input + label
|
||||
# :nested => label > input
|
||||
config.boolean_style = :nested
|
||||
|
||||
# Default class for buttons
|
||||
config.button_class = 'btn'
|
||||
|
||||
# Method used to tidy up errors. Specify any Rails Array method.
|
||||
# :first lists the first message for each field.
|
||||
# Use :to_sentence to list all errors for each field.
|
||||
# config.error_method = :first
|
||||
|
||||
# Default tag used for error notification helper.
|
||||
config.error_notification_tag = :div
|
||||
|
||||
# CSS class to add for error notification helper.
|
||||
config.error_notification_class = 'alert alert-error'
|
||||
|
||||
# ID to add for error notification helper.
|
||||
# config.error_notification_id = nil
|
||||
|
||||
# Series of attempts to detect a default label method for collection.
|
||||
# config.collection_label_methods = [ :to_label, :name, :title, :to_s ]
|
||||
|
||||
# Series of attempts to detect a default value method for collection.
|
||||
# config.collection_value_methods = [ :id, :to_s ]
|
||||
|
||||
# You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none.
|
||||
# config.collection_wrapper_tag = nil
|
||||
|
||||
# You can define the class to use on all collection wrappers. Defaulting to none.
|
||||
# config.collection_wrapper_class = nil
|
||||
|
||||
# You can wrap each item in a collection of radio/check boxes with a tag,
|
||||
# defaulting to :span. Please note that when using :boolean_style = :nested,
|
||||
# SimpleForm will force this option to be a label.
|
||||
# config.item_wrapper_tag = :span
|
||||
|
||||
# You can define a class to use in all item wrappers. Defaulting to none.
|
||||
# config.item_wrapper_class = nil
|
||||
|
||||
# How the label text should be generated altogether with the required text.
|
||||
# config.label_text = lambda { |label, required| "#{required} #{label}" }
|
||||
|
||||
# You can define the class to use on all labels. Default is nil.
|
||||
config.label_class = 'control-label'
|
||||
|
||||
# You can define the class to use on all forms. Default is simple_form.
|
||||
# config.form_class = :simple_form
|
||||
|
||||
# You can define which elements should obtain additional classes
|
||||
# config.generate_additional_classes_for = [:wrapper, :label, :input]
|
||||
|
||||
# Whether attributes are required by default (or not). Default is true.
|
||||
# config.required_by_default = true
|
||||
|
||||
# Tell browsers whether to use default HTML5 validations (novalidate option).
|
||||
# Default is enabled.
|
||||
config.browser_validations = false
|
||||
|
||||
# Collection of methods to detect if a file type was given.
|
||||
# config.file_methods = [ :mounted_as, :file?, :public_filename ]
|
||||
|
||||
# Custom mappings for input types. This should be a hash containing a regexp
|
||||
# to match as key, and the input type that will be used when the field name
|
||||
# matches the regexp as value.
|
||||
# config.input_mappings = { /count/ => :integer }
|
||||
|
||||
# Default priority for time_zone inputs.
|
||||
# config.time_zone_priority = nil
|
||||
|
||||
# Default priority for country inputs.
|
||||
# config.country_priority = nil
|
||||
|
||||
# Default size for text inputs.
|
||||
# config.default_input_size = 50
|
||||
|
||||
# When false, do not use translations for labels.
|
||||
# config.translate_labels = true
|
||||
|
||||
# Automatically discover new inputs in Rails' autoload path.
|
||||
# config.inputs_discovery = true
|
||||
|
||||
# Cache SimpleForm inputs discovery
|
||||
# config.cache_discovery = !Rails.env.development?
|
||||
end
|
||||
@@ -55,7 +55,7 @@ ar:
|
||||
messages:
|
||||
inclusion: "غير مدرجة في القائمة"
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^ الرجاء إضافة منتج واحد على الأقل"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -53,7 +53,7 @@ en_BE:
|
||||
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"
|
||||
|
||||
@@ -55,7 +55,7 @@ en_CA:
|
||||
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"
|
||||
|
||||
@@ -53,7 +53,7 @@ en_DE:
|
||||
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"
|
||||
|
||||
@@ -59,7 +59,7 @@ en_FR:
|
||||
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"
|
||||
|
||||
@@ -31,6 +31,10 @@ en_GB:
|
||||
taken: "There's already an account for this email. Please login or reset your password."
|
||||
spree/order:
|
||||
no_card: There are no authorised credit cards available to charge
|
||||
spree/credit_card:
|
||||
attributes:
|
||||
base:
|
||||
card_expired: "has expired"
|
||||
order_cycle:
|
||||
attributes:
|
||||
orders_close_at:
|
||||
@@ -55,7 +59,7 @@ en_GB:
|
||||
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"
|
||||
|
||||
@@ -31,6 +31,10 @@ en_NZ:
|
||||
taken: "There's already an account for this email. Please login or reset your password."
|
||||
spree/order:
|
||||
no_card: There are no authorised credit cards available to charge
|
||||
spree/credit_card:
|
||||
attributes:
|
||||
base:
|
||||
card_expired: "has expired"
|
||||
order_cycle:
|
||||
attributes:
|
||||
orders_close_at:
|
||||
@@ -55,7 +59,7 @@ en_NZ:
|
||||
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"
|
||||
@@ -867,6 +871,7 @@ en_NZ:
|
||||
distributor: "Distributor"
|
||||
products: "Products"
|
||||
tags: "Tags"
|
||||
delivery_details: "Delivery Details"
|
||||
fees: "Fees"
|
||||
previous: "Previous"
|
||||
save: "Save"
|
||||
|
||||
3373
config/locales/en_PH.yml
Normal file
3373
config/locales/en_PH.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,7 @@ en_US:
|
||||
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"
|
||||
|
||||
@@ -53,7 +53,7 @@ en_ZA:
|
||||
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"
|
||||
|
||||
@@ -53,7 +53,7 @@ es:
|
||||
payment_method_ids: "Métodos de Pago"
|
||||
errors:
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^Por favor agrega al menos un producto"
|
||||
|
||||
3385
config/locales/fil_PH.yml
Normal file
3385
config/locales/fil_PH.yml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -59,7 +59,7 @@ fr:
|
||||
messages:
|
||||
inclusion: "n'est pas inclus dans la liste"
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^Veuillez ajouter au moins un produit"
|
||||
@@ -1670,7 +1670,7 @@ fr:
|
||||
orders_form_admin: Admin & traitements
|
||||
orders_form_total: Total
|
||||
orders_oc_expired_headline: Les commandes ne sont plus possibles pour ce cycle de vente.
|
||||
orders_oc_expired_text: "Désolé, les commandes pour ce cycle de vente ont été clôturées il y a %{time}! Veuillez contacter directement le hub pour voir s'il accepte les commandes tardives."
|
||||
orders_oc_expired_text: "Désolé, les commandes pour ce cycle de vente ont été clôturées il y a %{time}!"
|
||||
orders_oc_expired_text_others_html: "Désolé, les commandes pour ce cycle de vente ont été clôturées il y a %{time}! Veuillez contacter directement le hub pour voir s'il accepte les commandes tardives <strong>%{link}</strong>."
|
||||
orders_oc_expired_text_link: "ou voir si d'autres cycles de vente sont ouverts pour ce hub"
|
||||
orders_oc_expired_email: "Email:"
|
||||
|
||||
@@ -55,7 +55,7 @@ fr_BE:
|
||||
messages:
|
||||
inclusion: "N'est pas inclus dans la liste"
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^Veuillez ajouter au moins un produit"
|
||||
|
||||
@@ -55,7 +55,7 @@ fr_CA:
|
||||
messages:
|
||||
inclusion: "n'est pas inclus dans la liste"
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "Veuillez ajouter au moins un produit"
|
||||
|
||||
@@ -55,7 +55,7 @@ it:
|
||||
messages:
|
||||
inclusion: "non incluso nella lista"
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^Aggiungi almeno un prodotto"
|
||||
|
||||
@@ -55,7 +55,7 @@ nb:
|
||||
messages:
|
||||
inclusion: "er ikke inkludert i listen"
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^Vennligst legg til minst ett produkt"
|
||||
|
||||
@@ -53,7 +53,7 @@ nl_BE:
|
||||
payment_method_ids: "Betaalmethodes"
|
||||
errors:
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^Gelieve minstens één product toe te voegen"
|
||||
|
||||
@@ -51,7 +51,7 @@ pt:
|
||||
payment_method_ids: "Métodos de pagamento"
|
||||
errors:
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^Por favor adicione pelo menos um produto"
|
||||
|
||||
@@ -53,7 +53,7 @@ pt_BR:
|
||||
messages:
|
||||
inclusion: "não está incluso na lista"
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^ Por favor, adicione pelo menos um produto"
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
en:
|
||||
simple_form:
|
||||
"yes": 'Yes'
|
||||
"no": 'No'
|
||||
required:
|
||||
text: 'required'
|
||||
mark: '*'
|
||||
# You can uncomment the line below if you need to overwrite the whole required html.
|
||||
# When using html, text and mark won't be used.
|
||||
# html: '<abbr title="required">*</abbr>'
|
||||
error_notification:
|
||||
default_message: "Please review the problems below:"
|
||||
# Labels and hints examples
|
||||
# labels:
|
||||
# defaults:
|
||||
# password: 'Password'
|
||||
# user:
|
||||
# new:
|
||||
# email: 'E-mail to sign in.'
|
||||
# edit:
|
||||
# email: 'E-mail.'
|
||||
# hints:
|
||||
# defaults:
|
||||
# username: 'User name to sign in.'
|
||||
# password: 'No special characters, please.'
|
||||
@@ -55,7 +55,7 @@ tr:
|
||||
messages:
|
||||
inclusion: "Listeye dahil değil"
|
||||
models:
|
||||
subscription_validator:
|
||||
order_management/subscriptions/validator:
|
||||
attributes:
|
||||
subscription_line_items:
|
||||
at_least_one_product: "^ Lütfen en az bir ürün ekleyin"
|
||||
|
||||
@@ -33,9 +33,11 @@ Openfoodnetwork::Application.routes.draw do
|
||||
|
||||
resource :logo, only: [:destroy]
|
||||
resource :promo_image, only: [:destroy]
|
||||
end
|
||||
|
||||
member do
|
||||
get :shopfront
|
||||
resources :shops, only: [:show] do
|
||||
collection do
|
||||
get :closed_shops
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -28,10 +28,7 @@ services:
|
||||
ADMIN_PASSWORD: ofn123
|
||||
OFN_DB_HOST: db
|
||||
command: >
|
||||
bash -c "(bundle check || bundle install) &&
|
||||
wait-for-it -t 30 db:5432 &&
|
||||
bundle exec rake db:reset &&
|
||||
bundle exec rake db:test:prepare ofn:sample_data || true &&
|
||||
bash -c "wait-for-it -t 30 db:5432 &&
|
||||
rm -f tmp/pids/server.pid &&
|
||||
bundle exec rails s -p 3000 -b '0.0.0.0'"
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
class Count
|
||||
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
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
|
||||
# Responsible for estimating prices and fees for subscriptions
|
||||
# Used by Form as part of the create/update process
|
||||
# The values calculated here are intended to be persisted in the db
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
class Estimator
|
||||
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
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'order_management/subscriptions/proxy_order_syncer'
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
class Form
|
||||
attr_accessor :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, params = {})
|
||||
@subscription = subscription
|
||||
@params = params
|
||||
@estimator = OrderManagement::Subscriptions::Estimator.new(subscription)
|
||||
@validator = OrderManagement::Subscriptions::Validator.new(subscription)
|
||||
@order_syncer = OrderSyncer.new(subscription)
|
||||
end
|
||||
|
||||
def save
|
||||
subscription.assign_attributes(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
|
||||
OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscription)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
class PaymentSetup
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def call!
|
||||
create_payment
|
||||
ensure_payment_source
|
||||
return if order.errors.any?
|
||||
|
||||
payment.update_attributes(amount: order.outstanding_balance)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
|
||||
def payment
|
||||
@payment ||= order.pending_payments.last
|
||||
end
|
||||
|
||||
def create_payment
|
||||
return if payment.present?
|
||||
|
||||
@payment = order.payments.create(
|
||||
payment_method_id: order.subscription.payment_method_id,
|
||||
amount: order.outstanding_balance
|
||||
)
|
||||
end
|
||||
|
||||
def card_required?
|
||||
[Spree::Gateway::StripeConnect,
|
||||
Spree::Gateway::StripeSCA].include? payment.payment_method.class
|
||||
end
|
||||
|
||||
def card_set?
|
||||
payment.source is_a? Spree::CreditCard
|
||||
end
|
||||
|
||||
def ensure_payment_source
|
||||
return unless card_required? && !card_set?
|
||||
|
||||
ensure_credit_card || order.errors.add(:base, :no_card)
|
||||
end
|
||||
|
||||
def ensure_credit_card
|
||||
return false if saved_credit_card.blank? || !allow_charges?
|
||||
|
||||
payment.update_attributes(source: saved_credit_card)
|
||||
end
|
||||
|
||||
def allow_charges?
|
||||
order.customer.allow_charges?
|
||||
end
|
||||
|
||||
def saved_credit_card
|
||||
order.user.default_card
|
||||
end
|
||||
|
||||
def errors_present?
|
||||
order.errors.any?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,99 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
class ProxyOrderSyncer
|
||||
attr_reader :subscription
|
||||
|
||||
delegate :order_cycles, :proxy_orders, :begins_at, :ends_at, to: :subscription
|
||||
|
||||
def initialize(subscriptions)
|
||||
case subscriptions
|
||||
when Subscription
|
||||
@subscription = subscriptions
|
||||
when ActiveRecord::Relation
|
||||
@subscriptions = subscriptions.not_ended.not_canceled
|
||||
else
|
||||
raise "ProxyOrderSyncer must be initialized with " \
|
||||
"an instance of Subscription or ActiveRecord::Relation"
|
||||
end
|
||||
end
|
||||
|
||||
def sync!
|
||||
return sync_subscriptions! if @subscriptions
|
||||
|
||||
return initialise_proxy_orders! unless @subscription.id
|
||||
|
||||
sync_subscription!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sync_subscriptions!
|
||||
@subscriptions.each do |subscription|
|
||||
@subscription = subscription
|
||||
sync_subscription!
|
||||
end
|
||||
end
|
||||
|
||||
def initialise_proxy_orders!
|
||||
uninitialised_order_cycle_ids.each do |order_cycle_id|
|
||||
Rails.logger.info "Initializing Proxy Order " \
|
||||
"of subscription #{@subscription.id} in order cycle #{order_cycle_id}"
|
||||
proxy_orders << ProxyOrder.new(subscription: subscription, order_cycle_id: order_cycle_id)
|
||||
end
|
||||
end
|
||||
|
||||
def sync_subscription!
|
||||
Rails.logger.info "Syncing Proxy Orders of subscription #{@subscription.id}"
|
||||
create_proxy_orders!
|
||||
remove_orphaned_proxy_orders!
|
||||
end
|
||||
|
||||
def create_proxy_orders!
|
||||
return unless not_closed_in_range_order_cycles.any?
|
||||
|
||||
query = "INSERT INTO proxy_orders (subscription_id, order_cycle_id, updated_at, created_at)"
|
||||
query << " VALUES #{insert_values}"
|
||||
query << " ON CONFLICT DO NOTHING"
|
||||
|
||||
ActiveRecord::Base.connection.exec_query(query)
|
||||
end
|
||||
|
||||
def uninitialised_order_cycle_ids
|
||||
not_closed_in_range_order_cycles.pluck(:id) - proxy_orders.map(&:order_cycle_id)
|
||||
end
|
||||
|
||||
def remove_orphaned_proxy_orders!
|
||||
orphaned_proxy_orders.scoped.delete_all
|
||||
end
|
||||
|
||||
# Remove Proxy Orders that have not been placed yet
|
||||
# and are in Order Cycles that are out of range
|
||||
def orphaned_proxy_orders
|
||||
orphaned = proxy_orders.where(placed_at: nil)
|
||||
order_cycle_ids = in_range_order_cycles.pluck(:id)
|
||||
return orphaned unless order_cycle_ids.any?
|
||||
|
||||
orphaned.where('order_cycle_id NOT IN (?)', order_cycle_ids)
|
||||
end
|
||||
|
||||
def insert_values
|
||||
now = Time.now.utc.iso8601
|
||||
not_closed_in_range_order_cycles
|
||||
.map{ |oc| "(#{subscription.id},#{oc.id},'#{now}','#{now}')" }
|
||||
.join(",")
|
||||
end
|
||||
|
||||
def not_closed_in_range_order_cycles
|
||||
in_range_order_cycles.merge(OrderCycle.not_closed)
|
||||
end
|
||||
|
||||
def in_range_order_cycles
|
||||
order_cycles.where("orders_close_at >= ? AND orders_close_at <= ?",
|
||||
begins_at,
|
||||
ends_at || 100.years.from_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,55 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Used by for SubscriptionPlacementJob and SubscriptionConfirmJob to summarize the
|
||||
# result of automatic processing of subscriptions for the relevant shop owners.
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
class Summarizer
|
||||
def initialize
|
||||
@summaries = {}
|
||||
end
|
||||
|
||||
def record_order(order)
|
||||
summary_for(order).record_order(order)
|
||||
end
|
||||
|
||||
def record_success(order)
|
||||
summary_for(order).record_success(order)
|
||||
end
|
||||
|
||||
def record_issue(type, order, message = nil)
|
||||
Rails.logger.info "Issue in Subscription Order #{order.id}: #{type}"
|
||||
summary_for(order).record_issue(type, order, message)
|
||||
end
|
||||
|
||||
def record_and_log_error(type, order)
|
||||
return record_issue(type, order) unless order.errors.any?
|
||||
|
||||
error = "Subscription#{type.to_s.camelize}Error"
|
||||
line1 = "#{error}: Cannot process order #{order.number} due to errors"
|
||||
line2 = "Errors: #{order.errors.full_messages.join(', ')}"
|
||||
Rails.logger.info("#{line1}\n#{line2}")
|
||||
record_issue(type, order, line2)
|
||||
end
|
||||
|
||||
def send_placement_summary_emails
|
||||
@summaries.values.each do |summary|
|
||||
SubscriptionMailer.placement_summary_email(summary).deliver
|
||||
end
|
||||
end
|
||||
|
||||
def send_confirmation_summary_emails
|
||||
@summaries.values.each do |summary|
|
||||
SubscriptionMailer.confirmation_summary_email(summary).deliver
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def summary_for(order)
|
||||
shop_id = order.distributor_id
|
||||
@summaries[shop_id] ||= Summary.new(shop_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
class Summary
|
||||
attr_reader :shop_id, :issues
|
||||
|
||||
def initialize(shop_id)
|
||||
@shop_id = shop_id
|
||||
@order_ids = []
|
||||
@success_ids = []
|
||||
@issues = {}
|
||||
end
|
||||
|
||||
def record_order(order)
|
||||
@order_ids << order.id
|
||||
end
|
||||
|
||||
def record_success(order)
|
||||
@success_ids << order.id
|
||||
end
|
||||
|
||||
def record_issue(type, order, message)
|
||||
issues[type] ||= {}
|
||||
issues[type][order.id] = message
|
||||
end
|
||||
|
||||
def order_count
|
||||
@order_ids.count
|
||||
end
|
||||
|
||||
def success_count
|
||||
@success_ids.count
|
||||
end
|
||||
|
||||
def issue_count
|
||||
(@order_ids - @success_ids).count
|
||||
end
|
||||
|
||||
def orders_affected_by(type)
|
||||
case type
|
||||
when :other then Spree::Order.where(id: unrecorded_ids)
|
||||
else Spree::Order.where(id: issues[type].keys)
|
||||
end
|
||||
end
|
||||
|
||||
def unrecorded_ids
|
||||
recorded_ids = issues.values.map(&:keys).flatten
|
||||
@order_ids - @success_ids - recorded_ids
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,132 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# 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
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
class Validator
|
||||
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 |(key, value), errors|
|
||||
errors[key] = value.map { |msg| build_msg_from(key, 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 = OrderManagement::Subscriptions::VariantsList.eligible_variants(shop)
|
||||
.where(id: subscription_variant_ids).pluck(:id)
|
||||
end
|
||||
|
||||
def build_msg_from(key, msg)
|
||||
return msg[1..-1] if msg.starts_with?("^")
|
||||
|
||||
errors.full_message(key, msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
class VariantsList
|
||||
# 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
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe ProxyOrderSyncer, performance: true do
|
||||
let(:start) { Time.zone.now.beginning_of_day }
|
||||
let!(:schedule) { create(:schedule, order_cycles: order_cycles) }
|
||||
|
||||
let!(:order_cycles) do
|
||||
Array.new(10) do |i|
|
||||
create(:simple_order_cycle, orders_open_at: start + i.days,
|
||||
orders_close_at: start + (i + 1).days )
|
||||
end
|
||||
end
|
||||
|
||||
let!(:subscriptions) do
|
||||
Array.new(150) do |_i|
|
||||
create(:subscription, schedule: schedule, begins_at: start, ends_at: start + 10.days)
|
||||
end
|
||||
Subscription.where(schedule_id: schedule)
|
||||
end
|
||||
|
||||
context "measuring performance for initialisation" do
|
||||
it "reports the average run time for adding 10 OCs to 150 subscriptions" do
|
||||
expect(ProxyOrder.count).to be 0
|
||||
times = []
|
||||
10.times do
|
||||
syncer = ProxyOrderSyncer.new(subscriptions.reload)
|
||||
|
||||
t1 = Time.zone.now
|
||||
syncer.sync!
|
||||
t2 = Time.zone.now
|
||||
diff = t2 - t1
|
||||
times << diff
|
||||
puts diff.round(2)
|
||||
|
||||
expect(ProxyOrder.count).to be 1500
|
||||
ProxyOrder.destroy_all
|
||||
end
|
||||
puts "AVG: #{(times.sum / times.count).round(2)}"
|
||||
end
|
||||
end
|
||||
|
||||
context "measuring performance for removal" do
|
||||
it "reports the average run time for removing 8 OCs from 150 subscriptions" do
|
||||
times = []
|
||||
10.times do
|
||||
syncer = ProxyOrderSyncer.new(subscriptions.reload)
|
||||
syncer.sync!
|
||||
expect(ProxyOrder.count).to be 1500
|
||||
subscriptions.update_all(begins_at: start + 8.days + 1.minute)
|
||||
syncer = ProxyOrderSyncer.new(subscriptions.reload)
|
||||
|
||||
t1 = Time.zone.now
|
||||
syncer.sync!
|
||||
t2 = Time.zone.now
|
||||
diff = t2 - t1
|
||||
times << diff
|
||||
puts diff.round(2)
|
||||
|
||||
expect(ProxyOrder.count).to be 300
|
||||
subscriptions.update_all(begins_at: start)
|
||||
end
|
||||
puts "AVG: #{(times.sum / times.count).round(2)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe Count do
|
||||
let(:oc1) { create(:simple_order_cycle) }
|
||||
let(:oc2) { create(:simple_order_cycle) }
|
||||
let(:subscriptions_count) { Count.new(order_cycles) }
|
||||
|
||||
describe "#for" do
|
||||
context "when the collection has not been set" do
|
||||
let(:order_cycles) { nil }
|
||||
it "returns 0" do
|
||||
expect(subscriptions_count.for(oc1.id)).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context "when the collection has been set" do
|
||||
let(:order_cycles) { OrderCycle.where(id: [oc1]) }
|
||||
let!(:po1) { create(:proxy_order, order_cycle: oc1) }
|
||||
let!(:po2) { create(:proxy_order, order_cycle: oc1) }
|
||||
let!(:po3) { create(:proxy_order, order_cycle: oc2) }
|
||||
|
||||
context "but the requested id is not present in the list of order cycles provided" do
|
||||
it "returns 0" do
|
||||
# Note that po3 applies to oc2, but oc2 in not in the collection
|
||||
expect(subscriptions_count.for(oc2.id)).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context "and the requested id is present in the list of order cycles provided" do
|
||||
it "returns a count of active proxy orders associated with the requested order cycle" do
|
||||
expect(subscriptions_count.for(oc1.id)).to eq 2
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,171 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe Estimator do
|
||||
describe "estimating prices for subscription line items" do
|
||||
let!(:subscription) { create(:subscription, with_items: true) }
|
||||
let!(:sli1) { subscription.subscription_line_items.first }
|
||||
let!(:sli2) { subscription.subscription_line_items.second }
|
||||
let!(:sli3) { subscription.subscription_line_items.third }
|
||||
let(:estimator) { Estimator.new(subscription) }
|
||||
|
||||
before do
|
||||
sli1.update_attributes(price_estimate: 4.0)
|
||||
sli2.update_attributes(price_estimate: 5.0)
|
||||
sli3.update_attributes(price_estimate: 6.0)
|
||||
sli1.variant.update_attributes(price: 1.0)
|
||||
sli2.variant.update_attributes(price: 2.0)
|
||||
sli3.variant.update_attributes(price: 3.0)
|
||||
|
||||
# Simulating assignment of attrs from params
|
||||
sli1.assign_attributes(price_estimate: 7.0)
|
||||
sli2.assign_attributes(price_estimate: 8.0)
|
||||
sli3.assign_attributes(price_estimate: 9.0)
|
||||
end
|
||||
|
||||
context "when a insufficient information exists to calculate price estimates" do
|
||||
before do
|
||||
# This might be because a shop has not been assigned yet, or no
|
||||
# current or future order cycles exist for the schedule
|
||||
allow(estimator).to receive(:fee_calculator) { nil }
|
||||
end
|
||||
|
||||
it "resets the price estimates for all items" do
|
||||
estimator.estimate!
|
||||
expect(sli1.price_estimate).to eq 4.0
|
||||
expect(sli2.price_estimate).to eq 5.0
|
||||
expect(sli3.price_estimate).to eq 6.0
|
||||
end
|
||||
end
|
||||
|
||||
context "when sufficient information to calculate price estimates exists" do
|
||||
let(:fee_calculator) { instance_double(OpenFoodNetwork::EnterpriseFeeCalculator) }
|
||||
|
||||
before do
|
||||
allow(estimator).to receive(:fee_calculator) { fee_calculator }
|
||||
allow(fee_calculator).to receive(:indexed_fees_for).with(sli1.variant) { 1.0 }
|
||||
allow(fee_calculator).to receive(:indexed_fees_for).with(sli2.variant) { 0.0 }
|
||||
allow(fee_calculator).to receive(:indexed_fees_for).with(sli3.variant) { 3.0 }
|
||||
end
|
||||
|
||||
context "when no variant overrides apply" do
|
||||
it "recalculates price_estimates based on variant prices and associated fees" do
|
||||
estimator.estimate!
|
||||
expect(sli1.price_estimate).to eq 2.0
|
||||
expect(sli2.price_estimate).to eq 2.0
|
||||
expect(sli3.price_estimate).to eq 6.0
|
||||
end
|
||||
end
|
||||
|
||||
context "when variant overrides apply" do
|
||||
let!(:override1) {
|
||||
create(:variant_override, hub: subscription.shop, variant: sli1.variant, price: 1.2)
|
||||
}
|
||||
let!(:override2) {
|
||||
create(:variant_override, hub: subscription.shop, variant: sli2.variant, price: 2.3)
|
||||
}
|
||||
|
||||
it "recalculates price_estimates based on override prices and associated fees" do
|
||||
estimator.estimate!
|
||||
expect(sli1.price_estimate).to eq 2.2
|
||||
expect(sli2.price_estimate).to eq 2.3
|
||||
expect(sli3.price_estimate).to eq 6.0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "updating estimates for shipping and payment fees" do
|
||||
let(:subscription) {
|
||||
create(:subscription, with_items: true,
|
||||
payment_method: payment_method,
|
||||
shipping_method: shipping_method)
|
||||
}
|
||||
let!(:sli1) { subscription.subscription_line_items.first }
|
||||
let!(:sli2) { subscription.subscription_line_items.second }
|
||||
let!(:sli3) { subscription.subscription_line_items.third }
|
||||
let(:estimator) { OrderManagement::Subscriptions::Estimator.new(subscription) }
|
||||
|
||||
before do
|
||||
allow(estimator).to receive(:assign_price_estimates)
|
||||
sli1.update_attributes(price_estimate: 4.0)
|
||||
sli2.update_attributes(price_estimate: 5.0)
|
||||
sli3.update_attributes(price_estimate: 6.0)
|
||||
end
|
||||
|
||||
context "using flat rate calculators" do
|
||||
let(:shipping_method) {
|
||||
create(:shipping_method,
|
||||
calculator: Spree::Calculator::FlatRate.new(preferred_amount: 12.34))
|
||||
}
|
||||
let(:payment_method) {
|
||||
create(:payment_method,
|
||||
calculator: Spree::Calculator::FlatRate.new(preferred_amount: 9.12))
|
||||
}
|
||||
|
||||
it "calculates fees based on the rates provided" do
|
||||
estimator.estimate!
|
||||
expect(subscription.shipping_fee_estimate.to_f).to eq 12.34
|
||||
expect(subscription.payment_fee_estimate.to_f).to eq 9.12
|
||||
end
|
||||
end
|
||||
|
||||
context "using flat percent item total calculators" do
|
||||
let(:shipping_method) {
|
||||
create(:shipping_method,
|
||||
calculator: Spree::Calculator::FlatPercentItemTotal.new(
|
||||
preferred_flat_percent: 10
|
||||
))
|
||||
}
|
||||
let(:payment_method) {
|
||||
create(:payment_method,
|
||||
calculator: Spree::Calculator::FlatPercentItemTotal.new(
|
||||
preferred_flat_percent: 20
|
||||
))
|
||||
}
|
||||
|
||||
it "calculates fees based on the estimated item total and percentage provided" do
|
||||
estimator.estimate!
|
||||
expect(subscription.shipping_fee_estimate.to_f).to eq 1.5
|
||||
expect(subscription.payment_fee_estimate.to_f).to eq 3.0
|
||||
end
|
||||
end
|
||||
|
||||
context "using flat percent per item calculators" do
|
||||
let(:shipping_method) {
|
||||
create(:shipping_method,
|
||||
calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 5))
|
||||
}
|
||||
let(:payment_method) {
|
||||
create(:payment_method,
|
||||
calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 10))
|
||||
}
|
||||
|
||||
it "calculates fees based on the estimated item prices and percentage provided" do
|
||||
estimator.estimate!
|
||||
expect(subscription.shipping_fee_estimate.to_f).to eq 0.75
|
||||
expect(subscription.payment_fee_estimate.to_f).to eq 1.5
|
||||
end
|
||||
end
|
||||
|
||||
context "using per item calculators" do
|
||||
let(:shipping_method) {
|
||||
create(:shipping_method,
|
||||
calculator: Spree::Calculator::PerItem.new(preferred_amount: 1.2))
|
||||
}
|
||||
let(:payment_method) {
|
||||
create(:payment_method,
|
||||
calculator: Spree::Calculator::PerItem.new(preferred_amount: 0.3))
|
||||
}
|
||||
|
||||
it "calculates fees based on the number of items and rate provided" do
|
||||
estimator.estimate!
|
||||
expect(subscription.shipping_fee_estimate.to_f).to eq 3.6
|
||||
expect(subscription.payment_fee_estimate.to_f).to eq 0.9
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,148 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe Form do
|
||||
describe "creating a new subscription" do
|
||||
let!(:shop) { create(:distributor_enterprise) }
|
||||
let!(:customer) { create(:customer, enterprise: shop) }
|
||||
let!(:product1) { create(:product, supplier: shop) }
|
||||
let!(:product2) { create(:product, supplier: shop) }
|
||||
let!(:product3) { create(:product, supplier: shop) }
|
||||
let!(:variant1) {
|
||||
create(:variant, product: product1, unit_value: '100', price: 12.00, option_values: [])
|
||||
}
|
||||
let!(:variant2) {
|
||||
create(:variant, product: product2, unit_value: '1000', price: 6.00, option_values: [])
|
||||
}
|
||||
let!(:variant3) {
|
||||
create(:variant, product: product2, unit_value: '1000',
|
||||
price: 2.50, option_values: [], on_hand: 1)
|
||||
}
|
||||
let!(:enterprise_fee) { create(:enterprise_fee, amount: 1.75) }
|
||||
let!(:order_cycle1) {
|
||||
create(:simple_order_cycle, coordinator: shop,
|
||||
orders_open_at: 9.days.ago,
|
||||
orders_close_at: 2.days.ago)
|
||||
}
|
||||
let!(:order_cycle2) {
|
||||
create(:simple_order_cycle, coordinator: shop,
|
||||
orders_open_at: 2.days.ago,
|
||||
orders_close_at: 5.days.from_now)
|
||||
}
|
||||
let!(:order_cycle3) {
|
||||
create(:simple_order_cycle, coordinator: shop,
|
||||
orders_open_at: 5.days.from_now,
|
||||
orders_close_at: 12.days.from_now)
|
||||
}
|
||||
let!(:order_cycle4) {
|
||||
create(:simple_order_cycle, coordinator: shop,
|
||||
orders_open_at: 12.days.from_now,
|
||||
orders_close_at: 19.days.from_now)
|
||||
}
|
||||
let!(:outgoing_exchange1) {
|
||||
order_cycle1.exchanges.create(sender: shop,
|
||||
receiver: shop,
|
||||
variants: [variant1, variant2, variant3],
|
||||
enterprise_fees: [enterprise_fee])
|
||||
}
|
||||
let!(:outgoing_exchange2) {
|
||||
order_cycle2.exchanges.create(sender: shop,
|
||||
receiver: shop,
|
||||
variants: [variant1, variant2, variant3],
|
||||
enterprise_fees: [enterprise_fee])
|
||||
}
|
||||
let!(:outgoing_exchange3) {
|
||||
order_cycle3.exchanges.create(sender: shop,
|
||||
receiver: shop,
|
||||
variants: [variant1, variant3],
|
||||
enterprise_fees: [])
|
||||
}
|
||||
let!(:outgoing_exchange4) {
|
||||
order_cycle4.exchanges.create(sender: shop,
|
||||
receiver: shop,
|
||||
variants: [variant1, variant2, variant3],
|
||||
enterprise_fees: [enterprise_fee])
|
||||
}
|
||||
let!(:schedule) {
|
||||
create(:schedule, order_cycles: [order_cycle1, order_cycle2, order_cycle3, order_cycle4])
|
||||
}
|
||||
let!(:payment_method) { create(:payment_method, distributors: [shop]) }
|
||||
let!(:shipping_method) { create(:shipping_method, distributors: [shop]) }
|
||||
let!(:address) { create(:address) }
|
||||
let(:subscription) { Subscription.new }
|
||||
|
||||
let!(:params) {
|
||||
{
|
||||
shop_id: shop.id,
|
||||
customer_id: customer.id,
|
||||
schedule_id: schedule.id,
|
||||
bill_address_attributes: address.clone.attributes,
|
||||
ship_address_attributes: address.clone.attributes,
|
||||
payment_method_id: payment_method.id,
|
||||
shipping_method_id: shipping_method.id,
|
||||
begins_at: 4.days.ago,
|
||||
ends_at: 14.days.from_now,
|
||||
subscription_line_items_attributes: [
|
||||
{ variant_id: variant1.id, quantity: 1, price_estimate: 7.0 },
|
||||
{ variant_id: variant2.id, quantity: 2, price_estimate: 8.0 },
|
||||
{ variant_id: variant3.id, quantity: 3, price_estimate: 9.0 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
let(:form) { OrderManagement::Subscriptions::Form.new(subscription, params) }
|
||||
|
||||
it "creates orders for each order cycle in the schedule" do
|
||||
expect(form.save).to be true
|
||||
|
||||
expect(subscription.proxy_orders.count).to be 2
|
||||
expect(subscription.subscription_line_items.count).to be 3
|
||||
expect(subscription.subscription_line_items[0].price_estimate).to eq 13.75
|
||||
expect(subscription.subscription_line_items[1].price_estimate).to eq 7.75
|
||||
expect(subscription.subscription_line_items[2].price_estimate).to eq 4.25
|
||||
|
||||
# This order cycle has already closed, so no order is initialized
|
||||
proxy_order1 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle1.id)
|
||||
expect(proxy_order1).to be nil
|
||||
|
||||
# Currently open order cycle, closing after begins_at and before ends_at
|
||||
proxy_order2 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle2.id)
|
||||
expect(proxy_order2).to be_a ProxyOrder
|
||||
order2 = proxy_order2.initialise_order!
|
||||
expect(order2.line_items.count).to eq 3
|
||||
expect(order2.line_items.find_by_variant_id(variant3.id).quantity).to be 3
|
||||
expect(order2.shipments.count).to eq 1
|
||||
expect(order2.shipments.first.shipping_method).to eq shipping_method
|
||||
expect(order2.payments.count).to eq 1
|
||||
expect(order2.payments.first.payment_method).to eq payment_method
|
||||
expect(order2.payments.first.state).to eq 'checkout'
|
||||
expect(order2.total).to eq 42
|
||||
expect(order2.completed?).to be false
|
||||
|
||||
# Future order cycle, closing after begins_at and before ends_at
|
||||
# Adds line items for variants that aren't yet available from the order cycle
|
||||
proxy_order3 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle3.id)
|
||||
expect(proxy_order3).to be_a ProxyOrder
|
||||
order3 = proxy_order3.initialise_order!
|
||||
expect(order3).to be_a Spree::Order
|
||||
expect(order3.line_items.count).to eq 3
|
||||
expect(order2.line_items.find_by_variant_id(variant3.id).quantity).to be 3
|
||||
expect(order3.shipments.count).to eq 1
|
||||
expect(order3.shipments.first.shipping_method).to eq shipping_method
|
||||
expect(order3.payments.count).to eq 1
|
||||
expect(order3.payments.first.payment_method).to eq payment_method
|
||||
expect(order3.payments.first.state).to eq 'checkout'
|
||||
expect(order3.total).to eq 31.50
|
||||
expect(order3.completed?).to be false
|
||||
|
||||
# Future order cycle closing after ends_at
|
||||
proxy_order4 = subscription.proxy_orders.find_by_order_cycle_id(order_cycle4.id)
|
||||
expect(proxy_order4).to be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,231 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe PaymentSetup do
|
||||
let(:order) { create(:order) }
|
||||
let(:payment_setup) { OrderManagement::Subscriptions::PaymentSetup.new(order) }
|
||||
|
||||
describe "#payment" do
|
||||
context "when only one payment exists on the order" do
|
||||
let!(:payment) { create(:payment, order: order) }
|
||||
|
||||
context "where the payment is pending" do
|
||||
it { expect(payment_setup.__send__(:payment)).to eq payment }
|
||||
end
|
||||
|
||||
context "where the payment is failed" do
|
||||
before { payment.update_attribute(:state, 'failed') }
|
||||
it { expect(payment_setup.__send__(:payment)).to be nil }
|
||||
end
|
||||
end
|
||||
|
||||
context "when more that one payment exists on the order" do
|
||||
let!(:payment1) { create(:payment, order: order) }
|
||||
let!(:payment2) { create(:payment, order: order) }
|
||||
|
||||
context "where more than one payment is pending" do
|
||||
it { expect([payment1, payment2]).to include payment_setup.__send__(:payment) }
|
||||
end
|
||||
|
||||
context "where only one payment is pending" do
|
||||
before { payment1.update_attribute(:state, 'failed') }
|
||||
it { expect(payment_setup.__send__(:payment)).to eq payment2 }
|
||||
end
|
||||
|
||||
context "where no payments are pending" do
|
||||
before do
|
||||
payment1.update_attribute(:state, 'failed')
|
||||
payment2.update_attribute(:state, 'failed')
|
||||
end
|
||||
|
||||
it { expect(payment_setup.__send__(:payment)).to be nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#call!" do
|
||||
let!(:payment){ create(:payment, amount: 10) }
|
||||
|
||||
context "when no pending payments are present" do
|
||||
let(:payment_method) { create(:payment_method) }
|
||||
let(:subscription) { double(:subscription, payment_method_id: payment_method.id) }
|
||||
|
||||
before do
|
||||
allow(order).to receive(:pending_payments).once { [] }
|
||||
allow(order).to receive(:outstanding_balance) { 5 }
|
||||
allow(order).to receive(:subscription) { subscription }
|
||||
end
|
||||
|
||||
it "creates a new payment on the order" do
|
||||
expect{ payment_setup.call! }.to change(Spree::Payment, :count).by(1)
|
||||
expect(order.payments.first.amount).to eq 5
|
||||
end
|
||||
end
|
||||
|
||||
context "when a payment is present" do
|
||||
before { allow(order).to receive(:pending_payments).once { [payment] } }
|
||||
|
||||
context "when a credit card is not required" do
|
||||
before do
|
||||
allow(payment_setup).to receive(:card_required?) { false }
|
||||
expect(payment_setup).to_not receive(:card_available?)
|
||||
expect(payment_setup).to_not receive(:ensure_credit_card)
|
||||
end
|
||||
|
||||
context "when the payment total doesn't match the outstanding balance on the order" do
|
||||
before { allow(order).to receive(:outstanding_balance) { 5 } }
|
||||
it "updates the payment total to reflect the outstanding balance" do
|
||||
expect{ payment_setup.call! }.to change(payment, :amount).from(10).to(5)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the payment total matches the outstanding balance on the order" do
|
||||
before { allow(order).to receive(:outstanding_balance) { 10 } }
|
||||
|
||||
it "does nothing" do
|
||||
expect{ payment_setup.call! }.to_not change(payment, :amount).from(10)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when a credit card is required" do
|
||||
before do
|
||||
expect(payment_setup).to receive(:card_required?) { true }
|
||||
end
|
||||
|
||||
context "and the payment source is not a credit card" do
|
||||
before { expect(payment_setup).to receive(:card_set?) { false } }
|
||||
|
||||
context "and no default credit card has been set by the customer" do
|
||||
before do
|
||||
allow(order).to receive(:user) { instance_double(Spree::User, default_card: nil) }
|
||||
end
|
||||
|
||||
it "adds an error to the order and does not update the payment" do
|
||||
expect(payment).to_not receive(:update_attributes)
|
||||
expect{ payment_setup.call! }.to change(order.errors[:base], :count).from(0).to(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "and the customer has not authorised the shop to charge to credit cards" do
|
||||
before do
|
||||
allow(order).to receive(:user) {
|
||||
instance_double(Spree::User, default_card: create(:credit_card))
|
||||
}
|
||||
allow(order).to receive(:customer) {
|
||||
instance_double(Customer, allow_charges?: false)
|
||||
}
|
||||
end
|
||||
|
||||
it "adds an error to the order and does not update the payment" do
|
||||
expect(payment).to_not receive(:update_attributes)
|
||||
expect{ payment_setup.call! }.to change(order.errors[:base], :count).from(0).to(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "and an authorised default credit card is available to charge" do
|
||||
before do
|
||||
allow(order).to receive(:user) {
|
||||
instance_double(Spree::User, default_card: create(:credit_card))
|
||||
}
|
||||
allow(order).to receive(:customer) {
|
||||
instance_double(Customer, allow_charges?: true)
|
||||
}
|
||||
end
|
||||
|
||||
context "when the payment total doesn't match the order's outstanding balance" do
|
||||
before { allow(order).to receive(:outstanding_balance) { 5 } }
|
||||
it "updates the payment total to reflect the outstanding balance" do
|
||||
expect{ payment_setup.call! }.to change(payment, :amount).from(10).to(5)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the payment total matches the outstanding balance on the order" do
|
||||
before { allow(order).to receive(:outstanding_balance) { 10 } }
|
||||
|
||||
it "does nothing" do
|
||||
expect{ payment_setup.call! }.to_not change(payment, :amount).from(10)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "and the payment source is already a credit card" do
|
||||
before { expect(payment_setup).to receive(:card_set?) { true } }
|
||||
|
||||
context "when the payment total doesn't match the outstanding balance on the order" do
|
||||
before { allow(order).to receive(:outstanding_balance) { 5 } }
|
||||
it "updates the payment total to reflect the outstanding balance" do
|
||||
expect{ payment_setup.call! }.to change(payment, :amount).from(10).to(5)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the payment total matches the outstanding balance on the order" do
|
||||
before { allow(order).to receive(:outstanding_balance) { 10 } }
|
||||
|
||||
it "does nothing" do
|
||||
expect{ payment_setup.call! }.to_not change(payment, :amount).from(10)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#ensure_credit_card" do
|
||||
let!(:payment) { create(:payment, source: nil) }
|
||||
before { allow(payment_setup).to receive(:payment) { payment } }
|
||||
|
||||
context "when no default credit card is found" do
|
||||
before do
|
||||
allow(order).to receive(:user) { instance_double(Spree::User, default_card: nil) }
|
||||
end
|
||||
|
||||
it "returns false and down not update the payment source" do
|
||||
expect do
|
||||
expect(payment_setup.__send__(:ensure_credit_card)).to be false
|
||||
end.to_not change(payment, :source).from(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "when a default credit card is found" do
|
||||
let(:credit_card) { create(:credit_card) }
|
||||
before do
|
||||
allow(order).to receive(:user) {
|
||||
instance_double(Spree::User, default_card: credit_card)
|
||||
}
|
||||
end
|
||||
|
||||
context "and charge have not been authorised by the customer" do
|
||||
before do
|
||||
allow(order).to receive(:customer) {
|
||||
instance_double(Customer, allow_charges?: false)
|
||||
}
|
||||
end
|
||||
|
||||
it "returns false and does not update the payment source" do
|
||||
expect do
|
||||
expect(payment_setup.__send__(:ensure_credit_card)).to be false
|
||||
end.to_not change(payment, :source).from(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "and charges have been authorised by the customer" do
|
||||
before do
|
||||
allow(order).to receive(:customer) { instance_double(Customer, allow_charges?: true) }
|
||||
end
|
||||
|
||||
it "returns true and stores the credit card as the payment source" do
|
||||
expect do
|
||||
expect(payment_setup.__send__(:ensure_credit_card)).to be true
|
||||
end.to change(payment, :source_id).from(nil).to(credit_card.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,437 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe ProxyOrderSyncer do
|
||||
describe "initialization" do
|
||||
let!(:subscription) { create(:subscription) }
|
||||
|
||||
it "raises an error when initialized with an object
|
||||
that is not a Subscription or an ActiveRecord::Relation" do
|
||||
expect{ ProxyOrderSyncer.new(subscription) }.to_not raise_error
|
||||
expect{ ProxyOrderSyncer.new(Subscription.where(id: subscription.id)) }.to_not raise_error
|
||||
expect{ ProxyOrderSyncer.new("something") }.to raise_error RuntimeError
|
||||
end
|
||||
end
|
||||
|
||||
describe "#sync!" do
|
||||
let(:now) { Time.zone.now }
|
||||
let(:schedule) { create(:schedule) }
|
||||
let(:closed_oc) { # Closed
|
||||
create(:simple_order_cycle, schedules: [schedule],
|
||||
orders_open_at: now - 1.minute,
|
||||
orders_close_at: now)
|
||||
}
|
||||
let(:open_oc_closes_before_begins_at_oc) { # Open, but closes before begins at
|
||||
create(:simple_order_cycle, schedules: [schedule],
|
||||
orders_open_at: now - 1.minute,
|
||||
orders_close_at: now + 59.seconds)
|
||||
}
|
||||
let(:open_oc) { # Open & closes between begins at and ends at
|
||||
create(:simple_order_cycle, schedules: [schedule],
|
||||
orders_open_at: now - 1.minute,
|
||||
orders_close_at: now + 90.seconds)
|
||||
}
|
||||
let(:upcoming_closes_before_begins_at_oc) { # Upcoming, but closes before begins at
|
||||
create(:simple_order_cycle, schedules: [schedule],
|
||||
orders_open_at: now + 30.seconds,
|
||||
orders_close_at: now + 59.seconds)
|
||||
}
|
||||
let(:upcoming_closes_on_begins_at_oc) { # Upcoming & closes on begins at
|
||||
create(:simple_order_cycle, schedules: [schedule],
|
||||
orders_open_at: now + 30.seconds,
|
||||
orders_close_at: now + 1.minute)
|
||||
}
|
||||
let(:upcoming_closes_on_ends_at_oc) { # Upcoming & closes on ends at
|
||||
create(:simple_order_cycle, schedules: [schedule],
|
||||
orders_open_at: now + 30.seconds,
|
||||
orders_close_at: now + 2.minutes)
|
||||
}
|
||||
let(:upcoming_closes_after_ends_at_oc) { # Upcoming & closes after ends at
|
||||
create(:simple_order_cycle, schedules: [schedule],
|
||||
orders_open_at: now + 30.seconds,
|
||||
orders_close_at: now + 121.seconds)
|
||||
}
|
||||
let(:subscription) {
|
||||
build(:subscription, schedule: schedule,
|
||||
begins_at: now + 1.minute,
|
||||
ends_at: now + 2.minutes)
|
||||
}
|
||||
let(:proxy_orders) { subscription.reload.proxy_orders }
|
||||
let(:order_cycles) { proxy_orders.map(&:order_cycle) }
|
||||
let(:syncer) { ProxyOrderSyncer.new(subscription) }
|
||||
|
||||
context "when the subscription is not persisted" do
|
||||
before do
|
||||
oc # Ensure oc is created before we attempt to sync
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
|
||||
end
|
||||
|
||||
context "and the schedule includes a closed oc (ie. closed before opens_at)" do
|
||||
let(:oc) { closed_oc }
|
||||
it "does not create a new proxy order for that oc" do
|
||||
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
|
||||
expect(order_cycles).to_not include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes an open oc that closes before begins_at" do
|
||||
let(:oc) { open_oc_closes_before_begins_at_oc }
|
||||
it "does not create a new proxy order for that oc" do
|
||||
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
|
||||
expect(order_cycles).to_not include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule has an open OC that closes between begins_at and ends_at" do
|
||||
let(:oc) { open_oc }
|
||||
it "creates a new proxy order for that oc" do
|
||||
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
|
||||
expect(order_cycles).to include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes upcoming oc that closes before begins_at" do
|
||||
let(:oc) { upcoming_closes_before_begins_at_oc }
|
||||
it "does not create a new proxy order for that oc" do
|
||||
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
|
||||
expect(order_cycles).to_not include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes upcoming oc that closes on begins_at" do
|
||||
let(:oc) { upcoming_closes_on_begins_at_oc }
|
||||
it "creates a new proxy order for that oc" do
|
||||
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
|
||||
expect(order_cycles).to include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes upcoming oc that closes after ends_at" do
|
||||
let(:oc) { upcoming_closes_on_ends_at_oc }
|
||||
it "creates a new proxy order for that oc" do
|
||||
expect{ subscription.save! }.to change(ProxyOrder, :count).from(0).to(1)
|
||||
expect(order_cycles).to include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes upcoming oc that closes after ends_at" do
|
||||
let(:oc) { upcoming_closes_after_ends_at_oc }
|
||||
it "does not create a new proxy order for that oc" do
|
||||
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
|
||||
expect(order_cycles).to_not include oc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the subscription is persisted" do
|
||||
before { expect(subscription.save!).to be true }
|
||||
|
||||
context "when a proxy order exists" do
|
||||
let!(:proxy_order) { create(:proxy_order, subscription: subscription, order_cycle: oc) }
|
||||
|
||||
context "for an oc included in the relevant schedule" do
|
||||
context "and the proxy order has already been placed" do
|
||||
before { proxy_order.update_attributes(placed_at: 5.minutes.ago) }
|
||||
|
||||
context "the oc is closed (ie. closed before opens_at)" do
|
||||
let(:oc) { closed_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes an open oc that closes before begins_at" do
|
||||
let(:oc) { open_oc_closes_before_begins_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is open and closes between begins_at and ends_at" do
|
||||
let(:oc) { open_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes before begins_at" do
|
||||
let(:oc) { upcoming_closes_before_begins_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes on begins_at" do
|
||||
let(:oc) { upcoming_closes_on_begins_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes on ends_at" do
|
||||
let(:oc) { upcoming_closes_on_ends_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes after ends_at" do
|
||||
let(:oc) { upcoming_closes_after_ends_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "and the proxy order has not already been placed" do
|
||||
context "the oc is closed (ie. closed before opens_at)" do
|
||||
let(:oc) { closed_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes an open oc that closes before begins_at" do
|
||||
let(:oc) { open_oc_closes_before_begins_at_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is open and closes between begins_at and ends_at" do
|
||||
let(:oc) { open_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes before begins_at" do
|
||||
let(:oc) { upcoming_closes_before_begins_at_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes on begins_at" do
|
||||
let(:oc) { upcoming_closes_on_begins_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes on ends_at" do
|
||||
let(:oc) { upcoming_closes_on_ends_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes after ends_at" do
|
||||
let(:oc) { upcoming_closes_after_ends_at_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for an oc not included in the relevant schedule" do
|
||||
let!(:proxy_order) {
|
||||
create(:proxy_order, subscription: subscription, order_cycle: open_oc)
|
||||
}
|
||||
before do
|
||||
open_oc.schedule_ids = []
|
||||
expect(open_oc.save!).to be true
|
||||
end
|
||||
|
||||
context "and the proxy order has already been placed" do
|
||||
before { proxy_order.update_attributes(placed_at: 5.minutes.ago) }
|
||||
|
||||
context "the oc is closed (ie. closed before opens_at)" do
|
||||
let(:oc) { closed_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes an open oc that closes before begins_at" do
|
||||
let(:oc) { open_oc_closes_before_begins_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is open and closes between begins_at and ends_at" do
|
||||
let(:oc) { open_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes before begins_at" do
|
||||
let(:oc) { upcoming_closes_before_begins_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes on begins_at" do
|
||||
let(:oc) { upcoming_closes_on_begins_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes on ends_at" do
|
||||
let(:oc) { upcoming_closes_on_ends_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes after ends_at" do
|
||||
let(:oc) { upcoming_closes_after_ends_at_oc }
|
||||
it "keeps the proxy order" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(1)
|
||||
expect(proxy_orders).to include proxy_order
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "and the proxy order has not already been placed" do
|
||||
# This shouldn't really happen, but it is possible
|
||||
context "the oc is closed (ie. closed before opens_at)" do
|
||||
let(:oc) { closed_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
# This shouldn't really happen, but it is possible
|
||||
context "and the oc is open and closes between begins_at and ends_at" do
|
||||
let(:oc) { open_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes before begins_at" do
|
||||
let(:oc) { upcoming_closes_before_begins_at_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes on begins_at" do
|
||||
let(:oc) { upcoming_closes_on_begins_at_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes on ends_at" do
|
||||
let(:oc) { upcoming_closes_on_ends_at_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
|
||||
context "and the oc is upcoming and closes after ends_at" do
|
||||
let(:oc) { upcoming_closes_after_ends_at_oc }
|
||||
it "removes the proxy order" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(1).to(0)
|
||||
expect(proxy_orders).to_not include proxy_order
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when a proxy order does not exist" do
|
||||
context "and the schedule includes a closed oc (ie. closed before opens_at)" do
|
||||
let!(:oc) { closed_oc }
|
||||
it "does not create a new proxy order for that oc" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
|
||||
expect(order_cycles).to_not include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes an open oc that closes before begins_at" do
|
||||
let(:oc) { open_oc_closes_before_begins_at_oc }
|
||||
it "does not create a new proxy order for that oc" do
|
||||
expect{ subscription.save! }.to_not change(ProxyOrder, :count).from(0)
|
||||
expect(order_cycles).to_not include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule has an open oc that closes between begins_at and ends_at" do
|
||||
let!(:oc) { open_oc }
|
||||
it "creates a new proxy order for that oc" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
|
||||
expect(order_cycles).to include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes upcoming oc that closes before begins_at" do
|
||||
let!(:oc) { upcoming_closes_before_begins_at_oc }
|
||||
it "does not create a new proxy order for that oc" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
|
||||
expect(order_cycles).to_not include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes upcoming oc that closes on begins_at" do
|
||||
let!(:oc) { upcoming_closes_on_begins_at_oc }
|
||||
it "creates a new proxy order for that oc" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
|
||||
expect(order_cycles).to include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes upcoming oc that closes on ends_at" do
|
||||
let!(:oc) { upcoming_closes_on_ends_at_oc }
|
||||
it "creates a new proxy order for that oc" do
|
||||
expect{ syncer.sync! }.to change(ProxyOrder, :count).from(0).to(1)
|
||||
expect(order_cycles).to include oc
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule includes upcoming oc that closes after ends_at" do
|
||||
let!(:oc) { upcoming_closes_after_ends_at_oc }
|
||||
it "does not create a new proxy order for that oc" do
|
||||
expect{ syncer.sync! }.to_not change(ProxyOrder, :count).from(0)
|
||||
expect(order_cycles).to_not include oc
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,132 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe Summarizer do
|
||||
let(:order) { create(:order) }
|
||||
let(:summarizer) { OrderManagement::Subscriptions::Summarizer.new }
|
||||
|
||||
before { allow(Rails.logger).to receive(:info) }
|
||||
|
||||
describe "#summary_for" do
|
||||
let(:order) { double(:order, distributor_id: 123) }
|
||||
|
||||
context "when a summary for the order's distributor doesn't already exist" do
|
||||
it "initializes a new summary object, and returns it" do
|
||||
expect(summarizer.instance_variable_get(:@summaries).count).to be 0
|
||||
summary = summarizer.__send__(:summary_for, order)
|
||||
expect(summary.shop_id).to be 123
|
||||
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
|
||||
end
|
||||
end
|
||||
|
||||
context "when a summary for the order's distributor already exists" do
|
||||
let(:summary) { double(:summary) }
|
||||
|
||||
before do
|
||||
summarizer.instance_variable_set(:@summaries, 123 => summary)
|
||||
end
|
||||
|
||||
it "returns the existing summary object" do
|
||||
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
|
||||
expect(summarizer.__send__(:summary_for, order)).to eq summary
|
||||
expect(summarizer.instance_variable_get(:@summaries).count).to be 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "recording events" do
|
||||
let(:order) { double(:order) }
|
||||
let(:summary) { double(:summary) }
|
||||
before { allow(summarizer).to receive(:summary_for).with(order) { summary } }
|
||||
|
||||
describe "#record_order" do
|
||||
it "requests a summary for the order and calls #record_order on it" do
|
||||
expect(summary).to receive(:record_order).with(order).once
|
||||
summarizer.record_order(order)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#record_success" do
|
||||
it "requests a summary for the order and calls #record_success on it" do
|
||||
expect(summary).to receive(:record_success).with(order).once
|
||||
summarizer.record_success(order)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#record_issue" do
|
||||
it "requests a summary for the order and calls #record_issue on it" do
|
||||
expect(order).to receive(:id)
|
||||
expect(summary).to receive(:record_issue).with(:type, order, "message").once
|
||||
summarizer.record_issue(:type, order, "message")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#record_and_log_error" do
|
||||
before do
|
||||
allow(order).to receive(:number) { "123" }
|
||||
end
|
||||
|
||||
context "when errors exist on the order" do
|
||||
before do
|
||||
allow(order).to receive(:errors) {
|
||||
double(:errors, any?: true, full_messages: ["Some error"])
|
||||
}
|
||||
end
|
||||
|
||||
it "sends error info to rails logger and calls #record_issue with an error message" do
|
||||
expect(summarizer).to receive(:record_issue).with(:processing,
|
||||
order, "Errors: Some error")
|
||||
summarizer.record_and_log_error(:processing, order)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no errors exist on the order" do
|
||||
before do
|
||||
allow(order).to receive(:errors) { double(:errors, any?: false) }
|
||||
end
|
||||
|
||||
it "falls back to calling record_issue" do
|
||||
expect(summarizer).to receive(:record_issue).with(:processing, order)
|
||||
summarizer.record_and_log_error(:processing, order)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_placement_summary_emails" do
|
||||
let(:summary1) { double(:summary) }
|
||||
let(:summary2) { double(:summary) }
|
||||
let(:summaries) { { 1 => summary1, 2 => summary2 } }
|
||||
let(:mail_mock) { double(:mail, deliver: true) }
|
||||
|
||||
before do
|
||||
summarizer.instance_variable_set(:@summaries, summaries)
|
||||
end
|
||||
|
||||
it "sends a placement summary email for each summary" do
|
||||
expect(SubscriptionMailer).to receive(:placement_summary_email).twice { mail_mock }
|
||||
summarizer.send_placement_summary_emails
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_confirmation_summary_emails" do
|
||||
let(:summary1) { double(:summary) }
|
||||
let(:summary2) { double(:summary) }
|
||||
let(:summaries) { { 1 => summary1, 2 => summary2 } }
|
||||
let(:mail_mock) { double(:mail, deliver: true) }
|
||||
|
||||
before do
|
||||
summarizer.instance_variable_set(:@summaries, summaries)
|
||||
end
|
||||
|
||||
it "sends a placement summary email for each summary" do
|
||||
expect(SubscriptionMailer).to receive(:confirmation_summary_email).twice { mail_mock }
|
||||
summarizer.send_confirmation_summary_emails
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,127 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe Summary do
|
||||
let(:summary) { OrderManagement::Subscriptions::Summary.new(123) }
|
||||
|
||||
describe "#initialize" do
|
||||
it "initializes instance variables: shop_id, order_count, success_count and issues" do
|
||||
expect(summary.shop_id).to be 123
|
||||
expect(summary.order_count).to be 0
|
||||
expect(summary.success_count).to be 0
|
||||
expect(summary.issues).to be_a Hash
|
||||
end
|
||||
end
|
||||
|
||||
describe "#record_order" do
|
||||
let(:order) { double(:order, id: 37) }
|
||||
it "adds the order id to the order_ids array" do
|
||||
summary.record_order(order)
|
||||
expect(summary.instance_variable_get(:@order_ids)).to eq [order.id]
|
||||
end
|
||||
end
|
||||
|
||||
describe "#record_success" do
|
||||
let(:order) { double(:order, id: 37) }
|
||||
it "adds the order id to the success_ids array" do
|
||||
summary.record_success(order)
|
||||
expect(summary.instance_variable_get(:@success_ids)).to eq [order.id]
|
||||
end
|
||||
end
|
||||
|
||||
describe "#record_issue" do
|
||||
let(:order) { double(:order, id: 1) }
|
||||
|
||||
context "when no issues of the same type have been recorded yet" do
|
||||
it "adds a new type to the issues hash, and stores a new issue against it" do
|
||||
summary.record_issue(:some_type, order, "message")
|
||||
expect(summary.issues.keys).to include :some_type
|
||||
expect(summary.issues[:some_type][order.id]).to eq "message"
|
||||
end
|
||||
end
|
||||
|
||||
context "when an issue of the same type has already been recorded" do
|
||||
let(:existing_issue) { double(:existing_issue) }
|
||||
|
||||
before { summary.issues[:some_type] = [existing_issue] }
|
||||
|
||||
it "stores a new issue against the existing type" do
|
||||
summary.record_issue(:some_type, order, "message")
|
||||
expect(summary.issues[:some_type]).to include existing_issue
|
||||
expect(summary.issues[:some_type][order.id]).to eq "message"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#order_count" do
|
||||
let(:order_ids) { [1, 2, 3, 4, 5, 6, 7] }
|
||||
it "counts the number of items in the order_ids instance_variable" do
|
||||
summary.instance_variable_set(:@order_ids, order_ids)
|
||||
expect(summary.order_count).to be 7
|
||||
end
|
||||
end
|
||||
|
||||
describe "#success_count" do
|
||||
let(:success_ids) { [1, 2, 3, 4, 5, 6, 7] }
|
||||
it "counts the number of items in the success_ids instance_variable" do
|
||||
summary.instance_variable_set(:@success_ids, success_ids)
|
||||
expect(summary.success_count).to be 7
|
||||
end
|
||||
end
|
||||
|
||||
describe "#issue_count" do
|
||||
let(:order_ids) { [1, 3, 5, 7, 9] }
|
||||
let(:success_ids) { [1, 2, 3, 4, 5] }
|
||||
|
||||
it "counts the number of items in order_ids that are not in success_ids" do
|
||||
summary.instance_variable_set(:@order_ids, order_ids)
|
||||
summary.instance_variable_set(:@success_ids, success_ids)
|
||||
expect(summary.issue_count).to be 2 # 7 & 9
|
||||
end
|
||||
end
|
||||
|
||||
describe "#orders_affected_by" do
|
||||
let(:order1) { create(:order) }
|
||||
let(:order2) { create(:order) }
|
||||
|
||||
before do
|
||||
allow(summary).to receive(:unrecorded_ids) { [order1.id] }
|
||||
allow(summary).to receive(:issues) { { failure: { order2.id => "A message" } } }
|
||||
end
|
||||
|
||||
context "when the issue type is :other" do
|
||||
let(:orders) { summary.orders_affected_by(:other) }
|
||||
|
||||
it "returns orders specified by unrecorded_ids" do
|
||||
expect(orders).to include order1
|
||||
expect(orders).to_not include order2
|
||||
end
|
||||
end
|
||||
|
||||
context "when the issue type is :other" do
|
||||
let(:orders) { summary.orders_affected_by(:failure) }
|
||||
|
||||
it "returns orders specified by the relevant issue hash" do
|
||||
expect(orders).to include order2
|
||||
expect(orders).to_not include order1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#unrecorded_ids" do
|
||||
let(:issues) { { type: { 7 => "message", 8 => "message" } } }
|
||||
|
||||
before do
|
||||
summary.instance_variable_set(:@order_ids, [1, 3, 5, 7, 9])
|
||||
summary.instance_variable_set(:@success_ids, [1, 2, 3, 4, 5])
|
||||
summary.instance_variable_set(:@issues, issues)
|
||||
end
|
||||
|
||||
it "returns order_ids that are not marked as an issue or a success" do
|
||||
expect(summary.unrecorded_ids).to eq [9]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,524 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe Validator do
|
||||
let(:owner) { create(:user) }
|
||||
let(:shop) { create(:enterprise, name: "Shop", owner: owner) }
|
||||
|
||||
describe "delegation" do
|
||||
let(:subscription) { create(:subscription, shop: shop) }
|
||||
let(:validator) { Validator.new(subscription) }
|
||||
|
||||
it "delegates to subscription" do
|
||||
expect(validator.shop).to eq subscription.shop
|
||||
expect(validator.customer).to eq subscription.customer
|
||||
expect(validator.schedule).to eq subscription.schedule
|
||||
expect(validator.shipping_method).to eq subscription.shipping_method
|
||||
expect(validator.payment_method).to eq subscription.payment_method
|
||||
expect(validator.bill_address).to eq subscription.bill_address
|
||||
expect(validator.ship_address).to eq subscription.ship_address
|
||||
expect(validator.begins_at).to eq subscription.begins_at
|
||||
expect(validator.ends_at).to eq subscription.ends_at
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
let(:subscription_stubs) do
|
||||
{
|
||||
shop: shop,
|
||||
customer: true,
|
||||
schedule: true,
|
||||
shipping_method: true,
|
||||
payment_method: true,
|
||||
bill_address: true,
|
||||
ship_address: true,
|
||||
begins_at: true,
|
||||
ends_at: true,
|
||||
}
|
||||
end
|
||||
|
||||
let(:validation_stubs) do
|
||||
{
|
||||
shipping_method_allowed?: true,
|
||||
payment_method_allowed?: true,
|
||||
payment_method_type_allowed?: true,
|
||||
ends_at_after_begins_at?: true,
|
||||
customer_allowed?: true,
|
||||
schedule_allowed?: true,
|
||||
credit_card_ok?: true,
|
||||
subscription_line_items_present?: true,
|
||||
requested_variants_available?: true
|
||||
}
|
||||
end
|
||||
|
||||
let(:subscription) { instance_double(Subscription, subscription_stubs) }
|
||||
let(:validator) { OrderManagement::Subscriptions::Validator.new(subscription) }
|
||||
|
||||
def stub_validations(validator, methods)
|
||||
methods.each do |name, value|
|
||||
allow(validator).to receive(name) { value }
|
||||
end
|
||||
end
|
||||
|
||||
describe "shipping method validation" do
|
||||
let(:subscription) {
|
||||
instance_double(Subscription, subscription_stubs.except(:shipping_method))
|
||||
}
|
||||
before { stub_validations(validator, validation_stubs.except(:shipping_method_allowed?)) }
|
||||
|
||||
context "when no shipping method is present" do
|
||||
before { expect(subscription).to receive(:shipping_method).at_least(:once) { nil } }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:shipping_method]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when a shipping method is present" do
|
||||
let(:shipping_method) { instance_double(Spree::ShippingMethod, distributors: [shop]) }
|
||||
before {
|
||||
expect(subscription).to receive(:shipping_method).at_least(:once) { shipping_method }
|
||||
}
|
||||
|
||||
context "and the shipping method is not associated with the shop" do
|
||||
before { allow(shipping_method).to receive(:distributors) { [double(:enterprise)] } }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:shipping_method]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "and the shipping method is associated with the shop" do
|
||||
before { allow(shipping_method).to receive(:distributors) { [shop] } }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:shipping_method]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "payment method validation" do
|
||||
let(:subscription) {
|
||||
instance_double(Subscription, subscription_stubs.except(:payment_method))
|
||||
}
|
||||
before { stub_validations(validator, validation_stubs.except(:payment_method_allowed?)) }
|
||||
|
||||
context "when no payment method is present" do
|
||||
before { expect(subscription).to receive(:payment_method).at_least(:once) { nil } }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:payment_method]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when a payment method is present" do
|
||||
let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) }
|
||||
before {
|
||||
expect(subscription).to receive(:payment_method).at_least(:once) { payment_method }
|
||||
}
|
||||
|
||||
context "and the payment method is not associated with the shop" do
|
||||
before { allow(payment_method).to receive(:distributors) { [double(:enterprise)] } }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:payment_method]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "and the payment method is associated with the shop" do
|
||||
before { allow(payment_method).to receive(:distributors) { [shop] } }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:payment_method]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "payment method type validation" do
|
||||
let(:subscription) {
|
||||
instance_double(Subscription, subscription_stubs.except(:payment_method))
|
||||
}
|
||||
before {
|
||||
stub_validations(validator, validation_stubs.except(:payment_method_type_allowed?))
|
||||
}
|
||||
|
||||
context "when a payment method is present" do
|
||||
let(:payment_method) { instance_double(Spree::PaymentMethod, distributors: [shop]) }
|
||||
before {
|
||||
expect(subscription).to receive(:payment_method).at_least(:once) { payment_method }
|
||||
}
|
||||
|
||||
context "and the payment method type is not in the approved list" do
|
||||
before { allow(payment_method).to receive(:type) { "Blah" } }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:payment_method]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "and the payment method is in the approved list" do
|
||||
let(:approved_type) { Subscription::ALLOWED_PAYMENT_METHOD_TYPES.first }
|
||||
before { allow(payment_method).to receive(:type) { approved_type } }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:payment_method]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "dates" do
|
||||
let(:subscription) {
|
||||
instance_double(Subscription, subscription_stubs.except(:begins_at, :ends_at))
|
||||
}
|
||||
before { stub_validations(validator, validation_stubs.except(:ends_at_after_begins_at?)) }
|
||||
before { expect(subscription).to receive(:begins_at).at_least(:once) { begins_at } }
|
||||
|
||||
context "when no begins_at is present" do
|
||||
let(:begins_at) { nil }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:begins_at]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when a start date is present" do
|
||||
let(:begins_at) { Time.zone.today }
|
||||
before { expect(subscription).to receive(:ends_at).at_least(:once) { ends_at } }
|
||||
|
||||
context "when no ends_at is present" do
|
||||
let(:ends_at) { nil }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:ends_at]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when ends_at is equal to begins_at" do
|
||||
let(:ends_at) { Time.zone.today }
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:ends_at]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when ends_at is before begins_at" do
|
||||
let(:ends_at) { Time.zone.today - 1.day }
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:ends_at]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when ends_at is after begins_at" do
|
||||
let(:ends_at) { Time.zone.today + 1.day }
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:ends_at]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "addresses" do
|
||||
before { stub_validations(validator, validation_stubs) }
|
||||
let(:subscription) {
|
||||
instance_double(Subscription, subscription_stubs.except(:bill_address, :ship_address))
|
||||
}
|
||||
before { expect(subscription).to receive(:bill_address).at_least(:once) { bill_address } }
|
||||
before { expect(subscription).to receive(:ship_address).at_least(:once) { ship_address } }
|
||||
|
||||
context "when bill_address and ship_address are not present" do
|
||||
let(:bill_address) { nil }
|
||||
let(:ship_address) { nil }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:bill_address]).to_not be_empty
|
||||
expect(validator.errors[:ship_address]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when bill_address and ship_address are present" do
|
||||
let(:bill_address) { instance_double(Spree::Address) }
|
||||
let(:ship_address) { instance_double(Spree::Address) }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:bill_address]).to be_empty
|
||||
expect(validator.errors[:ship_address]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "customer" do
|
||||
let(:subscription) { instance_double(Subscription, subscription_stubs.except(:customer)) }
|
||||
before { stub_validations(validator, validation_stubs.except(:customer_allowed?)) }
|
||||
before { expect(subscription).to receive(:customer).at_least(:once) { customer } }
|
||||
|
||||
context "when no customer is present" do
|
||||
let(:customer) { nil }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:customer]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when a customer is present" do
|
||||
let(:customer) { instance_double(Customer) }
|
||||
|
||||
context "and the customer is not associated with the shop" do
|
||||
before { allow(customer).to receive(:enterprise) { double(:enterprise) } }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:customer]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "and the customer is associated with the shop" do
|
||||
before { allow(customer).to receive(:enterprise) { shop } }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:customer]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "schedule" do
|
||||
let(:subscription) { instance_double(Subscription, subscription_stubs.except(:schedule)) }
|
||||
before { stub_validations(validator, validation_stubs.except(:schedule_allowed?)) }
|
||||
before { expect(subscription).to receive(:schedule).at_least(:once) { schedule } }
|
||||
|
||||
context "when no schedule is present" do
|
||||
let(:schedule) { nil }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:schedule]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when a schedule is present" do
|
||||
let(:schedule) { instance_double(Schedule) }
|
||||
|
||||
context "and the schedule is not associated with the shop" do
|
||||
before { allow(schedule).to receive(:coordinators) { [double(:enterprise)] } }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:schedule]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "and the schedule is associated with the shop" do
|
||||
before { allow(schedule).to receive(:coordinators) { [shop] } }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:schedule]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "credit card" do
|
||||
let(:subscription) {
|
||||
instance_double(Subscription, subscription_stubs.except(:payment_method))
|
||||
}
|
||||
before { stub_validations(validator, validation_stubs.except(:credit_card_ok?)) }
|
||||
before {
|
||||
expect(subscription).to receive(:payment_method).at_least(:once) { payment_method }
|
||||
}
|
||||
|
||||
context "when using a Check payment method" do
|
||||
let(:payment_method) {
|
||||
instance_double(Spree::PaymentMethod, type: "Spree::PaymentMethod::Check")
|
||||
}
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:subscription_line_items]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when using the StripeConnect payment gateway" do
|
||||
let(:payment_method) {
|
||||
instance_double(Spree::PaymentMethod, type: "Spree::Gateway::StripeConnect")
|
||||
}
|
||||
before { expect(subscription).to receive(:customer).at_least(:once) { customer } }
|
||||
|
||||
context "when the customer does not allow charges" do
|
||||
let(:customer) { instance_double(Customer, allow_charges: false) }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:payment_method]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when the customer allows charges" do
|
||||
let(:customer) { instance_double(Customer, allow_charges: true) }
|
||||
|
||||
context "and the customer is not associated with a user" do
|
||||
before { allow(customer).to receive(:user) { nil } }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:payment_method]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "and the customer is associated with a user" do
|
||||
before { expect(customer).to receive(:user).once { user } }
|
||||
|
||||
context "and the user has no default card set" do
|
||||
let(:user) { instance_double(Spree::User, default_card: nil) }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:payment_method]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "and the user has a default card set" do
|
||||
let(:user) { instance_double(Spree::User, default_card: 'some card') }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:payment_method]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "subscription line items" do
|
||||
let(:subscription) { instance_double(Subscription, subscription_stubs) }
|
||||
before {
|
||||
stub_validations(validator, validation_stubs.except(:subscription_line_items_present?))
|
||||
}
|
||||
before {
|
||||
expect(subscription).to receive(:subscription_line_items).at_least(:once) {
|
||||
subscription_line_items
|
||||
}
|
||||
}
|
||||
|
||||
context "when no subscription line items exist" do
|
||||
let(:subscription_line_items) { [] }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:subscription_line_items]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when subscription line items exist but all are marked for destruction" do
|
||||
let(:subscription_line_item1) {
|
||||
instance_double(SubscriptionLineItem, marked_for_destruction?: true)
|
||||
}
|
||||
let(:subscription_line_items) { [subscription_line_item1] }
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:subscription_line_items]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when subscription line items exist and some are not marked for destruction" do
|
||||
let(:subscription_line_item1) {
|
||||
instance_double(SubscriptionLineItem, marked_for_destruction?: true)
|
||||
}
|
||||
let(:subscription_line_item2) {
|
||||
instance_double(SubscriptionLineItem, marked_for_destruction?: false)
|
||||
}
|
||||
let(:subscription_line_items) { [subscription_line_item1, subscription_line_item2] }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:subscription_line_items]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "variant availability" do
|
||||
let(:subscription) { instance_double(Subscription, subscription_stubs) }
|
||||
before {
|
||||
stub_validations(validator, validation_stubs.except(:requested_variants_available?))
|
||||
}
|
||||
before {
|
||||
expect(subscription).to receive(:subscription_line_items).at_least(:once) {
|
||||
subscription_line_items
|
||||
}
|
||||
}
|
||||
|
||||
context "when no subscription line items exist" do
|
||||
let(:subscription_line_items) { [] }
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:subscription_line_items]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "when subscription line items exist" do
|
||||
let(:variant1) { instance_double(Spree::Variant, id: 1) }
|
||||
let(:variant2) { instance_double(Spree::Variant, id: 2) }
|
||||
let(:subscription_line_item1) {
|
||||
instance_double(SubscriptionLineItem, variant: variant1)
|
||||
}
|
||||
let(:subscription_line_item2) {
|
||||
instance_double(SubscriptionLineItem, variant: variant2)
|
||||
}
|
||||
let(:subscription_line_items) { [subscription_line_item1] }
|
||||
|
||||
context "but some variants are unavailable" do
|
||||
let(:product) { instance_double(Spree::Product, name: "some_name") }
|
||||
|
||||
before do
|
||||
allow(validator).to receive(:available_variant_ids) { [variant2.id] }
|
||||
allow(variant1).to receive(:product) { product }
|
||||
allow(variant1).to receive(:full_name) { "some name" }
|
||||
end
|
||||
|
||||
it "adds an error and returns false" do
|
||||
expect(validator.valid?).to be false
|
||||
expect(validator.errors[:subscription_line_items]).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context "and all requested variants are available" do
|
||||
before do
|
||||
allow(validator).to receive(:available_variant_ids) { [variant1.id, variant2.id] }
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
expect(validator.valid?).to be true
|
||||
expect(validator.errors[:subscription_line_items]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,165 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
module OrderManagement
|
||||
module Subscriptions
|
||||
describe VariantsList do
|
||||
describe "variant eligibility for subscription" do
|
||||
let!(:shop) { create(:distributor_enterprise) }
|
||||
let!(:producer) { create(:supplier_enterprise) }
|
||||
let!(:product) { create(:product, supplier: producer) }
|
||||
let!(:variant) { product.variants.first }
|
||||
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
let!(:subscription) { create(:subscription, shop: shop, schedule: schedule) }
|
||||
let!(:subscription_line_item) do
|
||||
create(:subscription_line_item, subscription: subscription, variant: variant)
|
||||
end
|
||||
|
||||
let(:current_order_cycle) do
|
||||
create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.ago,
|
||||
orders_close_at: 1.week.from_now)
|
||||
end
|
||||
|
||||
let(:future_order_cycle) do
|
||||
create(:simple_order_cycle, coordinator: shop, orders_open_at: 1.week.from_now,
|
||||
orders_close_at: 2.weeks.from_now)
|
||||
end
|
||||
|
||||
let(:past_order_cycle) do
|
||||
create(:simple_order_cycle, coordinator: shop, orders_open_at: 2.weeks.ago,
|
||||
orders_close_at: 1.week.ago)
|
||||
end
|
||||
|
||||
let!(:order_cycle) { current_order_cycle }
|
||||
|
||||
context "if the shop is the supplier for the product" do
|
||||
let!(:producer) { shop }
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if the supplier is permitted for the shop" do
|
||||
let!(:enterprise_relationship) {
|
||||
create(:enterprise_relationship, child: shop,
|
||||
parent: product.supplier,
|
||||
permissions_list: [:add_to_order_cycle])
|
||||
}
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if the variant is involved in an exchange" do
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
|
||||
context "if it is an incoming exchange where the shop is the receiver" do
|
||||
let!(:incoming_exchange) {
|
||||
order_cycle.exchanges.create(sender: product.supplier,
|
||||
receiver: shop,
|
||||
incoming: true, variants: [variant])
|
||||
}
|
||||
|
||||
it "is not eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to_not include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if it is an outgoing exchange where the shop is the receiver" do
|
||||
let!(:outgoing_exchange) {
|
||||
order_cycle.exchanges.create(sender: product.supplier,
|
||||
receiver: shop,
|
||||
incoming: false,
|
||||
variants: [variant])
|
||||
}
|
||||
|
||||
context "if the order cycle is currently open" do
|
||||
let!(:order_cycle) { current_order_cycle }
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if the order cycle opens in the future" do
|
||||
let!(:order_cycle) { future_order_cycle }
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if the order cycle closed in the past" do
|
||||
let!(:order_cycle) { past_order_cycle }
|
||||
|
||||
it "is eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to include(variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "if the variant is unrelated" do
|
||||
it "is not eligible" do
|
||||
expect(described_class.eligible_variants(shop)).to_not include(variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "checking if variant in open and upcoming order cycles" do
|
||||
let!(:shop) { create(:enterprise) }
|
||||
let!(:product) { create(:product) }
|
||||
let!(:variant) { product.variants.first }
|
||||
let!(:schedule) { create(:schedule) }
|
||||
|
||||
context "if the variant is involved in an exchange" do
|
||||
let!(:order_cycle) { create(:simple_order_cycle, coordinator: shop) }
|
||||
let!(:schedule) { create(:schedule, order_cycles: [order_cycle]) }
|
||||
|
||||
context "if it is an incoming exchange where the shop is the receiver" do
|
||||
let!(:incoming_exchange) {
|
||||
order_cycle.exchanges.create(sender: product.supplier,
|
||||
receiver: shop,
|
||||
incoming: true,
|
||||
variants: [variant])
|
||||
}
|
||||
|
||||
it "is is false" do
|
||||
expect(described_class).not_to be_in_open_and_upcoming_order_cycles(shop,
|
||||
schedule,
|
||||
variant)
|
||||
end
|
||||
end
|
||||
|
||||
context "if it is an outgoing exchange where the shop is the receiver" do
|
||||
let!(:outgoing_exchange) {
|
||||
order_cycle.exchanges.create(sender: product.supplier,
|
||||
receiver: shop,
|
||||
incoming: false,
|
||||
variants: [variant])
|
||||
}
|
||||
|
||||
it "is true" do
|
||||
expect(described_class).to be_in_open_and_upcoming_order_cycles(shop,
|
||||
schedule,
|
||||
variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "if the variant is unrelated" do
|
||||
it "is false" do
|
||||
expect(described_class).to_not be_in_open_and_upcoming_order_cycles(shop,
|
||||
schedule,
|
||||
variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,95 +0,0 @@
|
||||
module OpenFoodNetwork
|
||||
class ProxyOrderSyncer
|
||||
attr_reader :subscription
|
||||
|
||||
delegate :order_cycles, :proxy_orders, :begins_at, :ends_at, to: :subscription
|
||||
|
||||
def initialize(subscriptions)
|
||||
case subscriptions
|
||||
when Subscription
|
||||
@subscription = subscriptions
|
||||
when ActiveRecord::Relation
|
||||
@subscriptions = subscriptions.not_ended.not_canceled
|
||||
else
|
||||
raise "ProxyOrderSyncer must be initialized with " \
|
||||
"an instance of Subscription or ActiveRecord::Relation"
|
||||
end
|
||||
end
|
||||
|
||||
def sync!
|
||||
return sync_subscriptions! if @subscriptions
|
||||
|
||||
return initialise_proxy_orders! unless @subscription.id
|
||||
|
||||
sync_subscription!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sync_subscriptions!
|
||||
@subscriptions.each do |subscription|
|
||||
@subscription = subscription
|
||||
sync_subscription!
|
||||
end
|
||||
end
|
||||
|
||||
def initialise_proxy_orders!
|
||||
uninitialised_order_cycle_ids.each do |order_cycle_id|
|
||||
Rails.logger.info "Initializing Proxy Order " \
|
||||
"of subscription #{@subscription.id} in order cycle #{order_cycle_id}"
|
||||
proxy_orders << ProxyOrder.new(subscription: subscription, order_cycle_id: order_cycle_id)
|
||||
end
|
||||
end
|
||||
|
||||
def sync_subscription!
|
||||
Rails.logger.info "Syncing Proxy Orders of subscription #{@subscription.id}"
|
||||
create_proxy_orders!
|
||||
remove_orphaned_proxy_orders!
|
||||
end
|
||||
|
||||
def create_proxy_orders!
|
||||
return unless not_closed_in_range_order_cycles.any?
|
||||
|
||||
query = "INSERT INTO proxy_orders (subscription_id, order_cycle_id, updated_at, created_at)"
|
||||
query << " VALUES #{insert_values}"
|
||||
query << " ON CONFLICT DO NOTHING"
|
||||
|
||||
ActiveRecord::Base.connection.exec_query(query)
|
||||
end
|
||||
|
||||
def uninitialised_order_cycle_ids
|
||||
not_closed_in_range_order_cycles.pluck(:id) - proxy_orders.map(&:order_cycle_id)
|
||||
end
|
||||
|
||||
def remove_orphaned_proxy_orders!
|
||||
orphaned_proxy_orders.scoped.delete_all
|
||||
end
|
||||
|
||||
# Remove Proxy Orders that have not been placed yet
|
||||
# and are in Order Cycles that are out of range
|
||||
def orphaned_proxy_orders
|
||||
orphaned = proxy_orders.where(placed_at: nil)
|
||||
order_cycle_ids = in_range_order_cycles.pluck(:id)
|
||||
return orphaned unless order_cycle_ids.any?
|
||||
|
||||
orphaned.where('order_cycle_id NOT IN (?)', order_cycle_ids)
|
||||
end
|
||||
|
||||
def insert_values
|
||||
now = Time.now.utc.iso8601
|
||||
not_closed_in_range_order_cycles
|
||||
.map{ |oc| "(#{subscription.id},#{oc.id},'#{now}','#{now}')" }
|
||||
.join(",")
|
||||
end
|
||||
|
||||
def not_closed_in_range_order_cycles
|
||||
in_range_order_cycles.merge(OrderCycle.not_closed)
|
||||
end
|
||||
|
||||
def in_range_order_cycles
|
||||
order_cycles.where("orders_close_at >= ? AND orders_close_at <= ?",
|
||||
begins_at,
|
||||
ends_at || 100.years.from_now)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -61,7 +61,7 @@ module OpenFoodNetwork
|
||||
end
|
||||
|
||||
def scope_to_eligible_for_subscriptions_in_distributor
|
||||
eligible_variants_scope = SubscriptionVariantsService.eligible_variants(distributor)
|
||||
eligible_variants_scope = OrderManagement::Subscriptions::VariantsList.eligible_variants(distributor)
|
||||
@variants = @variants.merge(eligible_variants_scope)
|
||||
scope_variants_to_distributor(@variants, distributor)
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user