mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-09 03:20:21 +00:00
Compare commits
1 Commits
v5.0.12
...
RachL-patc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5020cc740 |
@@ -16,7 +16,6 @@ SECRET_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|||||||
|
|
||||||
OFN_REDIS_URL="redis://localhost:6379/1"
|
OFN_REDIS_URL="redis://localhost:6379/1"
|
||||||
OFN_REDIS_JOBS_URL="redis://localhost:6379/2"
|
OFN_REDIS_JOBS_URL="redis://localhost:6379/2"
|
||||||
OFN_REDIS_CABLE_URL="redis://localhost:6379/0"
|
|
||||||
|
|
||||||
SITE_URL="0.0.0.0:3000"
|
SITE_URL="0.0.0.0:3000"
|
||||||
|
|
||||||
|
|||||||
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -38,10 +38,10 @@ jobs:
|
|||||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||||
ci_node_total: [4]
|
ci_node_total: [8]
|
||||||
# Indexes for parallel jobs (starting from zero).
|
# Indexes for parallel jobs (starting from zero).
|
||||||
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
||||||
ci_node_index: [0, 1, 2, 3]
|
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -116,10 +116,10 @@ jobs:
|
|||||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||||
ci_node_total: [2]
|
ci_node_total: [4]
|
||||||
# Indexes for parallel jobs (starting from zero).
|
# Indexes for parallel jobs (starting from zero).
|
||||||
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
||||||
ci_node_index: [0, 1]
|
ci_node_index: [0, 1, 2, 3]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -184,10 +184,10 @@ jobs:
|
|||||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||||
ci_node_total: [10]
|
ci_node_total: [14]
|
||||||
# Indexes for parallel jobs (starting from zero).
|
# Indexes for parallel jobs (starting from zero).
|
||||||
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
||||||
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -271,10 +271,10 @@ jobs:
|
|||||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||||
ci_node_total: [6]
|
ci_node_total: [12]
|
||||||
# Indexes for parallel jobs (starting from zero).
|
# Indexes for parallel jobs (starting from zero).
|
||||||
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
||||||
ci_node_index: [0, 1, 2, 3, 4, 5]
|
ci_node_index: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -437,10 +437,10 @@ jobs:
|
|||||||
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
# [n] - where the n is a number of parallel jobs you want to run your tests on.
|
||||||
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
# Use a higher number if you have slow tests to split them between more parallel jobs.
|
||||||
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
# Remember to update the value of the `ci_node_index` below to (0..n-1).
|
||||||
ci_node_total: [3]
|
ci_node_total: [5]
|
||||||
# Indexes for parallel jobs (starting from zero).
|
# Indexes for parallel jobs (starting from zero).
|
||||||
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
# E.g. use [0, 1] for 2 parallel jobs, [0, 1, 2] for 3 parallel jobs, etc.
|
||||||
ci_node_index: [0, 1, 2]
|
ci_node_index: [0, 1, 2, 3, 4]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
@@ -553,6 +553,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
pattern: simplecov-chunk-*
|
pattern: simplecov-chunk-*
|
||||||
path: tmp/simplecov
|
path: tmp/simplecov
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
- name: collate results from each of the workers
|
- name: collate results from each of the workers
|
||||||
run: bundle exec rake 'simplecov:collate_results[tmp/simplecov]'
|
run: bundle exec rake 'simplecov:collate_results[tmp/simplecov]'
|
||||||
|
|||||||
@@ -3,14 +3,10 @@
|
|||||||
# These are the rules we agreed upon and we work towards.
|
# These are the rules we agreed upon and we work towards.
|
||||||
AllCops:
|
AllCops:
|
||||||
NewCops: enable
|
NewCops: enable
|
||||||
MigratedSchemaVersion: "20250111000000"
|
|
||||||
Exclude:
|
Exclude:
|
||||||
- bin/**/*
|
- bin/**/*
|
||||||
|
- db/**/*
|
||||||
- config/**/*
|
- config/**/*
|
||||||
- db/bad_migrations/*
|
|
||||||
- db/migrate/201*
|
|
||||||
- db/migrate/202[0-4]*
|
|
||||||
- db/schema.rb
|
|
||||||
- script/**/*
|
- script/**/*
|
||||||
- vendor/**/*
|
- vendor/**/*
|
||||||
- node_modules/**/*
|
- node_modules/**/*
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ Lint/EmptyClass:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'spec/lib/reports/report_loader_spec.rb'
|
- 'spec/lib/reports/report_loader_spec.rb'
|
||||||
|
|
||||||
|
# Offense count: 1
|
||||||
|
# Configuration parameters: AllowComments.
|
||||||
|
Lint/EmptyFile:
|
||||||
|
Exclude:
|
||||||
|
- 'spec/lib/open_food_network/enterprise_injection_data_spec.rb'
|
||||||
|
|
||||||
# Offense count: 2
|
# Offense count: 2
|
||||||
Lint/FloatComparison:
|
Lint/FloatComparison:
|
||||||
Exclude:
|
Exclude:
|
||||||
@@ -86,6 +92,7 @@ Metrics/AbcSize:
|
|||||||
- 'app/controllers/admin/enterprises_controller.rb'
|
- 'app/controllers/admin/enterprises_controller.rb'
|
||||||
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
||||||
- 'app/controllers/spree/admin/payments_controller.rb'
|
- 'app/controllers/spree/admin/payments_controller.rb'
|
||||||
|
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||||
- 'app/controllers/spree/admin/variants_controller.rb'
|
- 'app/controllers/spree/admin/variants_controller.rb'
|
||||||
- 'app/controllers/spree/orders_controller.rb'
|
- 'app/controllers/spree/orders_controller.rb'
|
||||||
- 'app/helpers/spree/admin/navigation_helper.rb'
|
- 'app/helpers/spree/admin/navigation_helper.rb'
|
||||||
@@ -120,7 +127,7 @@ Metrics/BlockNesting:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/spree/payment/processing.rb'
|
- 'app/models/spree/payment/processing.rb'
|
||||||
|
|
||||||
# Offense count: 47
|
# Offense count: 46
|
||||||
# Configuration parameters: CountComments, Max, CountAsOne.
|
# Configuration parameters: CountComments, Max, CountAsOne.
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
@@ -130,7 +137,6 @@ Metrics/ClassLength:
|
|||||||
- 'app/controllers/admin/resource_controller.rb'
|
- 'app/controllers/admin/resource_controller.rb'
|
||||||
- 'app/controllers/admin/subscriptions_controller.rb'
|
- 'app/controllers/admin/subscriptions_controller.rb'
|
||||||
- 'app/controllers/application_controller.rb'
|
- 'app/controllers/application_controller.rb'
|
||||||
- 'app/controllers/checkout_controller.rb'
|
|
||||||
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
||||||
- 'app/controllers/spree/admin/orders_controller.rb'
|
- 'app/controllers/spree/admin/orders_controller.rb'
|
||||||
- 'app/controllers/spree/admin/payment_methods_controller.rb'
|
- 'app/controllers/spree/admin/payment_methods_controller.rb'
|
||||||
@@ -177,7 +183,7 @@ Metrics/ClassLength:
|
|||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/admin/enterprises_controller.rb'
|
- 'app/controllers/admin/enterprises_controller.rb'
|
||||||
- 'app/controllers/spree/admin/payments_controller.rb'
|
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||||
- 'app/controllers/spree/orders_controller.rb'
|
- 'app/controllers/spree/orders_controller.rb'
|
||||||
- 'app/helpers/checkout_helper.rb'
|
- 'app/helpers/checkout_helper.rb'
|
||||||
- 'app/helpers/order_cycles_helper.rb'
|
- 'app/helpers/order_cycles_helper.rb'
|
||||||
@@ -202,12 +208,13 @@ Metrics/CyclomaticComplexity:
|
|||||||
- 'lib/spree/localized_number.rb'
|
- 'lib/spree/localized_number.rb'
|
||||||
- 'spec/models/product_importer_spec.rb'
|
- 'spec/models/product_importer_spec.rb'
|
||||||
|
|
||||||
# Offense count: 23
|
# Offense count: 24
|
||||||
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
|
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/admin/enterprises_controller.rb'
|
- 'app/controllers/admin/enterprises_controller.rb'
|
||||||
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
- 'app/controllers/payment_gateways/paypal_controller.rb'
|
||||||
|
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||||
- 'app/controllers/spree/orders_controller.rb'
|
- 'app/controllers/spree/orders_controller.rb'
|
||||||
- 'app/helpers/spree/admin/navigation_helper.rb'
|
- 'app/helpers/spree/admin/navigation_helper.rb'
|
||||||
- 'app/models/spree/ability.rb'
|
- 'app/models/spree/ability.rb'
|
||||||
@@ -286,17 +293,19 @@ Metrics/ParameterLists:
|
|||||||
- 'spec/support/controller_requests_helper.rb'
|
- 'spec/support/controller_requests_helper.rb'
|
||||||
- 'spec/system/admin/reports_spec.rb'
|
- 'spec/system/admin/reports_spec.rb'
|
||||||
|
|
||||||
# Offense count: 3
|
# Offense count: 4
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
- 'app/controllers/spree/admin/taxons_controller.rb'
|
||||||
- 'app/models/enterprise_relationship.rb'
|
- 'app/models/enterprise_relationship.rb'
|
||||||
- 'app/models/spree/ability.rb'
|
- 'app/models/spree/ability.rb'
|
||||||
- 'app/models/spree/order/checkout.rb'
|
- 'app/models/spree/order/checkout.rb'
|
||||||
|
|
||||||
# Offense count: 7
|
# Offense count: 8
|
||||||
Naming/AccessorMethodName:
|
Naming/AccessorMethodName:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
- 'app/controllers/spree/admin/taxonomies_controller.rb'
|
||||||
- 'app/mailers/producer_mailer.rb'
|
- 'app/mailers/producer_mailer.rb'
|
||||||
- 'app/models/spree/order.rb'
|
- 'app/models/spree/order.rb'
|
||||||
- 'app/services/checkout/post_checkout_actions.rb'
|
- 'app/services/checkout/post_checkout_actions.rb'
|
||||||
@@ -344,7 +353,7 @@ Naming/VariableNumber:
|
|||||||
- 'spec/models/spree/tax_rate_spec.rb'
|
- 'spec/models/spree/tax_rate_spec.rb'
|
||||||
- 'spec/requests/api/orders_spec.rb'
|
- 'spec/requests/api/orders_spec.rb'
|
||||||
|
|
||||||
# Offense count: 143
|
# Offense count: 142
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
# Configuration parameters: ResponseMethods.
|
# Configuration parameters: ResponseMethods.
|
||||||
# ResponseMethods: response, last_response
|
# ResponseMethods: response, last_response
|
||||||
@@ -548,7 +557,7 @@ RSpecRails/InferredSpecType:
|
|||||||
- 'spec/requests/voucher_adjustments_spec.rb'
|
- 'spec/requests/voucher_adjustments_spec.rb'
|
||||||
- 'spec/routing/stripe_spec.rb'
|
- 'spec/routing/stripe_spec.rb'
|
||||||
|
|
||||||
# Offense count: 21
|
# Offense count: 22
|
||||||
# Configuration parameters: IgnoreScopes, Include.
|
# Configuration parameters: IgnoreScopes, Include.
|
||||||
# Include: app/models/**/*.rb
|
# Include: app/models/**/*.rb
|
||||||
Rails/InverseOf:
|
Rails/InverseOf:
|
||||||
@@ -563,6 +572,7 @@ Rails/InverseOf:
|
|||||||
- 'app/models/spree/price.rb'
|
- 'app/models/spree/price.rb'
|
||||||
- 'app/models/spree/product.rb'
|
- 'app/models/spree/product.rb'
|
||||||
- 'app/models/spree/stock_item.rb'
|
- 'app/models/spree/stock_item.rb'
|
||||||
|
- 'app/models/spree/taxonomy.rb'
|
||||||
- 'app/models/spree/variant.rb'
|
- 'app/models/spree/variant.rb'
|
||||||
- 'app/models/subscription_line_item.rb'
|
- 'app/models/subscription_line_item.rb'
|
||||||
|
|
||||||
@@ -710,7 +720,7 @@ Style/GlobalStdStream:
|
|||||||
- 'lib/tasks/subscriptions/debug.rake'
|
- 'lib/tasks/subscriptions/debug.rake'
|
||||||
- 'lib/tasks/subscriptions/test.rake'
|
- 'lib/tasks/subscriptions/test.rake'
|
||||||
|
|
||||||
# Offense count: 10
|
# Offense count: 12
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
# Configuration parameters: AllowSplatArgument.
|
# Configuration parameters: AllowSplatArgument.
|
||||||
Style/HashConversion:
|
Style/HashConversion:
|
||||||
@@ -718,7 +728,9 @@ Style/HashConversion:
|
|||||||
- 'app/controllers/admin/column_preferences_controller.rb'
|
- 'app/controllers/admin/column_preferences_controller.rb'
|
||||||
- 'app/controllers/admin/variant_overrides_controller.rb'
|
- 'app/controllers/admin/variant_overrides_controller.rb'
|
||||||
- 'app/controllers/spree/admin/products_controller.rb'
|
- 'app/controllers/spree/admin/products_controller.rb'
|
||||||
|
- 'app/models/order_cycle.rb'
|
||||||
- 'app/models/product_import/product_importer.rb'
|
- 'app/models/product_import/product_importer.rb'
|
||||||
|
- 'app/models/spree/shipping_method.rb'
|
||||||
- 'app/serializers/api/admin/exchange_serializer.rb'
|
- 'app/serializers/api/admin/exchange_serializer.rb'
|
||||||
- 'app/services/variants_stock_levels.rb'
|
- 'app/services/variants_stock_levels.rb'
|
||||||
- 'spec/controllers/admin/inventory_items_controller_spec.rb'
|
- 'spec/controllers/admin/inventory_items_controller_spec.rb'
|
||||||
|
|||||||
@@ -89,4 +89,4 @@ RUN ./script/install-bundler
|
|||||||
RUN yarn install
|
RUN yarn install
|
||||||
|
|
||||||
# Run bundler install in parallel with the amount of available CPUs
|
# Run bundler install in parallel with the amount of available CPUs
|
||||||
RUN bundle install --jobs="$(nproc)"
|
RUN bundle install --jobs="$(nproc)"
|
||||||
|
|||||||
2
Gemfile
2
Gemfile
@@ -86,7 +86,7 @@ gem "active_model_serializers", "0.8.4"
|
|||||||
gem 'activerecord-session_store'
|
gem 'activerecord-session_store'
|
||||||
gem 'acts-as-taggable-on'
|
gem 'acts-as-taggable-on'
|
||||||
gem 'angularjs-file-upload-rails', '~> 2.4.1'
|
gem 'angularjs-file-upload-rails', '~> 2.4.1'
|
||||||
gem 'bigdecimal'
|
gem 'bigdecimal', '3.0.2'
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
gem 'geocoder'
|
gem 'geocoder'
|
||||||
gem 'gmaps4rails'
|
gem 'gmaps4rails'
|
||||||
|
|||||||
22
Gemfile.lock
22
Gemfile.lock
@@ -180,7 +180,7 @@ GEM
|
|||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcp47_spec (0.2.1)
|
bcp47_spec (0.2.1)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bigdecimal (3.1.8)
|
bigdecimal (3.0.2)
|
||||||
bindata (2.5.0)
|
bindata (2.5.0)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.18.3)
|
bootsnap (1.18.3)
|
||||||
@@ -191,7 +191,7 @@ GEM
|
|||||||
bullet (7.1.6)
|
bullet (7.1.6)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
cable_ready (5.0.6)
|
cable_ready (5.0.5)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
actionview (>= 5.2)
|
actionview (>= 5.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
@@ -683,10 +683,10 @@ GEM
|
|||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.25.1)
|
rubocop-factory_bot (2.25.1)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-rails (2.28.0)
|
rubocop-rails (2.24.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.52.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rspec (2.29.2)
|
rubocop-rspec (2.29.2)
|
||||||
rubocop (~> 1.40)
|
rubocop (~> 1.40)
|
||||||
@@ -755,16 +755,16 @@ GEM
|
|||||||
state_machines-activerecord (0.9.0)
|
state_machines-activerecord (0.9.0)
|
||||||
activerecord (>= 6.0)
|
activerecord (>= 6.0)
|
||||||
state_machines-activemodel (>= 0.9.0)
|
state_machines-activemodel (>= 0.9.0)
|
||||||
stimulus_reflex (3.5.3)
|
stimulus_reflex (3.5.1)
|
||||||
actioncable (>= 5.2)
|
actioncable (>= 5.2, < 8)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2, < 8)
|
||||||
actionview (>= 5.2)
|
actionview (>= 5.2, < 8)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2, < 8)
|
||||||
cable_ready (~> 5.0)
|
cable_ready (~> 5.0)
|
||||||
nokogiri (~> 1.0)
|
nokogiri (~> 1.0)
|
||||||
nokogiri-html5-inference (~> 0.3)
|
nokogiri-html5-inference (~> 0.3)
|
||||||
rack (>= 2, < 4)
|
rack (>= 2, < 4)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2, < 8)
|
||||||
redis (>= 4.0, < 6.0)
|
redis (>= 4.0, < 6.0)
|
||||||
stringex (2.8.6)
|
stringex (2.8.6)
|
||||||
stringio (3.1.0)
|
stringio (3.1.0)
|
||||||
@@ -865,7 +865,7 @@ DEPENDENCIES
|
|||||||
angularjs-rails (= 1.8.0)
|
angularjs-rails (= 1.8.0)
|
||||||
arel-helpers (~> 2.12)
|
arel-helpers (~> 2.12)
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
bigdecimal
|
bigdecimal (= 3.0.2)
|
||||||
bootsnap
|
bootsnap
|
||||||
bugsnag
|
bugsnag
|
||||||
bullet
|
bullet
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
FROM ruby:3.1.4-alpine3.19 AS base
|
|
||||||
ENV LANG=C.UTF-8 \
|
|
||||||
LC_ALL=C.UTF-8 \
|
|
||||||
TZ=Europe/London \
|
|
||||||
RAILS_ROOT=/usr/src/app
|
|
||||||
RUN apk --no-cache upgrade && \
|
|
||||||
apk add --no-cache tzdata postgresql-client imagemagick imagemagick-jpeg && \
|
|
||||||
apk add --no-cache --virtual wkhtmltopdf
|
|
||||||
|
|
||||||
WORKDIR $RAILS_ROOT
|
|
||||||
|
|
||||||
# Development dependencies
|
|
||||||
FROM base AS development-base
|
|
||||||
RUN apk add --no-cache --virtual .build-deps \
|
|
||||||
build-base postgresql-dev git nodejs yarn && \
|
|
||||||
apk add --no-cache --virtual .dev-utils \
|
|
||||||
bash curl less vim chromium-chromedriver zlib-dev openssl-dev \
|
|
||||||
readline-dev yaml-dev sqlite-dev libxml2-dev libxslt-dev libffi-dev vips-dev && \
|
|
||||||
curl -o /usr/local/bin/wait-for-it https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh && \
|
|
||||||
chmod +x /usr/local/bin/wait-for-it
|
|
||||||
|
|
||||||
# Install yarn dependencies separately for caching
|
|
||||||
FROM development-base AS yarn-dependencies
|
|
||||||
COPY package.json yarn.lock ./
|
|
||||||
RUN yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
# Install Ruby gems
|
|
||||||
FROM development-base
|
|
||||||
COPY . $RAILS_ROOT
|
|
||||||
COPY Gemfile Gemfile.lock ./
|
|
||||||
RUN bundle install --jobs "$(nproc)"
|
|
||||||
COPY --from=yarn-dependencies $RAILS_ROOT/node_modules ./node_modules
|
|
||||||
@@ -67,5 +67,8 @@
|
|||||||
// foundation
|
// foundation
|
||||||
//= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js
|
//= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js
|
||||||
|
|
||||||
|
// LocalStorage
|
||||||
|
//= require ../shared/angular-local-storage.js
|
||||||
|
|
||||||
// requires the rest of the JS code in this folder
|
// requires the rest of the JS code in this folder
|
||||||
//= require_tree .
|
//= require_tree .
|
||||||
|
|||||||
@@ -307,6 +307,9 @@ filterSubmitProducts = (productsToFilter) ->
|
|||||||
variantHasUpdatableProperty = result.hasUpdatableProperty
|
variantHasUpdatableProperty = result.hasUpdatableProperty
|
||||||
filteredVariants.push filteredVariant if variantHasUpdatableProperty
|
filteredVariants.push filteredVariant if variantHasUpdatableProperty
|
||||||
|
|
||||||
|
if product.hasOwnProperty("sku")
|
||||||
|
filteredProduct.sku = product.sku
|
||||||
|
hasUpdatableProperty = true
|
||||||
if product.hasOwnProperty("name")
|
if product.hasOwnProperty("name")
|
||||||
filteredProduct.name = product.name
|
filteredProduct.name = product.name
|
||||||
hasUpdatableProperty = true
|
hasUpdatableProperty = true
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
angular.module("admin.indexUtils").factory 'KeyValueMapStore', (localStorageService)->
|
||||||
|
new class KeyValueMapStore
|
||||||
|
localStorageKey: ''
|
||||||
|
storableKeys: []
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
localStorageService.setStorageType("sessionStorage")
|
||||||
|
|
||||||
|
getStoredKeyValueMap: ->
|
||||||
|
localStorageService.get(@localStorageKey) || {}
|
||||||
|
|
||||||
|
setStoredValues: (source) ->
|
||||||
|
keyValueMap = {}
|
||||||
|
for key in @storableKeys
|
||||||
|
keyValueMap[key] = source[key]
|
||||||
|
localStorageService.set(@localStorageKey, keyValueMap)
|
||||||
|
|
||||||
|
restoreValues: (target) ->
|
||||||
|
storedKeyValueMap = @getStoredKeyValueMap()
|
||||||
|
|
||||||
|
return false if _.isEmpty(storedKeyValueMap)
|
||||||
|
|
||||||
|
for k,v of storedKeyValueMap
|
||||||
|
target[k] = v
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
clearKeyValueMap: () ->
|
||||||
|
localStorageService.remove(@localStorageKey)
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
#= require angular-google-maps.min.js
|
#= require angular-google-maps.min.js
|
||||||
#= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js
|
#= require ../shared/mm-foundation-tpls-0.9.0-20180826174721.min.js
|
||||||
#= require ../shared/ng-infinite-scroll.min.js
|
#= require ../shared/ng-infinite-scroll.min.js
|
||||||
|
#= require ../shared/angular-local-storage.js
|
||||||
#= require ../shared/angular-slideables.js
|
#= require ../shared/angular-slideables.js
|
||||||
#= require ../shared/shared
|
#= require ../shared/shared
|
||||||
#= require_tree ../shared/directives
|
#= require_tree ../shared/directives
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
angular.module("Darkswarm", [
|
angular.module("Darkswarm", [
|
||||||
'ngResource',
|
'ngResource',
|
||||||
'mm.foundation',
|
'mm.foundation',
|
||||||
|
'LocalStorageModule',
|
||||||
'infinite-scroll',
|
'infinite-scroll',
|
||||||
'angular-flash.service',
|
'angular-flash.service',
|
||||||
'templates',
|
'templates',
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
angular.module('Darkswarm').directive "ofnDisableScroll", ()->
|
||||||
|
# Stops scrolling from incrementing or decrementing input value
|
||||||
|
# Useful for number inputs
|
||||||
|
restrict: 'A'
|
||||||
|
link: (scope, element, attrs)->
|
||||||
|
element.bind 'focus', ->
|
||||||
|
element.bind 'mousewheel', (e)->
|
||||||
|
e.preventDefault()
|
||||||
|
element.bind 'blur', ->
|
||||||
|
element.unbind 'mousewheel'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
angular.module('Darkswarm').directive "integer", ->
|
||||||
|
restrict: 'A'
|
||||||
|
link: (scope, elem, attr) ->
|
||||||
|
elem.bind 'input', ->
|
||||||
|
elem.val Math.round(elem.val())
|
||||||
@@ -20,13 +20,10 @@ angular.module('Darkswarm').directive 'mapSearch', ($timeout, Search) ->
|
|||||||
$timeout =>
|
$timeout =>
|
||||||
map = ctrl.getMap()
|
map = ctrl.getMap()
|
||||||
|
|
||||||
if !map
|
searchBox = scope.createSearchBox map
|
||||||
alert(t('gmap_load_failure'))
|
scope.bindSearchResponse map, searchBox
|
||||||
else
|
scope.biasResults map, searchBox
|
||||||
searchBox = scope.createSearchBox map
|
scope.performUrlSearch map
|
||||||
scope.bindSearchResponse map, searchBox
|
|
||||||
scope.biasResults map, searchBox
|
|
||||||
scope.performUrlSearch map
|
|
||||||
|
|
||||||
scope.createSearchBox = (map) ->
|
scope.createSearchBox = (map) ->
|
||||||
map.controls[google.maps.ControlPosition.TOP_LEFT].push scope.input
|
map.controls[google.maps.ControlPosition.TOP_LEFT].push scope.input
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
angular.module('Darkswarm').directive "renderSvg", ()->
|
||||||
|
# Magical directive that'll render SVGs from URLs
|
||||||
|
# If only there were a neater way of doing this
|
||||||
|
restrict: 'E'
|
||||||
|
priority: 99
|
||||||
|
template: "<svg-wrapper></svg-wrapper>"
|
||||||
|
|
||||||
|
# Fetch SVG via ajax, inject into page using DOM
|
||||||
|
link: (scope, elem, attr)->
|
||||||
|
if /.svg/.test attr.path # Only do this if we've got an svg
|
||||||
|
$.ajax
|
||||||
|
url: attr.path
|
||||||
|
success: (html)->
|
||||||
|
elem.html($(html).find("svg"))
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
angular.module('Darkswarm').directive "ofnScrollTo", ($location, $anchorScroll)->
|
||||||
|
# Onclick sets $location.hash to attrs.ofnScrollTo
|
||||||
|
# Then triggers anchorScroll
|
||||||
|
restrict: 'A'
|
||||||
|
link: (scope, element, attrs)->
|
||||||
|
element.bind 'click', (ev)->
|
||||||
|
ev.stopPropagation()
|
||||||
|
$location.hash attrs.ofnScrollTo
|
||||||
|
$anchorScroll()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
angular.module('Darkswarm').factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $rootScope, $resource, Messages) ->
|
angular.module('Darkswarm').factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $rootScope, $resource, localStorageService, Messages) ->
|
||||||
# Handles syncing of current cart/order state to server
|
# Handles syncing of current cart/order state to server
|
||||||
new class Cart
|
new class Cart
|
||||||
dirty: false
|
dirty: false
|
||||||
@@ -113,6 +113,7 @@ angular.module('Darkswarm').factory 'Cart', (CurrentOrder, Variants, $timeout, $
|
|||||||
|
|
||||||
clear: ->
|
clear: ->
|
||||||
@line_items = []
|
@line_items = []
|
||||||
|
localStorageService.clearAll() # One day this will have to be moar GRANULAR
|
||||||
|
|
||||||
isOnlyItemInOrder: (id) =>
|
isOnlyItemInOrder: (id) =>
|
||||||
deletedItem = @line_items_finalised.find((item) -> item.id == id)
|
deletedItem = @line_items_finalised.find((item) -> item.id == id)
|
||||||
|
|||||||
546
app/assets/javascripts/shared/angular-local-storage.js
vendored
Normal file
546
app/assets/javascripts/shared/angular-local-storage.js
vendored
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
/**
|
||||||
|
* An Angular module that gives you access to the browsers local storage
|
||||||
|
* @version v0.5.0 - 2016-08-29
|
||||||
|
* @link https://github.com/grevory/angular-local-storage
|
||||||
|
* @author grevory <greg@gregpike.ca>
|
||||||
|
* @license MIT License, http://www.opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
(function (window, angular) {
|
||||||
|
var isDefined = angular.isDefined,
|
||||||
|
isUndefined = angular.isUndefined,
|
||||||
|
isNumber = angular.isNumber,
|
||||||
|
isObject = angular.isObject,
|
||||||
|
isArray = angular.isArray,
|
||||||
|
extend = angular.extend,
|
||||||
|
toJson = angular.toJson;
|
||||||
|
|
||||||
|
angular
|
||||||
|
.module('LocalStorageModule', [])
|
||||||
|
.provider('localStorageService', function() {
|
||||||
|
// You should set a prefix to avoid overwriting any local storage variables from the rest of your app
|
||||||
|
// e.g. localStorageServiceProvider.setPrefix('yourAppName');
|
||||||
|
// With provider you can use config as this:
|
||||||
|
// myApp.config(function (localStorageServiceProvider) {
|
||||||
|
// localStorageServiceProvider.prefix = 'yourAppName';
|
||||||
|
// });
|
||||||
|
this.prefix = 'ls';
|
||||||
|
|
||||||
|
// You could change web storage type localstorage or sessionStorage
|
||||||
|
this.storageType = 'localStorage';
|
||||||
|
|
||||||
|
// Cookie options (usually in case of fallback)
|
||||||
|
// expiry = Number of days before cookies expire // 0 = Does not expire
|
||||||
|
// path = The web path the cookie represents
|
||||||
|
// secure = Wether the cookies should be secure (i.e only sent on HTTPS requests)
|
||||||
|
this.cookie = {
|
||||||
|
expiry: 30,
|
||||||
|
path: '/',
|
||||||
|
secure: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decides wether we should default to cookies if localstorage is not supported.
|
||||||
|
this.defaultToCookie = true;
|
||||||
|
|
||||||
|
// Send signals for each of the following actions?
|
||||||
|
this.notify = {
|
||||||
|
setItem: true,
|
||||||
|
removeItem: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setter for the prefix
|
||||||
|
this.setPrefix = function(prefix) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setter for the storageType
|
||||||
|
this.setStorageType = function(storageType) {
|
||||||
|
this.storageType = storageType;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
// Setter for defaultToCookie value, default is true.
|
||||||
|
this.setDefaultToCookie = function (shouldDefault) {
|
||||||
|
this.defaultToCookie = !!shouldDefault; // Double-not to make sure it's a bool value.
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
// Setter for cookie config
|
||||||
|
this.setStorageCookie = function(exp, path, secure) {
|
||||||
|
this.cookie.expiry = exp;
|
||||||
|
this.cookie.path = path;
|
||||||
|
this.cookie.secure = secure;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setter for cookie domain
|
||||||
|
this.setStorageCookieDomain = function(domain) {
|
||||||
|
this.cookie.domain = domain;
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setter for notification config
|
||||||
|
// itemSet & itemRemove should be booleans
|
||||||
|
this.setNotify = function(itemSet, itemRemove) {
|
||||||
|
this.notify = {
|
||||||
|
setItem: itemSet,
|
||||||
|
removeItem: itemRemove
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$get = ['$rootScope', '$window', '$document', '$parse','$timeout', function($rootScope, $window, $document, $parse, $timeout) {
|
||||||
|
var self = this;
|
||||||
|
var prefix = self.prefix;
|
||||||
|
var cookie = self.cookie;
|
||||||
|
var notify = self.notify;
|
||||||
|
var storageType = self.storageType;
|
||||||
|
var webStorage;
|
||||||
|
|
||||||
|
// When Angular's $document is not available
|
||||||
|
if (!$document) {
|
||||||
|
$document = document;
|
||||||
|
} else if ($document[0]) {
|
||||||
|
$document = $document[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a prefix set in the config lets use that with an appended period for readability
|
||||||
|
if (prefix.substr(-1) !== '.') {
|
||||||
|
prefix = !!prefix ? prefix + '.' : '';
|
||||||
|
}
|
||||||
|
var deriveQualifiedKey = function(key) {
|
||||||
|
return prefix + key;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Removes prefix from the key.
|
||||||
|
var underiveQualifiedKey = function (key) {
|
||||||
|
return key.replace(new RegExp('^' + prefix, 'g'), '');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the key is within our prefix namespace.
|
||||||
|
var isKeyPrefixOurs = function (key) {
|
||||||
|
return key.indexOf(prefix) === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Checks the browser to see if local storage is supported
|
||||||
|
var checkSupport = function () {
|
||||||
|
try {
|
||||||
|
var supported = (storageType in $window && $window[storageType] !== null);
|
||||||
|
|
||||||
|
// When Safari (OS X or iOS) is in private browsing mode, it appears as though localStorage
|
||||||
|
// is available, but trying to call .setItem throws an exception.
|
||||||
|
//
|
||||||
|
// "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage
|
||||||
|
// that exceeded the quota."
|
||||||
|
var key = deriveQualifiedKey('__' + Math.round(Math.random() * 1e7));
|
||||||
|
if (supported) {
|
||||||
|
webStorage = $window[storageType];
|
||||||
|
webStorage.setItem(key, '');
|
||||||
|
webStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return supported;
|
||||||
|
} catch (e) {
|
||||||
|
// Only change storageType to cookies if defaulting is enabled.
|
||||||
|
if (self.defaultToCookie)
|
||||||
|
storageType = 'cookie';
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var browserSupportsLocalStorage = checkSupport();
|
||||||
|
|
||||||
|
// Directly adds a value to local storage
|
||||||
|
// If local storage is not available in the browser use cookies
|
||||||
|
// Example use: localStorageService.add('library','angular');
|
||||||
|
var addToLocalStorage = function (key, value, type) {
|
||||||
|
setStorageType(type);
|
||||||
|
|
||||||
|
// Let's convert undefined values to null to get the value consistent
|
||||||
|
if (isUndefined(value)) {
|
||||||
|
value = null;
|
||||||
|
} else {
|
||||||
|
value = toJson(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this browser does not support local storage use cookies
|
||||||
|
if (!browserSupportsLocalStorage && self.defaultToCookie || self.storageType === 'cookie') {
|
||||||
|
if (!browserSupportsLocalStorage) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notify.setItem) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.setitem', {key: key, newvalue: value, storageType: 'cookie'});
|
||||||
|
}
|
||||||
|
return addToCookies(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (webStorage) {
|
||||||
|
webStorage.setItem(deriveQualifiedKey(key), value);
|
||||||
|
}
|
||||||
|
if (notify.setItem) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.setitem', {key: key, newvalue: value, storageType: self.storageType});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||||
|
return addToCookies(key, value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Directly get a value from local storage
|
||||||
|
// Example use: localStorageService.get('library'); // returns 'angular'
|
||||||
|
var getFromLocalStorage = function (key, type) {
|
||||||
|
setStorageType(type);
|
||||||
|
|
||||||
|
if (!browserSupportsLocalStorage && self.defaultToCookie || self.storageType === 'cookie') {
|
||||||
|
if (!browserSupportsLocalStorage) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFromCookies(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = webStorage ? webStorage.getItem(deriveQualifiedKey(key)) : null;
|
||||||
|
// angular.toJson will convert null to 'null', so a proper conversion is needed
|
||||||
|
// FIXME not a perfect solution, since a valid 'null' string can't be stored
|
||||||
|
if (!item || item === 'null') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(item);
|
||||||
|
} catch (e) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove an item from local storage
|
||||||
|
// Example use: localStorageService.remove('library'); // removes the key/value pair of library='angular'
|
||||||
|
//
|
||||||
|
// This is var-arg removal, check the last argument to see if it is a storageType
|
||||||
|
// and set type accordingly before removing.
|
||||||
|
//
|
||||||
|
var removeFromLocalStorage = function () {
|
||||||
|
// can't pop on arguments, so we do this
|
||||||
|
var consumed = 0;
|
||||||
|
if (arguments.length >= 1 &&
|
||||||
|
(arguments[arguments.length - 1] === 'localStorage' ||
|
||||||
|
arguments[arguments.length - 1] === 'sessionStorage')) {
|
||||||
|
consumed = 1;
|
||||||
|
setStorageType(arguments[arguments.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var i, key;
|
||||||
|
for (i = 0; i < arguments.length - consumed; i++) {
|
||||||
|
key = arguments[i];
|
||||||
|
if (!browserSupportsLocalStorage && self.defaultToCookie || self.storageType === 'cookie') {
|
||||||
|
if (!browserSupportsLocalStorage) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notify.removeItem) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.removeitem', {key: key, storageType: 'cookie'});
|
||||||
|
}
|
||||||
|
removeFromCookies(key);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
webStorage.removeItem(deriveQualifiedKey(key));
|
||||||
|
if (notify.removeItem) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.removeitem', {
|
||||||
|
key: key,
|
||||||
|
storageType: self.storageType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||||
|
removeFromCookies(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return array of keys for local storage
|
||||||
|
// Example use: var keys = localStorageService.keys()
|
||||||
|
var getKeysForLocalStorage = function (type) {
|
||||||
|
setStorageType(type);
|
||||||
|
|
||||||
|
if (!browserSupportsLocalStorage) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefixLength = prefix.length;
|
||||||
|
var keys = [];
|
||||||
|
for (var key in webStorage) {
|
||||||
|
// Only return keys that are for this app
|
||||||
|
if (key.substr(0, prefixLength) === prefix) {
|
||||||
|
try {
|
||||||
|
keys.push(key.substr(prefixLength));
|
||||||
|
} catch (e) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.error', e.Description);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove all data for this app from local storage
|
||||||
|
// Also optionally takes a regular expression string and removes the matching key-value pairs
|
||||||
|
// Example use: localStorageService.clearAll();
|
||||||
|
// Should be used mostly for development purposes
|
||||||
|
var clearAllFromLocalStorage = function (regularExpression, type) {
|
||||||
|
setStorageType(type);
|
||||||
|
|
||||||
|
// Setting both regular expressions independently
|
||||||
|
// Empty strings result in catchall RegExp
|
||||||
|
var prefixRegex = !!prefix ? new RegExp('^' + prefix) : new RegExp();
|
||||||
|
var testRegex = !!regularExpression ? new RegExp(regularExpression) : new RegExp();
|
||||||
|
|
||||||
|
if (!browserSupportsLocalStorage && self.defaultToCookie || self.storageType === 'cookie') {
|
||||||
|
if (!browserSupportsLocalStorage) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
|
||||||
|
}
|
||||||
|
return clearAllFromCookies();
|
||||||
|
}
|
||||||
|
if (!browserSupportsLocalStorage && !self.defaultToCookie)
|
||||||
|
return false;
|
||||||
|
var prefixLength = prefix.length;
|
||||||
|
|
||||||
|
for (var key in webStorage) {
|
||||||
|
// Only remove items that are for this app and match the regular expression
|
||||||
|
if (prefixRegex.test(key) && testRegex.test(key.substr(prefixLength))) {
|
||||||
|
try {
|
||||||
|
removeFromLocalStorage(key.substr(prefixLength));
|
||||||
|
} catch (e) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||||
|
return clearAllFromCookies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Checks the browser to see if cookies are supported
|
||||||
|
var browserSupportsCookies = (function() {
|
||||||
|
try {
|
||||||
|
return $window.navigator.cookieEnabled ||
|
||||||
|
("cookie" in $document && ($document.cookie.length > 0 ||
|
||||||
|
($document.cookie = "test").indexOf.call($document.cookie, "test") > -1));
|
||||||
|
} catch (e) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
|
||||||
|
// Directly adds a value to cookies
|
||||||
|
// Typically used as a fallback if local storage is not available in the browser
|
||||||
|
// Example use: localStorageService.cookie.add('library','angular');
|
||||||
|
var addToCookies = function (key, value, daysToExpiry, secure) {
|
||||||
|
|
||||||
|
if (isUndefined(value)) {
|
||||||
|
return false;
|
||||||
|
} else if(isArray(value) || isObject(value)) {
|
||||||
|
value = toJson(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!browserSupportsCookies) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.error', 'COOKIES_NOT_SUPPORTED');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var expiry = '',
|
||||||
|
expiryDate = new Date(),
|
||||||
|
cookieDomain = '';
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
// Mark that the cookie has expired one day ago
|
||||||
|
expiryDate.setTime(expiryDate.getTime() + (-1 * 24 * 60 * 60 * 1000));
|
||||||
|
expiry = "; expires=" + expiryDate.toGMTString();
|
||||||
|
value = '';
|
||||||
|
} else if (isNumber(daysToExpiry) && daysToExpiry !== 0) {
|
||||||
|
expiryDate.setTime(expiryDate.getTime() + (daysToExpiry * 24 * 60 * 60 * 1000));
|
||||||
|
expiry = "; expires=" + expiryDate.toGMTString();
|
||||||
|
} else if (cookie.expiry !== 0) {
|
||||||
|
expiryDate.setTime(expiryDate.getTime() + (cookie.expiry * 24 * 60 * 60 * 1000));
|
||||||
|
expiry = "; expires=" + expiryDate.toGMTString();
|
||||||
|
}
|
||||||
|
if (!!key) {
|
||||||
|
var cookiePath = "; path=" + cookie.path;
|
||||||
|
if (cookie.domain) {
|
||||||
|
cookieDomain = "; domain=" + cookie.domain;
|
||||||
|
}
|
||||||
|
/* Providing the secure parameter always takes precedence over config
|
||||||
|
* (allows developer to mix and match secure + non-secure) */
|
||||||
|
if (typeof secure === 'boolean') {
|
||||||
|
if (secure === true) {
|
||||||
|
/* We've explicitly specified secure,
|
||||||
|
* add the secure attribute to the cookie (after domain) */
|
||||||
|
cookieDomain += "; secure";
|
||||||
|
}
|
||||||
|
// else - secure has been supplied but isn't true - so don't set secure flag, regardless of what config says
|
||||||
|
}
|
||||||
|
else if (cookie.secure === true) {
|
||||||
|
// secure parameter wasn't specified, get default from config
|
||||||
|
cookieDomain += "; secure";
|
||||||
|
}
|
||||||
|
$document.cookie = deriveQualifiedKey(key) + "=" + encodeURIComponent(value) + expiry + cookiePath + cookieDomain;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Directly get a value from a cookie
|
||||||
|
// Example use: localStorageService.cookie.get('library'); // returns 'angular'
|
||||||
|
var getFromCookies = function (key) {
|
||||||
|
if (!browserSupportsCookies) {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.error', 'COOKIES_NOT_SUPPORTED');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cookies = $document.cookie && $document.cookie.split(';') || [];
|
||||||
|
for(var i=0; i < cookies.length; i++) {
|
||||||
|
var thisCookie = cookies[i];
|
||||||
|
while (thisCookie.charAt(0) === ' ') {
|
||||||
|
thisCookie = thisCookie.substring(1,thisCookie.length);
|
||||||
|
}
|
||||||
|
if (thisCookie.indexOf(deriveQualifiedKey(key) + '=') === 0) {
|
||||||
|
var storedValues = decodeURIComponent(thisCookie.substring(prefix.length + key.length + 1, thisCookie.length));
|
||||||
|
try {
|
||||||
|
return JSON.parse(storedValues);
|
||||||
|
} catch(e) {
|
||||||
|
return storedValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
var removeFromCookies = function (key) {
|
||||||
|
addToCookies(key,null);
|
||||||
|
};
|
||||||
|
|
||||||
|
var clearAllFromCookies = function () {
|
||||||
|
var thisCookie = null;
|
||||||
|
var prefixLength = prefix.length;
|
||||||
|
var cookies = $document.cookie.split(';');
|
||||||
|
for(var i = 0; i < cookies.length; i++) {
|
||||||
|
thisCookie = cookies[i];
|
||||||
|
|
||||||
|
while (thisCookie.charAt(0) === ' ') {
|
||||||
|
thisCookie = thisCookie.substring(1, thisCookie.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = thisCookie.substring(prefixLength, thisCookie.indexOf('='));
|
||||||
|
removeFromCookies(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var getStorageType = function() {
|
||||||
|
return storageType;
|
||||||
|
};
|
||||||
|
|
||||||
|
var setStorageType = function(type) {
|
||||||
|
if (type && storageType !== type) {
|
||||||
|
storageType = type;
|
||||||
|
browserSupportsLocalStorage = checkSupport();
|
||||||
|
}
|
||||||
|
return browserSupportsLocalStorage;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a listener on scope variable to save its changes to local storage
|
||||||
|
// Return a function which when called cancels binding
|
||||||
|
var bindToScope = function(scope, key, def, lsKey, type) {
|
||||||
|
lsKey = lsKey || key;
|
||||||
|
var value = getFromLocalStorage(lsKey, type);
|
||||||
|
|
||||||
|
if (value === null && isDefined(def)) {
|
||||||
|
value = def;
|
||||||
|
} else if (isObject(value) && isObject(def)) {
|
||||||
|
value = extend(value, def);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parse(key).assign(scope, value);
|
||||||
|
|
||||||
|
return scope.$watch(key, function(newVal) {
|
||||||
|
addToLocalStorage(lsKey, newVal, type);
|
||||||
|
}, isObject(scope[key]));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add listener to local storage, for update callbacks.
|
||||||
|
if (browserSupportsLocalStorage) {
|
||||||
|
if ($window.addEventListener) {
|
||||||
|
$window.addEventListener("storage", handleStorageChangeCallback, false);
|
||||||
|
$rootScope.$on('$destroy', function() {
|
||||||
|
$window.removeEventListener("storage", handleStorageChangeCallback);
|
||||||
|
});
|
||||||
|
} else if($window.attachEvent){
|
||||||
|
// attachEvent and detachEvent are proprietary to IE v6-10
|
||||||
|
$window.attachEvent("onstorage", handleStorageChangeCallback);
|
||||||
|
$rootScope.$on('$destroy', function() {
|
||||||
|
$window.detachEvent("onstorage", handleStorageChangeCallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback handler for storage changed.
|
||||||
|
function handleStorageChangeCallback(e) {
|
||||||
|
if (!e) { e = $window.event; }
|
||||||
|
if (notify.setItem) {
|
||||||
|
if (isKeyPrefixOurs(e.key)) {
|
||||||
|
var key = underiveQualifiedKey(e.key);
|
||||||
|
// Use timeout, to avoid using $rootScope.$apply.
|
||||||
|
$timeout(function () {
|
||||||
|
$rootScope.$broadcast('LocalStorageModule.notification.changed', { key: key, newvalue: e.newValue, storageType: self.storageType });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return localStorageService.length
|
||||||
|
// ignore keys that not owned
|
||||||
|
var lengthOfLocalStorage = function(type) {
|
||||||
|
setStorageType(type);
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
var storage = $window[storageType];
|
||||||
|
for(var i = 0; i < storage.length; i++) {
|
||||||
|
if(storage.key(i).indexOf(prefix) === 0 ) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSupported: browserSupportsLocalStorage,
|
||||||
|
getStorageType: getStorageType,
|
||||||
|
setStorageType: setStorageType,
|
||||||
|
set: addToLocalStorage,
|
||||||
|
add: addToLocalStorage, //DEPRECATED
|
||||||
|
get: getFromLocalStorage,
|
||||||
|
keys: getKeysForLocalStorage,
|
||||||
|
remove: removeFromLocalStorage,
|
||||||
|
clearAll: clearAllFromLocalStorage,
|
||||||
|
bind: bindToScope,
|
||||||
|
deriveKey: deriveQualifiedKey,
|
||||||
|
underiveKey: underiveQualifiedKey,
|
||||||
|
length: lengthOfLocalStorage,
|
||||||
|
defaultToCookie: this.defaultToCookie,
|
||||||
|
cookie: {
|
||||||
|
isSupported: browserSupportsCookies,
|
||||||
|
set: addToCookies,
|
||||||
|
add: addToCookies, //DEPRECATED
|
||||||
|
get: getFromCookies,
|
||||||
|
remove: removeFromCookies,
|
||||||
|
clearAll: clearAllFromCookies
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
})(window, window.angular);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
window.OFNShared = angular.module("OFNShared", [
|
window.OFNShared = angular.module("OFNShared", [
|
||||||
"mm.foundation",
|
"mm.foundation",
|
||||||
|
"LocalStorageModule"
|
||||||
]).config ($httpProvider) ->
|
]).config ($httpProvider) ->
|
||||||
$httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"
|
$httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ module Admin
|
|||||||
|
|
||||||
create_connected_app
|
create_connected_app
|
||||||
|
|
||||||
jwt_service = Vine::JwtService.new(secret: connected_app_params[:vine_secret])
|
jwt_service = VineJwtService.new(secret: connected_app_params[:vine_secret])
|
||||||
vine_api = Vine::ApiService.new(api_key: connected_app_params[:vine_api_key],
|
vine_api = VineApiService.new(api_key: connected_app_params[:vine_api_key],
|
||||||
jwt_generator: jwt_service)
|
jwt_generator: jwt_service)
|
||||||
|
|
||||||
if !@app.connect(api_key: connected_app_params[:vine_api_key],
|
if !@app.connect(api_key: connected_app_params[:vine_api_key],
|
||||||
secret: connected_app_params[:vine_secret], vine_api:)
|
secret: connected_app_params[:vine_secret], vine_api:)
|
||||||
@@ -77,7 +77,7 @@ module Admin
|
|||||||
|
|
||||||
def log_and_notify_exception(exception)
|
def log_and_notify_exception(exception)
|
||||||
Rails.logger.error exception.inspect
|
Rails.logger.error exception.inspect
|
||||||
Alert.raise(exception)
|
Bugsnag.notify(exception)
|
||||||
end
|
end
|
||||||
|
|
||||||
def vine_params_empty?
|
def vine_params_empty?
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ module Admin
|
|||||||
.find(params.require(:enterprise_id))
|
.find(params.require(:enterprise_id))
|
||||||
|
|
||||||
catalog_url = params.require(:catalog_url)
|
catalog_url = params.require(:catalog_url)
|
||||||
catalog = DfcCatalog.load(spree_current_user, catalog_url)
|
|
||||||
catalog.apply_wholesale_values!
|
json_catalog = fetch_catalog(catalog_url)
|
||||||
|
graph = DfcIo.import(json_catalog)
|
||||||
|
|
||||||
# * First step: import all products for given enterprise.
|
# * First step: import all products for given enterprise.
|
||||||
# * Second step: render table and let user decide which ones to import.
|
# * Second step: render table and let user decide which ones to import.
|
||||||
imported = catalog.products.map do |subject|
|
imported = graph.map do |subject|
|
||||||
|
next unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
|
||||||
|
|
||||||
existing_variant = enterprise.supplied_variants.linked_to(subject.semanticId)
|
existing_variant = enterprise.supplied_variants.linked_to(subject.semanticId)
|
||||||
|
|
||||||
if existing_variant
|
if existing_variant
|
||||||
@@ -41,5 +44,11 @@ module Admin
|
|||||||
flash[:error] = e.message
|
flash[:error] = e.message
|
||||||
redirect_to admin_product_import_path
|
redirect_to admin_product_import_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_catalog(url)
|
||||||
|
DfcRequest.new(spree_current_user).call(url)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ module Admin
|
|||||||
def validate_data
|
def validate_data
|
||||||
return unless process_data('validate')
|
return unless process_data('validate')
|
||||||
|
|
||||||
render json: @importer.import_results
|
render json: @importer.import_results, response: 200
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_data
|
def save_data
|
||||||
return unless process_data('save')
|
return unless process_data('save')
|
||||||
|
|
||||||
render json: @importer.save_results
|
render json: @importer.save_results, response: 200
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_absent_products
|
def reset_absent_products
|
||||||
@@ -76,7 +76,7 @@ module Admin
|
|||||||
begin
|
begin
|
||||||
@importer.public_send("#{method}_entries")
|
@importer.public_send("#{method}_entries")
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
render plain: e.message, status: :internal_server_error
|
render json: e.message, response: 500
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ module Admin
|
|||||||
|
|
||||||
def product_scope
|
def product_scope
|
||||||
user = spree_current_user
|
user = spree_current_user
|
||||||
scope = if user.admin? || user.enterprises.present?
|
scope = if user.has_spree_role?("admin") || user.enterprises.present?
|
||||||
Spree::Product
|
Spree::Product
|
||||||
else
|
else
|
||||||
Spree::Product.active
|
Spree::Product.active
|
||||||
|
|||||||
@@ -71,12 +71,6 @@ module Admin
|
|||||||
|
|
||||||
def load_collection
|
def load_collection
|
||||||
collection_hash = Hash[variant_overrides_params.each_with_index.map { |vo, i| [i, vo] }]
|
collection_hash = Hash[variant_overrides_params.each_with_index.map { |vo, i| [i, vo] }]
|
||||||
|
|
||||||
# Reset count_on_hand when switching to producer settings:
|
|
||||||
collection_hash.each_value do |vo|
|
|
||||||
vo["count_on_hand"] = nil if vo.fetch("on_demand", :unchanged).nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
@vo_set = Sets::VariantOverrideSet.new(@variant_overrides,
|
@vo_set = Sets::VariantOverrideSet.new(@variant_overrides,
|
||||||
collection_attributes: collection_hash)
|
collection_attributes: collection_hash)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def error_during_processing(exception)
|
def error_during_processing(exception)
|
||||||
Alert.raise(exception)
|
Bugsnag.notify(exception)
|
||||||
|
|
||||||
render(json: { exception: exception.message },
|
render(json: { exception: exception.message },
|
||||||
status: :unprocessable_entity) && return
|
status: :unprocessable_entity) && return
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ module Api
|
|||||||
Orders::WorkflowService.new(@order).advance_to_payment if @order.line_items.any?
|
Orders::WorkflowService.new(@order).advance_to_payment if @order.line_items.any?
|
||||||
|
|
||||||
@order.recreate_all_fees!
|
@order.recreate_all_fees!
|
||||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
|
||||||
|
|
||||||
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
||||||
end
|
end
|
||||||
@@ -74,7 +73,6 @@ module Api
|
|||||||
|
|
||||||
@order.contents.add(variant, quantity, @shipment)
|
@order.contents.add(variant, quantity, @shipment)
|
||||||
@order.recreate_all_fees!
|
@order.recreate_all_fees!
|
||||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
|
||||||
|
|
||||||
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
||||||
end
|
end
|
||||||
@@ -88,7 +86,6 @@ module Api
|
|||||||
@shipment.reload if @shipment.persisted?
|
@shipment.reload if @shipment.persisted?
|
||||||
|
|
||||||
@order.recreate_all_fees!
|
@order.recreate_all_fees!
|
||||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
|
||||||
|
|
||||||
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -55,14 +55,14 @@ module Api
|
|||||||
|
|
||||||
def scope
|
def scope
|
||||||
if @product
|
if @product
|
||||||
variants = if current_api_user.admin? || params[:show_deleted]
|
variants = if current_api_user.has_spree_role?("admin") || params[:show_deleted]
|
||||||
@product.variants.with_deleted
|
@product.variants.with_deleted
|
||||||
else
|
else
|
||||||
@product.variants
|
@product.variants
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
variants = Spree::Variant.where(nil)
|
variants = Spree::Variant.where(nil)
|
||||||
if current_api_user.admin?
|
if current_api_user.has_spree_role?("admin")
|
||||||
unless params[:show_deleted]
|
unless params[:show_deleted]
|
||||||
variants = Spree::Variant.active
|
variants = Spree::Variant.active
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def error_during_processing(exception)
|
def error_during_processing(exception)
|
||||||
Alert.raise(exception)
|
Bugsnag.notify(exception)
|
||||||
|
|
||||||
if Rails.env.development? || Rails.env.test?
|
if Rails.env.development? || Rails.env.test?
|
||||||
render status: :unprocessable_entity,
|
render status: :unprocessable_entity,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class CartController < BaseController
|
|||||||
order.cap_quantity_at_stock!
|
order.cap_quantity_at_stock!
|
||||||
order.recreate_all_fees!
|
order.recreate_all_fees!
|
||||||
|
|
||||||
StockSyncJob.sync_linked_catalogs_later(order)
|
StockSyncJob.sync_linked_catalogs(order)
|
||||||
|
|
||||||
render json: { error: false, stock_levels: stock_levels(order) }, status: :ok
|
render json: { error: false, stock_levels: stock_levels(order) }, status: :ok
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -78,21 +78,8 @@ class CheckoutController < BaseController
|
|||||||
|
|
||||||
return true if redirect_to_payment_gateway
|
return true if redirect_to_payment_gateway
|
||||||
|
|
||||||
# Redeem VINE voucher
|
|
||||||
vine_voucher_redeemer = Vine::VoucherRedeemerService.new(order: @order)
|
|
||||||
unless vine_voucher_redeemer.redeem
|
|
||||||
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
|
|
||||||
flash[:error] = if vine_voucher_redeemer.errors.keys.include?(:redeeming_failed)
|
|
||||||
vine_voucher_redeemer.errors[:redeeming_failed]
|
|
||||||
else
|
|
||||||
I18n.t('checkout.errors.voucher_redeeming_error')
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
|
|
||||||
end
|
|
||||||
@order.process_payments!
|
@order.process_payments!
|
||||||
@order.confirm!
|
@order.confirm!
|
||||||
BackorderJob.check_stock(@order)
|
|
||||||
order_completion_reset @order
|
order_completion_reset @order
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ module OrderCompletion
|
|||||||
end
|
end
|
||||||
|
|
||||||
def order_invalid!
|
def order_invalid!
|
||||||
Alert.raise_with_record("Notice: invalid order loaded during checkout", @order)
|
Bugsnag.notify("Notice: invalid order loaded during checkout") do |payload|
|
||||||
|
payload.add_metadata :order, :order, @order
|
||||||
|
end
|
||||||
|
|
||||||
flash[:error] = t('checkout.order_not_loaded')
|
flash[:error] = t('checkout.order_not_loaded')
|
||||||
redirect_to main_app.shop_path
|
redirect_to main_app.shop_path
|
||||||
@@ -90,7 +92,9 @@ module OrderCompletion
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notify_failure(error = RuntimeError.new(order_processing_error))
|
def notify_failure(error = RuntimeError.new(order_processing_error))
|
||||||
Alert.raise_with_record(error, @order)
|
Bugsnag.notify(error) do |payload|
|
||||||
|
payload.add_metadata :order, @order
|
||||||
|
end
|
||||||
flash[:error] = order_processing_error if flash.blank?
|
flash[:error] = order_processing_error if flash.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ module OrderStockCheck
|
|||||||
def check_order_cycle_expiry
|
def check_order_cycle_expiry
|
||||||
return unless current_order_cycle&.closed?
|
return unless current_order_cycle&.closed?
|
||||||
|
|
||||||
Alert.raise_with_record("Notice: order cycle closed during checkout completion", current_order)
|
Bugsnag.notify("Notice: order cycle closed during checkout completion") do |payload|
|
||||||
|
payload.add_metadata :order, :order, current_order
|
||||||
|
end
|
||||||
current_order.empty!
|
current_order.empty!
|
||||||
current_order.set_order_cycle! nil
|
current_order.set_order_cycle! nil
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ class ErrorsController < ApplicationController
|
|||||||
layout "errors"
|
layout "errors"
|
||||||
|
|
||||||
def not_found
|
def not_found
|
||||||
|
Bugsnag.notify("404") do |event|
|
||||||
|
event.severity = "info"
|
||||||
|
|
||||||
|
event.add_metadata(:request, :env, request.env)
|
||||||
|
end
|
||||||
render status: :not_found, formats: :html
|
render status: :not_found, formats: :html
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ module Spree
|
|||||||
@order.restock_items = params.fetch(:restock_items, "true") == "true"
|
@order.restock_items = params.fetch(:restock_items, "true") == "true"
|
||||||
|
|
||||||
if @order.public_send(event.to_s)
|
if @order.public_send(event.to_s)
|
||||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
|
||||||
flash[:success] = Spree.t(:order_updated)
|
flash[:success] = Spree.t(:order_updated)
|
||||||
else
|
else
|
||||||
flash[:error] = Spree.t(:cannot_perform_operation)
|
flash[:error] = Spree.t(:cannot_perform_operation)
|
||||||
|
|||||||
@@ -24,12 +24,9 @@ module Spree
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
# Try to redeem VINE voucher first as we don't want to create a payment and complete
|
|
||||||
# the order if it fails
|
|
||||||
return redirect_to spree.admin_order_payments_path(@order) unless redeem_vine_voucher
|
|
||||||
|
|
||||||
@payment = @order.payments.build(object_params)
|
@payment = @order.payments.build(object_params)
|
||||||
load_payment_source
|
load_payment_source
|
||||||
|
|
||||||
begin
|
begin
|
||||||
unless @payment.save
|
unless @payment.save
|
||||||
redirect_to spree.admin_order_payments_path(@order)
|
redirect_to spree.admin_order_payments_path(@order)
|
||||||
@@ -54,10 +51,6 @@ module Spree
|
|||||||
event = params[:e]
|
event = params[:e]
|
||||||
return unless event && @payment.payment_source
|
return unless event && @payment.payment_source
|
||||||
|
|
||||||
# capture_and_complete_order will complete the order, so we want to try to redeem VINE
|
|
||||||
# voucher first and exit if it fails
|
|
||||||
return if event == "capture_and_complete_order" && !redeem_vine_voucher
|
|
||||||
|
|
||||||
# Because we have a transition method also called void, we do this to avoid conflicts.
|
# Because we have a transition method also called void, we do this to avoid conflicts.
|
||||||
event = "void_transaction" if event == "void"
|
event = "void_transaction" if event == "void"
|
||||||
if allowed_events.include?(event) && @payment.public_send("#{event}!")
|
if allowed_events.include?(event) && @payment.public_send("#{event}!")
|
||||||
@@ -67,7 +60,7 @@ module Spree
|
|||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
logger.error e.message
|
logger.error e.message
|
||||||
Alert.raise(e)
|
Bugsnag.notify(e)
|
||||||
flash[:error] = e.message
|
flash[:error] = e.message
|
||||||
ensure
|
ensure
|
||||||
redirect_to request.referer
|
redirect_to request.referer
|
||||||
@@ -189,22 +182,6 @@ module Spree
|
|||||||
%w{capture void_transaction credit refund resend_authorization_email
|
%w{capture void_transaction credit refund resend_authorization_email
|
||||||
capture_and_complete_order}
|
capture_and_complete_order}
|
||||||
end
|
end
|
||||||
|
|
||||||
def redeem_vine_voucher
|
|
||||||
vine_voucher_redeemer = Vine::VoucherRedeemerService.new(order: @order)
|
|
||||||
if vine_voucher_redeemer.redeem == false
|
|
||||||
# rubocop:disable Rails/DeprecatedActiveModelErrorsMethods
|
|
||||||
flash[:error] = if vine_voucher_redeemer.errors.keys.include?(:redeeming_failed)
|
|
||||||
vine_voucher_redeemer.errors[:redeeming_failed]
|
|
||||||
else
|
|
||||||
I18n.t('checkout.errors.voucher_redeeming_error')
|
|
||||||
end
|
|
||||||
# rubocop:enable Rails/DeprecatedActiveModelErrorsMethods
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ module Spree
|
|||||||
include OrderCyclesHelper
|
include OrderCyclesHelper
|
||||||
include EnterprisesHelper
|
include EnterprisesHelper
|
||||||
helper ::Admin::ProductsHelper
|
helper ::Admin::ProductsHelper
|
||||||
helper Spree::Admin::TaxCategoriesHelper
|
|
||||||
|
|
||||||
before_action :load_data
|
before_action :load_data
|
||||||
before_action :load_producers, only: [:index, :new]
|
before_action :load_producers, only: [:index, :new]
|
||||||
@@ -214,7 +213,7 @@ module Spree
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notify_bugsnag(error, product, variant)
|
def notify_bugsnag(error, product, variant)
|
||||||
Alert.raise(error) do |report|
|
Bugsnag.notify(error) do |report|
|
||||||
report.add_metadata(:product,
|
report.add_metadata(:product,
|
||||||
{ product: product.attributes, variant: variant.attributes })
|
{ product: product.attributes, variant: variant.attributes })
|
||||||
report.add_metadata(:product, :product_error, product.errors.first) unless product.valid?
|
report.add_metadata(:product, :product_error, product.errors.first) unless product.valid?
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ module Spree
|
|||||||
|
|
||||||
# http://spreecommerce.com/blog/2010/11/02/json-hijacking-vulnerability/
|
# http://spreecommerce.com/blog/2010/11/02/json-hijacking-vulnerability/
|
||||||
before_action :check_json_authenticity, only: :index
|
before_action :check_json_authenticity, only: :index
|
||||||
|
before_action :load_roles, only: [:edit, :new, :update, :create,
|
||||||
|
:generate_api_key, :clear_api_key]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
respond_with(@collection) do |format|
|
respond_with(@collection) do |format|
|
||||||
@@ -121,6 +123,10 @@ module Spree
|
|||||||
sign_in(@user, event: :authentication, bypass: true)
|
sign_in(@user, event: :authentication, bypass: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_roles
|
||||||
|
@roles = Spree::Role.where(nil)
|
||||||
|
end
|
||||||
|
|
||||||
def new_email_unconfirmed?
|
def new_email_unconfirmed?
|
||||||
params[:user][:email] != @user.email
|
params[:user][:email] != @user.email
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -70,15 +70,14 @@ module Spree
|
|||||||
@order.recreate_all_fees! # Enterprise fees on line items and on the order itself
|
@order.recreate_all_fees! # Enterprise fees on line items and on the order itself
|
||||||
|
|
||||||
# Re apply the voucher
|
# Re apply the voucher
|
||||||
OrderManagement::Order::Updater.new(@order).update_voucher
|
VoucherAdjustmentsService.new(@order).update
|
||||||
|
@order.update_totals_and_states
|
||||||
|
|
||||||
if @order.complete?
|
if @order.complete?
|
||||||
@order.update_payment_fees!
|
@order.update_payment_fees!
|
||||||
@order.create_tax_charge!
|
@order.create_tax_charge!
|
||||||
end
|
end
|
||||||
|
|
||||||
AmendBackorderJob.perform_later(@order) if @order.completed?
|
|
||||||
|
|
||||||
respond_with(@order) do |format|
|
respond_with(@order) do |format|
|
||||||
format.html do
|
format.html do
|
||||||
if params.key?(:checkout)
|
if params.key?(:checkout)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'open_food_network/error_logger'
|
||||||
require "spree/core/controller_helpers/auth"
|
require "spree/core/controller_helpers/auth"
|
||||||
require "spree/core/controller_helpers/common"
|
require "spree/core/controller_helpers/common"
|
||||||
require "spree/core/controller_helpers/order"
|
require "spree/core/controller_helpers/order"
|
||||||
@@ -36,7 +37,7 @@ class UserRegistrationsController < Devise::RegistrationsController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Alert.raise(e)
|
OpenFoodNetwork::ErrorLogger.notify(e)
|
||||||
render_error(message: I18n.t('unknown_error', scope: I18N_SCOPE))
|
render_error(message: I18n.t('unknown_error', scope: I18N_SCOPE))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,7 @@ class VoucherAdjustmentsController < BaseController
|
|||||||
before_action :set_order
|
before_action :set_order
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if voucher_params[:voucher_code].blank?
|
if add_voucher
|
||||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
|
||||||
return render_error
|
|
||||||
end
|
|
||||||
|
|
||||||
voucher = load_voucher
|
|
||||||
|
|
||||||
return render_error unless valid_voucher?(voucher)
|
|
||||||
|
|
||||||
if add_voucher_to_order(voucher)
|
|
||||||
update_payment_section
|
update_payment_section
|
||||||
elsif @order.errors.present?
|
elsif @order.errors.present?
|
||||||
render_error
|
render_error
|
||||||
@@ -39,28 +30,19 @@ class VoucherAdjustmentsController < BaseController
|
|||||||
@order = current_order
|
@order = current_order
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_voucher?(voucher)
|
def add_voucher
|
||||||
return false if @order.errors.present?
|
if voucher_params[:voucher_code].blank?
|
||||||
|
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
voucher = Voucher.find_by(code: voucher_params[:voucher_code], enterprise: @order.distributor)
|
||||||
|
|
||||||
if voucher.nil?
|
if voucher.nil?
|
||||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
@order.errors.add(:voucher_code, I18n.t('checkout.errors.voucher_not_found'))
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if !voucher.valid?
|
|
||||||
@order.errors.add(
|
|
||||||
:voucher_code,
|
|
||||||
I18n.t(
|
|
||||||
'checkout.errors.create_voucher_error', error: voucher.errors.full_messages.to_sentence
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_voucher_to_order(voucher)
|
|
||||||
adjustment = voucher.create_adjustment(voucher.code, @order)
|
adjustment = voucher.create_adjustment(voucher.code, @order)
|
||||||
|
|
||||||
unless adjustment.persisted?
|
unless adjustment.persisted?
|
||||||
@@ -69,38 +51,14 @@ class VoucherAdjustmentsController < BaseController
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
# calculate_voucher_adjustment
|
|
||||||
clear_payments
|
clear_payments
|
||||||
|
|
||||||
OrderManagement::Order::Updater.new(@order).update_voucher
|
VoucherAdjustmentsService.new(@order).update
|
||||||
|
@order.update_totals_and_states
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_voucher
|
|
||||||
voucher = Voucher.find_by(code: voucher_params[:voucher_code],
|
|
||||||
enterprise: @order.distributor)
|
|
||||||
return voucher unless voucher.nil? || voucher.is_a?(Vouchers::Vine)
|
|
||||||
|
|
||||||
vine_voucher
|
|
||||||
end
|
|
||||||
|
|
||||||
def vine_voucher
|
|
||||||
vine_voucher_validator = Vine::VoucherValidatorService.new(
|
|
||||||
voucher_code: voucher_params[:voucher_code], enterprise: @order.distributor
|
|
||||||
)
|
|
||||||
voucher = vine_voucher_validator.validate
|
|
||||||
|
|
||||||
return nil if vine_voucher_validator.errors[:not_found_voucher].present?
|
|
||||||
|
|
||||||
if vine_voucher_validator.errors.present?
|
|
||||||
@order.errors.add(:voucher_code, I18n.t('checkout.errors.add_voucher_error'))
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
voucher
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_payment_section
|
def update_payment_section
|
||||||
render cable_ready: cable_car.replace(
|
render cable_ready: cable_car.replace(
|
||||||
selector: "#checkout-payment-methods",
|
selector: "#checkout-payment-methods",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ module Admin
|
|||||||
# e.g producer_options = [['producer name', id]]
|
# e.g producer_options = [['producer name', id]]
|
||||||
product.variants.build do |new_variant|
|
product.variants.build do |new_variant|
|
||||||
new_variant.supplier_id = producer_options.first.second if producer_options.one?
|
new_variant.supplier_id = producer_options.first.second if producer_options.one?
|
||||||
new_variant.tax_category_id = product.variants.first.tax_category_id
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module SharedHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def admin_user?
|
def admin_user?
|
||||||
spree_current_user&.admin?
|
spree_current_user&.has_spree_role? 'admin'
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_shop_products_path
|
def current_shop_products_path
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Spree
|
|
||||||
module Admin
|
|
||||||
module TaxCategoriesHelper
|
|
||||||
def tax_category_dropdown_options(require_tax_category)
|
|
||||||
if require_tax_category
|
|
||||||
{
|
|
||||||
include_blank: false,
|
|
||||||
selected: Spree::TaxCategory.find_by(is_default: true)&.id
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
include_blank: t(:none),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
module Spree
|
module Spree
|
||||||
module BaseHelper
|
module BaseHelper
|
||||||
def available_countries
|
def available_countries
|
||||||
checkout_zone = Zone.find_by(name: ENV.fetch("CHECKOUT_ZONE", nil))
|
checkout_zone = Zone.find_by(name: Spree::Config[:checkout_zone])
|
||||||
|
|
||||||
countries = if checkout_zone && checkout_zone.kind == 'country'
|
countries = if checkout_zone && checkout_zone.kind == 'country'
|
||||||
checkout_zone.countries
|
checkout_zone.countries
|
||||||
|
|||||||
@@ -1,38 +1,98 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# When orders are created, adjusted or cancelled, we need to amend
|
# When orders are cancelled, we need to amend
|
||||||
# an existing backorder as well.
|
# an existing backorder as well.
|
||||||
|
# We're not dealing with line item changes just yet.
|
||||||
class AmendBackorderJob < ApplicationJob
|
class AmendBackorderJob < ApplicationJob
|
||||||
sidekiq_options retry: 0
|
sidekiq_options retry: 0
|
||||||
|
|
||||||
def self.schedule_bulk_update_for(orders)
|
|
||||||
# We can have one backorder per order cycle and distributor.
|
|
||||||
groups = orders.group_by { |order| [order.order_cycle, order.distributor] }
|
|
||||||
groups.each_value do |orders_with_same_backorder|
|
|
||||||
# We need to trigger only one update per backorder.
|
|
||||||
perform_later(orders_with_same_backorder.first)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def perform(order)
|
def perform(order)
|
||||||
OrderLocker.lock_order_and_variants(order) do
|
OrderLocker.lock_order_and_variants(order) do
|
||||||
amend_backorder(order)
|
amend_backorder(order)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The following is a mix of the BackorderJob and the CompleteBackorderJob.
|
||||||
|
# TODO: Move the common code into a re-usable service class.
|
||||||
def amend_backorder(order)
|
def amend_backorder(order)
|
||||||
backorder = BackorderUpdater.new.amend_backorder(order)
|
order_cycle = order.order_cycle
|
||||||
|
distributor = order.distributor
|
||||||
|
user = distributor.owner
|
||||||
|
items = backorderable_items(order)
|
||||||
|
|
||||||
if backorder
|
return if items.empty?
|
||||||
user = order.distributor.owner
|
|
||||||
urls = nil # Not needed to send order. The backorder id is the URL.
|
|
||||||
FdcBackorderer.new(user, urls).send_order(backorder)
|
|
||||||
elsif !order.order_cycle.closed?
|
|
||||||
|
|
||||||
# We don't have an order to amend but the order cycle is or will open.
|
# We are assuming that all variants are linked to the same wholesale
|
||||||
# We can assume that this job was triggered by an admin creating a new
|
# shop and its catalog:
|
||||||
# order or adding backorderable items to an order.
|
reference_link = items[0].variant.semantic_links[0].semantic_id
|
||||||
BackorderJob.new.place_backorder(order)
|
urls = FdcUrlBuilder.new(reference_link)
|
||||||
|
orderer = FdcBackorderer.new(user, urls)
|
||||||
|
|
||||||
|
backorder = orderer.find_open_order(order)
|
||||||
|
|
||||||
|
variants = order_cycle.variants_distributed_by(distributor)
|
||||||
|
adjust_quantities(order_cycle, user, backorder, urls, variants)
|
||||||
|
|
||||||
|
FdcBackorderer.new(user, urls).send_order(backorder)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if we have enough stock to reduce the backorder.
|
||||||
|
#
|
||||||
|
# Our local stock can increase when users cancel their orders.
|
||||||
|
# But stock levels could also have been adjusted manually. So we review all
|
||||||
|
# quantities before finalising the order.
|
||||||
|
def adjust_quantities(order_cycle, user, order, urls, variants)
|
||||||
|
broker = FdcOfferBroker.new(user, urls)
|
||||||
|
|
||||||
|
order.lines.each do |line|
|
||||||
|
line.quantity = line.quantity.to_i
|
||||||
|
wholesale_product_id = line.offer.offeredItem.semanticId
|
||||||
|
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||||
|
linked_variant = variants.linked_to(transformation.retail_product_id)
|
||||||
|
|
||||||
|
# Assumption: If a transformation is present then we only sell the retail
|
||||||
|
# variant. If that can't be found, it was deleted and we'll ignore that
|
||||||
|
# for now.
|
||||||
|
next if linked_variant.nil?
|
||||||
|
|
||||||
|
# Find all line items for this order cycle
|
||||||
|
# Update quantity accordingly
|
||||||
|
if linked_variant.on_demand
|
||||||
|
release_superfluous_stock(line, linked_variant, transformation)
|
||||||
|
else
|
||||||
|
aggregate_final_quantities(order_cycle, line, linked_variant, transformation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clean up empty lines:
|
||||||
|
order.lines.reject! { |line| line.quantity.zero? }
|
||||||
|
end
|
||||||
|
|
||||||
|
# We look at all linked variants.
|
||||||
|
def backorderable_items(order)
|
||||||
|
order.line_items.select do |item|
|
||||||
|
# TODO: scope variants to hub.
|
||||||
|
# We are only supporting producer stock at the moment.
|
||||||
|
item.variant.semantic_links.present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def release_superfluous_stock(line, linked_variant, transformation)
|
||||||
|
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||||
|
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
|
||||||
|
|
||||||
|
# But maybe we didn't actually order that much:
|
||||||
|
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
||||||
|
line.quantity -= deductable_quantity
|
||||||
|
|
||||||
|
retail_stock_changes = deductable_quantity * transformation.factor
|
||||||
|
linked_variant.on_hand -= retail_stock_changes
|
||||||
|
end
|
||||||
|
|
||||||
|
def aggregate_final_quantities(order_cycle, line, variant, transformation)
|
||||||
|
orders = order_cycle.orders.invoiceable
|
||||||
|
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
|
||||||
|
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
|
||||||
|
line.quantity = wholesale_quantity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ class BackorderJob < ApplicationJob
|
|||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
# Errors here shouldn't affect the checkout. So let's report them
|
# Errors here shouldn't affect the checkout. So let's report them
|
||||||
# separately:
|
# separately:
|
||||||
Alert.raise_with_record(e, order)
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata(:order, :order, order)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(order)
|
def perform(order)
|
||||||
@@ -117,8 +119,7 @@ class BackorderJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_broker(user, urls)
|
def load_broker(user, urls)
|
||||||
catalog = DfcCatalog.load(user, urls.catalog_url)
|
FdcOfferBroker.new(user, urls)
|
||||||
FdcOfferBroker.new(catalog)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def place_order(user, order, orderer, backorder)
|
def place_order(user, order, orderer, backorder)
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ class CompleteBackorderJob < ApplicationJob
|
|||||||
|
|
||||||
urls = FdcUrlBuilder.new(order.lines[0].offer.offeredItem.semanticId)
|
urls = FdcUrlBuilder.new(order.lines[0].offer.offeredItem.semanticId)
|
||||||
|
|
||||||
BackorderUpdater.new.update(order, user, distributor, order_cycle)
|
variants = order_cycle.variants_distributed_by(distributor)
|
||||||
|
adjust_quantities(order_cycle, user, order, urls, variants)
|
||||||
|
|
||||||
FdcBackorderer.new(user, urls).complete_order(order)
|
FdcBackorderer.new(user, urls).complete_order(order)
|
||||||
|
|
||||||
@@ -35,4 +36,55 @@ class CompleteBackorderJob < ApplicationJob
|
|||||||
|
|
||||||
raise
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check if we have enough stock to reduce the backorder.
|
||||||
|
#
|
||||||
|
# Our local stock can increase when users cancel their orders.
|
||||||
|
# But stock levels could also have been adjusted manually. So we review all
|
||||||
|
# quantities before finalising the order.
|
||||||
|
def adjust_quantities(order_cycle, user, order, urls, variants)
|
||||||
|
broker = FdcOfferBroker.new(user, urls)
|
||||||
|
|
||||||
|
order.lines.each do |line|
|
||||||
|
line.quantity = line.quantity.to_i
|
||||||
|
wholesale_product_id = line.offer.offeredItem.semanticId
|
||||||
|
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||||
|
linked_variant = variants.linked_to(transformation.retail_product_id)
|
||||||
|
|
||||||
|
# Assumption: If a transformation is present then we only sell the retail
|
||||||
|
# variant. If that can't be found, it was deleted and we'll ignore that
|
||||||
|
# for now.
|
||||||
|
next if linked_variant.nil?
|
||||||
|
|
||||||
|
# Find all line items for this order cycle
|
||||||
|
# Update quantity accordingly
|
||||||
|
if linked_variant.on_demand
|
||||||
|
release_superfluous_stock(line, linked_variant, transformation)
|
||||||
|
else
|
||||||
|
aggregate_final_quantities(order_cycle, line, linked_variant, transformation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Clean up empty lines:
|
||||||
|
order.lines.reject! { |line| line.quantity.zero? }
|
||||||
|
end
|
||||||
|
|
||||||
|
def release_superfluous_stock(line, linked_variant, transformation)
|
||||||
|
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||||
|
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
|
||||||
|
|
||||||
|
# But maybe we didn't actually order that much:
|
||||||
|
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
||||||
|
line.quantity -= deductable_quantity
|
||||||
|
|
||||||
|
retail_stock_changes = deductable_quantity * transformation.factor
|
||||||
|
linked_variant.on_hand -= retail_stock_changes
|
||||||
|
end
|
||||||
|
|
||||||
|
def aggregate_final_quantities(order_cycle, line, variant, transformation)
|
||||||
|
orders = order_cycle.orders.invoiceable
|
||||||
|
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
|
||||||
|
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
|
||||||
|
line.quantity = wholesale_quantity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ class ReportJob < ApplicationJob
|
|||||||
|
|
||||||
broadcast_result(channel, format, blob) if channel
|
broadcast_result(channel, format, blob) if channel
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Alert.raise(e, { report: { report_class:, user:, params:, format: } })
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata :report, {
|
||||||
|
report_class:, user:, params:, format:
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
broadcast_error(channel)
|
broadcast_error(channel)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,12 +8,30 @@ class StockSyncJob < ApplicationJob
|
|||||||
# product. These variants are rare though and we check first before we
|
# product. These variants are rare though and we check first before we
|
||||||
# enqueue a new job. That should save some time loading the order with
|
# enqueue a new job. That should save some time loading the order with
|
||||||
# all the stock data to make this decision.
|
# all the stock data to make this decision.
|
||||||
def self.sync_linked_catalogs_later(order)
|
def self.sync_linked_catalogs(order)
|
||||||
sync_catalogs_by_perform_method(order, :perform_later)
|
user = order.distributor.owner
|
||||||
|
catalog_ids(order).each do |catalog_id|
|
||||||
|
perform_later(user, catalog_id)
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
# Errors here shouldn't affect the shopping. So let's report them
|
||||||
|
# separately:
|
||||||
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata(:order, :order, order)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.sync_linked_catalogs_now(order)
|
def self.sync_linked_catalogs_now(order)
|
||||||
sync_catalogs_by_perform_method(order, :perform_now)
|
user = order.distributor.owner
|
||||||
|
catalog_ids(order).each do |catalog_id|
|
||||||
|
perform_now(user, catalog_id)
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
# Errors here shouldn't affect the shopping. So let's report them
|
||||||
|
# separately:
|
||||||
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata(:order, :order, order)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.catalog_ids(order)
|
def self.catalog_ids(order)
|
||||||
@@ -26,10 +44,7 @@ class StockSyncJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def perform(user, catalog_id)
|
def perform(user, catalog_id)
|
||||||
catalog = DfcCatalog.load(user, catalog_id)
|
products = load_products(user, catalog_id)
|
||||||
catalog.apply_wholesale_values!
|
|
||||||
|
|
||||||
products = catalog.products
|
|
||||||
products_by_id = products.index_by(&:semanticId)
|
products_by_id = products.index_by(&:semanticId)
|
||||||
product_ids = products_by_id.keys
|
product_ids = products_by_id.keys
|
||||||
variants = linked_variants(user.enterprises, product_ids)
|
variants = linked_variants(user.enterprises, product_ids)
|
||||||
@@ -47,23 +62,18 @@ class StockSyncJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_products(user, catalog_id)
|
||||||
|
json_catalog = DfcRequest.new(user).call(catalog_id)
|
||||||
|
graph = DfcIo.import(json_catalog)
|
||||||
|
|
||||||
|
graph.select do |subject|
|
||||||
|
subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def linked_variants(enterprises, product_ids)
|
def linked_variants(enterprises, product_ids)
|
||||||
Spree::Variant.where(supplier: enterprises)
|
Spree::Variant.where(supplier: enterprises)
|
||||||
.includes(:semantic_links).references(:semantic_links)
|
.includes(:semantic_links).references(:semantic_links)
|
||||||
.where(semantic_links: { semantic_id: product_ids })
|
.where(semantic_links: { semantic_id: product_ids })
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.sync_catalogs_by_perform_method(order, perform_method)
|
|
||||||
distributor = order.distributor
|
|
||||||
return unless distributor
|
|
||||||
|
|
||||||
user = distributor.owner
|
|
||||||
catalog_ids(order).each do |catalog_id|
|
|
||||||
public_send(perform_method, user, catalog_id)
|
|
||||||
end
|
|
||||||
rescue StandardError => e
|
|
||||||
# Errors here shouldn't affect the shopping. So let's report them
|
|
||||||
# separately:
|
|
||||||
Alert.raise_with_record(e, order)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ class SubscriptionConfirmJob < ApplicationJob
|
|||||||
if order.errors.any?
|
if order.errors.any?
|
||||||
send_failed_payment_email(order)
|
send_failed_payment_email(order)
|
||||||
else
|
else
|
||||||
Alert.raise_with_record(e, order)
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata :order, :order, order
|
||||||
|
end
|
||||||
send_failed_payment_email(order, e.message)
|
send_failed_payment_email(order, e.message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -106,6 +108,8 @@ class SubscriptionConfirmJob < ApplicationJob
|
|||||||
record_and_log_error(:failed_payment, order, error_message)
|
record_and_log_error(:failed_payment, order, error_message)
|
||||||
SubscriptionMailer.failed_payment_email(order).deliver_now
|
SubscriptionMailer.failed_payment_email(order).deliver_now
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Alert.raise(e, { subscription_data: { order:, error_message: } })
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata :subscription_data, { order:, error_message: }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -79,19 +79,14 @@ class ProducerMailer < ApplicationMailer
|
|||||||
def set_customer_data(line_items)
|
def set_customer_data(line_items)
|
||||||
return unless @coordinator.show_customer_names_to_suppliers?
|
return unless @coordinator.show_customer_names_to_suppliers?
|
||||||
|
|
||||||
@display_business_name = false
|
|
||||||
line_items.map do |line_item|
|
line_items.map do |line_item|
|
||||||
customer_code = line_item.order.customer&.code
|
|
||||||
@display_business_name = true if customer_code.present?
|
|
||||||
|
|
||||||
{
|
{
|
||||||
sku: line_item.variant.sku,
|
sku: line_item.variant.sku,
|
||||||
supplier_name: line_item.variant.supplier.name,
|
supplier_name: line_item.variant.supplier.name,
|
||||||
product_and_full_name: line_item.product_and_full_name,
|
product_and_full_name: line_item.product_and_full_name,
|
||||||
quantity: line_item.quantity,
|
quantity: line_item.quantity,
|
||||||
first_name: line_item.order.billing_address.first_name,
|
first_name: line_item.order.billing_address.first_name,
|
||||||
last_name: line_item.order.billing_address.last_name,
|
last_name: line_item.order.billing_address.last_name
|
||||||
business_name: customer_code,
|
|
||||||
}
|
}
|
||||||
end.sort_by { |line_item| [line_item[:last_name].downcase, line_item[:first_name].downcase] }
|
end.sort_by { |line_item| [line_item[:last_name].downcase, line_item[:first_name].downcase] }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ module Calculator
|
|||||||
# In theory it should never be called any more after this has been deployed.
|
# In theory it should never be called any more after this has been deployed.
|
||||||
# If the message below doesn't show up in Bugsnag, we can safely delete this method and all
|
# If the message below doesn't show up in Bugsnag, we can safely delete this method and all
|
||||||
# the related methods below it.
|
# the related methods below it.
|
||||||
Alert.raise("Calculator::DefaultTax was called with legacy tax calculations")
|
Bugsnag.notify("Calculator::DefaultTax was called with legacy tax calculations")
|
||||||
|
|
||||||
calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(order.distributor,
|
calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(order.distributor,
|
||||||
order.order_cycle)
|
order.order_cycle)
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "active_support/concern"
|
|
||||||
|
|
||||||
module Vouchers
|
|
||||||
module FlatRatable
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
included do
|
|
||||||
validates :amount,
|
|
||||||
presence: true,
|
|
||||||
numericality: { greater_than: 0 }
|
|
||||||
end
|
|
||||||
|
|
||||||
def display_value
|
|
||||||
Spree::Money.new(amount)
|
|
||||||
end
|
|
||||||
|
|
||||||
# We limit adjustment to the maximum amount needed to cover the order, ie if the voucher
|
|
||||||
# covers more than the order.total we only need to create an adjustment covering the order.total
|
|
||||||
def compute_amount(order)
|
|
||||||
-amount.clamp(0, order.pre_discount_total)
|
|
||||||
end
|
|
||||||
|
|
||||||
def rate(order)
|
|
||||||
amount = compute_amount(order)
|
|
||||||
|
|
||||||
amount / order.pre_discount_total
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -218,7 +218,7 @@ class Enterprise < ApplicationRecord
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope :managed_by, lambda { |user|
|
scope :managed_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
joins(:enterprise_roles).where(enterprise_roles: { user_id: user.id })
|
joins(:enterprise_roles).where(enterprise_roles: { user_id: user.id })
|
||||||
@@ -481,7 +481,7 @@ class Enterprise < ApplicationRecord
|
|||||||
|
|
||||||
image_variant_url_for(image.variant(name))
|
image_variant_url_for(image.variant(name))
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Alert.raise "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
|
Bugsnag.notify "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
|
||||||
Rails.logger.error(e.message)
|
Rails.logger.error(e.message)
|
||||||
|
|
||||||
nil
|
nil
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class EnterpriseFee < ApplicationRecord
|
|||||||
scope :for_enterprises, lambda { |enterprises| where(enterprise_id: enterprises) }
|
scope :for_enterprises, lambda { |enterprises| where(enterprise_id: enterprises) }
|
||||||
|
|
||||||
scope :managed_by, lambda { |user|
|
scope :managed_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
where(enterprise_id: user.enterprises.select(&:id))
|
where(enterprise_id: user.enterprises.select(&:id))
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class EnterpriseGroup < ApplicationRecord
|
|||||||
scope :by_position, -> { order('position ASC') }
|
scope :by_position, -> { order('position ASC') }
|
||||||
scope :on_front_page, -> { where(on_front_page: true) }
|
scope :on_front_page, -> { where(on_front_page: true) }
|
||||||
scope :managed_by, lambda { |user|
|
scope :managed_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
where(owner_id: user.id)
|
where(owner_id: user.id)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class Exchange < ApplicationRecord
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope :managed_by, lambda { |user|
|
scope :managed_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
joins("LEFT JOIN enterprises senders ON senders.id = exchanges.sender_id").
|
joins("LEFT JOIN enterprises senders ON senders.id = exchanges.sender_id").
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ class Invoice < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def previous_invoice
|
def previous_invoice
|
||||||
order.invoices.where(id: ...id).first
|
order.invoices.where("id < ?", id).first
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class OrderCycle < ApplicationRecord
|
|||||||
Time.zone.now,
|
Time.zone.now,
|
||||||
Time.zone.now)
|
Time.zone.now)
|
||||||
}
|
}
|
||||||
scope :active_or_complete, lambda { where(order_cycles: { orders_open_at: ..Time.zone.now }) }
|
scope :active_or_complete, lambda { where('order_cycles.orders_open_at <= ?', Time.zone.now) }
|
||||||
scope :inactive, lambda {
|
scope :inactive, lambda {
|
||||||
where('order_cycles.orders_open_at > ? OR order_cycles.orders_close_at < ?',
|
where('order_cycles.orders_open_at > ? OR order_cycles.orders_close_at < ?',
|
||||||
Time.zone.now,
|
Time.zone.now,
|
||||||
@@ -64,8 +64,8 @@ class OrderCycle < ApplicationRecord
|
|||||||
where('order_cycles.orders_close_at > ? OR order_cycles.orders_close_at IS NULL', Time.zone.now)
|
where('order_cycles.orders_close_at > ? OR order_cycles.orders_close_at IS NULL', Time.zone.now)
|
||||||
}
|
}
|
||||||
scope :closed, lambda {
|
scope :closed, lambda {
|
||||||
where(order_cycles: { orders_close_at: ...Time.zone.now })
|
where('order_cycles.orders_close_at < ?',
|
||||||
.order("order_cycles.orders_close_at DESC")
|
Time.zone.now).order("order_cycles.orders_close_at DESC")
|
||||||
}
|
}
|
||||||
scope :unprocessed, -> { where(processed_at: nil) }
|
scope :unprocessed, -> { where(processed_at: nil) }
|
||||||
scope :undated, -> { where('order_cycles.orders_open_at IS NULL OR orders_close_at IS NULL') }
|
scope :undated, -> { where('order_cycles.orders_open_at IS NULL OR orders_close_at IS NULL') }
|
||||||
@@ -84,7 +84,7 @@ class OrderCycle < ApplicationRecord
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope :managed_by, lambda { |user|
|
scope :managed_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
where(coordinator_id: user.enterprises.to_a)
|
where(coordinator_id: user.enterprises.to_a)
|
||||||
@@ -93,7 +93,7 @@ class OrderCycle < ApplicationRecord
|
|||||||
|
|
||||||
# Return order cycles that user coordinates, sends to or receives from
|
# Return order cycles that user coordinates, sends to or receives from
|
||||||
scope :visible_by, lambda { |user|
|
scope :visible_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
with_exchanging_enterprises_outer.
|
with_exchanging_enterprises_outer.
|
||||||
|
|||||||
@@ -79,9 +79,10 @@ module ProductImport
|
|||||||
if entry.attributes['on_hand'].present?
|
if entry.attributes['on_hand'].present?
|
||||||
new_variant.on_hand = entry.attributes['on_hand']
|
new_variant.on_hand = entry.attributes['on_hand']
|
||||||
end
|
end
|
||||||
check_on_hand_nil(entry, new_variant)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
check_on_hand_nil(entry, new_variant)
|
||||||
|
|
||||||
if new_variant.valid?
|
if new_variant.valid?
|
||||||
entry.product_object = new_variant
|
entry.product_object = new_variant
|
||||||
entry.validates_as = 'new_variant' unless entry.errors?
|
entry.validates_as = 'new_variant' unless entry.errors?
|
||||||
@@ -160,7 +161,7 @@ module ProductImport
|
|||||||
end
|
end
|
||||||
|
|
||||||
def unit_fields_validation(entry)
|
def unit_fields_validation(entry)
|
||||||
unit_types = ['mg', 'g', 'kg', 'oz', 'lb', 't', 'ml', 'cl', 'dl', 'l', 'kl', 'gal', '']
|
unit_types = ['g', 'oz', 'lb', 'kg', 't', 'ml', 'l', 'kl', '']
|
||||||
|
|
||||||
if entry.units.blank?
|
if entry.units.blank?
|
||||||
mark_as_invalid(entry, attribute: 'units',
|
mark_as_invalid(entry, attribute: 'units',
|
||||||
@@ -296,7 +297,7 @@ module ProductImport
|
|||||||
unscaled_units = entry.unscaled_units.to_f || 0
|
unscaled_units = entry.unscaled_units.to_f || 0
|
||||||
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
|
entry.unit_value = unscaled_units * unit_scale unless unit_scale.nil?
|
||||||
|
|
||||||
if entry.match_variant?(existing_variant)
|
if entry.match_inventory_variant?(existing_variant)
|
||||||
variant_override = create_inventory_item(entry, existing_variant)
|
variant_override = create_inventory_item(entry, existing_variant)
|
||||||
return validate_inventory_item(entry, variant_override)
|
return validate_inventory_item(entry, variant_override)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ module ProductImport
|
|||||||
end
|
end
|
||||||
|
|
||||||
def match_variant?(variant)
|
def match_variant?(variant)
|
||||||
|
match_display_name?(variant) && variant.unit_value.to_d == unscaled_units.to_d
|
||||||
|
end
|
||||||
|
|
||||||
|
def match_inventory_variant?(variant)
|
||||||
match_display_name?(variant) && variant.unit_value.to_d == unit_value.to_d
|
match_display_name?(variant) && variant.unit_value.to_d == unit_value.to_d
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -32,18 +32,14 @@ module ProductImport
|
|||||||
|
|
||||||
def unit_scales
|
def unit_scales
|
||||||
{
|
{
|
||||||
'mg' => { scale: 0.001, unit: 'weight' },
|
|
||||||
'g' => { scale: 1, unit: 'weight' },
|
'g' => { scale: 1, unit: 'weight' },
|
||||||
'kg' => { scale: 1000, unit: 'weight' },
|
'kg' => { scale: 1000, unit: 'weight' },
|
||||||
'oz' => { scale: 28.35, unit: 'weight' },
|
'oz' => { scale: 28.35, unit: 'weight' },
|
||||||
'lb' => { scale: 453.6, unit: 'weight' },
|
'lb' => { scale: 453.6, unit: 'weight' },
|
||||||
't' => { scale: 1_000_000, unit: 'weight' },
|
't' => { scale: 1_000_000, unit: 'weight' },
|
||||||
'ml' => { scale: 0.001, unit: 'volume' },
|
'ml' => { scale: 0.001, unit: 'volume' },
|
||||||
'cl' => { scale: 0.01, unit: 'volume' },
|
|
||||||
'dl' => { scale: 0.1, unit: 'volume' },
|
|
||||||
'l' => { scale: 1, unit: 'volume' },
|
'l' => { scale: 1, unit: 'volume' },
|
||||||
'kl' => { scale: 1000, unit: 'volume' },
|
'kl' => { scale: 1000, unit: 'volume' }
|
||||||
'gal' => { scale: 4.54609, unit: 'volume' },
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module Spree
|
|||||||
|
|
||||||
user ||= Spree::User.new
|
user ||= Spree::User.new
|
||||||
|
|
||||||
if user.try(:admin?)
|
if user.respond_to?(:has_spree_role?) && user.has_spree_role?('admin')
|
||||||
can :manage, :all
|
can :manage, :all
|
||||||
else
|
else
|
||||||
can [:index, :read], Country
|
can [:index, :read], Country
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ module Spree
|
|||||||
end
|
end
|
||||||
|
|
||||||
def address_and_city
|
def address_and_city
|
||||||
[address1, address2, city].compact_blank.join(' ')
|
[address1, address2, city].select(&:present?).join(' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -176,7 +176,7 @@ module Spree
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render_address(parts)
|
def render_address(parts)
|
||||||
parts.compact_blank.join(', ')
|
parts.select(&:present?).join(', ')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ module Spree
|
|||||||
preference :allow_backorder_shipping, :boolean, default: false
|
preference :allow_backorder_shipping, :boolean, default: false
|
||||||
preference :allow_checkout_on_gateway_error, :boolean, default: false
|
preference :allow_checkout_on_gateway_error, :boolean, default: false
|
||||||
preference :allow_guest_checkout, :boolean, default: true
|
preference :allow_guest_checkout, :boolean, default: true
|
||||||
|
# Replace with the name of a zone if you would like to limit the countries
|
||||||
|
preference :checkout_zone, :string, default: nil
|
||||||
|
preference :currency, :string, default: "USD"
|
||||||
preference :currency_decimal_mark, :string, default: "."
|
preference :currency_decimal_mark, :string, default: "."
|
||||||
preference :currency_symbol_position, :string, default: "before"
|
preference :currency_symbol_position, :string, default: "before"
|
||||||
preference :currency_thousands_separator, :string, default: ","
|
preference :currency_thousands_separator, :string, default: ","
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ module Spree
|
|||||||
|
|
||||||
image_variant_url_for(variant(size))
|
image_variant_url_for(variant(size))
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Alert.raise "Product ##{viewable_id} Image ##{id} error: #{e.message}"
|
Bugsnag.notify "Product ##{viewable_id} Image ##{id} error: #{e.message}"
|
||||||
Rails.logger.error(e.message)
|
Rails.logger.error(e.message)
|
||||||
|
|
||||||
self.class.default_image_url(size)
|
self.class.default_image_url(size)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ module Spree
|
|||||||
|
|
||||||
# -- Scopes
|
# -- Scopes
|
||||||
scope :managed_by, lambda { |user|
|
scope :managed_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
# Find line items that are from orders distributed by the user or supplied by the user
|
# Find line items that are from orders distributed by the user or supplied by the user
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ module Spree
|
|||||||
before_validation :clone_billing_address, if: :use_billing?
|
before_validation :clone_billing_address, if: :use_billing?
|
||||||
before_validation :ensure_customer
|
before_validation :ensure_customer
|
||||||
|
|
||||||
|
before_save :update_shipping_fees!, if: :complete?
|
||||||
before_save :update_payment_fees!, if: :complete?
|
before_save :update_payment_fees!, if: :complete?
|
||||||
before_create :link_by_email
|
before_create :link_by_email
|
||||||
|
|
||||||
@@ -124,7 +125,7 @@ module Spree
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope :managed_by, lambda { |user|
|
scope :managed_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
# Find orders that are distributed by the user or have products supplied by the user
|
# Find orders that are distributed by the user or have products supplied by the user
|
||||||
@@ -139,7 +140,7 @@ module Spree
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope :distributed_by_user, lambda { |user|
|
scope :distributed_by_user, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
where(spree_orders: { distributor_id: user.enterprises.select(&:id) })
|
where(spree_orders: { distributor_id: user.enterprises.select(&:id) })
|
||||||
@@ -391,6 +392,8 @@ module Spree
|
|||||||
|
|
||||||
deliver_order_confirmation_email
|
deliver_order_confirmation_email
|
||||||
|
|
||||||
|
BackorderJob.check_stock(self)
|
||||||
|
|
||||||
state_changes.create(
|
state_changes.create(
|
||||||
previous_state: 'cart',
|
previous_state: 'cart',
|
||||||
next_state: 'complete',
|
next_state: 'complete',
|
||||||
@@ -532,7 +535,7 @@ module Spree
|
|||||||
# because an outdated shipping fee is not as bad as a lost payment.
|
# because an outdated shipping fee is not as bad as a lost payment.
|
||||||
# And the shipping fee is already up-to-date when this error occurs.
|
# And the shipping fee is already up-to-date when this error occurs.
|
||||||
# https://github.com/openfoodfoundation/openfoodnetwork/issues/3924
|
# https://github.com/openfoodfoundation/openfoodnetwork/issues/3924
|
||||||
Alert.raise(e) do |report|
|
Bugsnag.notify(e) do |report|
|
||||||
report.add_metadata(:order, attributes)
|
report.add_metadata(:order, attributes)
|
||||||
report.add_metadata(:shipment, shipment.attributes)
|
report.add_metadata(:shipment, shipment.attributes)
|
||||||
report.add_metadata(:shipment_in_db, Spree::Shipment.find_by(id: shipment.id).attributes)
|
report.add_metadata(:shipment_in_db, Spree::Shipment.find_by(id: shipment.id).attributes)
|
||||||
@@ -671,6 +674,10 @@ module Spree
|
|||||||
break if payment_total >= total
|
break if payment_total >= total
|
||||||
|
|
||||||
yield payment
|
yield payment
|
||||||
|
|
||||||
|
if payment.completed?
|
||||||
|
self.payment_total += payment.amount
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ module Spree
|
|||||||
|
|
||||||
OrderMailer.cancel_email(id).deliver_later if send_cancellation_email
|
OrderMailer.cancel_email(id).deliver_later if send_cancellation_email
|
||||||
update(payment_state: updater.update_payment_state)
|
update(payment_state: updater.update_payment_state)
|
||||||
|
|
||||||
|
AmendBackorderJob.perform_later(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_resume
|
def after_resume
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ module Spree
|
|||||||
line_item.price = variant.price
|
line_item.price = variant.price
|
||||||
order.line_items << line_item
|
order.line_items << line_item
|
||||||
end
|
end
|
||||||
update_shipment
|
|
||||||
|
|
||||||
order.reload
|
order.reload
|
||||||
line_item
|
line_item
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ module Spree
|
|||||||
scope :by_name, -> { order('spree_products.name') }
|
scope :by_name, -> { order('spree_products.name') }
|
||||||
|
|
||||||
scope :managed_by, lambda { |user|
|
scope :managed_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
in_supplier(user.enterprises)
|
in_supplier(user.enterprises)
|
||||||
|
|||||||
@@ -4,10 +4,5 @@ module Spree
|
|||||||
class Role < ApplicationRecord
|
class Role < ApplicationRecord
|
||||||
has_and_belongs_to_many :users, join_table: 'spree_roles_users',
|
has_and_belongs_to_many :users, join_table: 'spree_roles_users',
|
||||||
class_name: "Spree::User"
|
class_name: "Spree::User"
|
||||||
|
|
||||||
# The only role we have at the moment:
|
|
||||||
def self.admin
|
|
||||||
Spree::Role.find_or_create_by(name: 'admin')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ module Spree
|
|||||||
after_save :touch_distributors
|
after_save :touch_distributors
|
||||||
|
|
||||||
scope :managed_by, lambda { |user|
|
scope :managed_by, lambda { |user|
|
||||||
if user.admin?
|
if user.has_spree_role?('admin')
|
||||||
where(nil)
|
where(nil)
|
||||||
else
|
else
|
||||||
joins(:distributors).
|
joins(:distributors).
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ module Spree
|
|||||||
calculator.compute(item)
|
calculator.compute(item)
|
||||||
else
|
else
|
||||||
# Tax refund should not be possible with the way our production server are configured
|
# Tax refund should not be possible with the way our production server are configured
|
||||||
Alert.raise(
|
Bugsnag.notify(
|
||||||
"Notice: Tax refund should not be possible, please check the default zone and " \
|
"Notice: Tax refund should not be possible, please check the default zone and " \
|
||||||
"the tax rate zone configuration"
|
"the tax rate zone configuration"
|
||||||
) do |payload|
|
) do |payload|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ module Spree
|
|||||||
after_create :associate_customers, :associate_orders
|
after_create :associate_customers, :associate_orders
|
||||||
before_destroy :check_completed_orders
|
before_destroy :check_completed_orders
|
||||||
|
|
||||||
scope :admin, lambda { includes(:spree_roles).where("spree_roles.name" => "admin") }
|
roles_table_name = Role.table_name
|
||||||
|
|
||||||
|
scope :admin, lambda { includes(:spree_roles).where("#{roles_table_name}.name" => "admin") }
|
||||||
|
|
||||||
has_many :enterprise_roles, dependent: :destroy
|
has_many :enterprise_roles, dependent: :destroy
|
||||||
has_many :enterprises, through: :enterprise_roles
|
has_many :enterprises, through: :enterprise_roles
|
||||||
@@ -58,9 +60,14 @@ module Spree
|
|||||||
User.admin.count > 0
|
User.admin.count > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Whether a user has a role or not.
|
||||||
|
def has_spree_role?(role_in_question)
|
||||||
|
spree_roles.where(name: role_in_question.to_s).any?
|
||||||
|
end
|
||||||
|
|
||||||
# Checks whether the specified user is a superadmin, with full control of the instance
|
# Checks whether the specified user is a superadmin, with full control of the instance
|
||||||
def admin?
|
def admin?
|
||||||
spree_roles.any? { |role| role.name == "admin" }
|
has_spree_role?('admin')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Send devise-based user emails asyncronously via ActiveJob
|
# Send devise-based user emails asyncronously via ActiveJob
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ module Spree
|
|||||||
end
|
end
|
||||||
|
|
||||||
def ensure_unit_value
|
def ensure_unit_value
|
||||||
Alert.raise("Trying to set unit_value to NaN") if unit_value&.nan?
|
Bugsnag.notify("Trying to set unit_value to NaN") if unit_value&.nan?
|
||||||
return unless (variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
|
return unless (variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
|
||||||
|
|
||||||
self.unit_value = 1.0
|
self.unit_value = 1.0
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class StripeAccount < ApplicationRecord
|
|||||||
|
|
||||||
destroy && Stripe::OAuth.deauthorize(stripe_user_id:)
|
destroy && Stripe::OAuth.deauthorize(stripe_user_id:)
|
||||||
rescue Stripe::OAuth::OAuthError => e
|
rescue Stripe::OAuth::OAuthError => e
|
||||||
Alert.raise(
|
Bugsnag.notify(
|
||||||
e,
|
e,
|
||||||
stripe_account: stripe_user_id,
|
stripe_account: stripe_user_id,
|
||||||
enterprise_id:
|
enterprise_id:
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ class VariantOverride < ApplicationRecord
|
|||||||
|
|
||||||
def move_stock!(quantity)
|
def move_stock!(quantity)
|
||||||
unless stock_overridden?
|
unless stock_overridden?
|
||||||
Alert.raise "Attempting to move stock of a VariantOverride " \
|
Bugsnag.notify RuntimeError.new "Attempting to move stock of a VariantOverride " \
|
||||||
"without a count_on_hand specified."
|
"without a count_on_hand specified."
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -73,8 +73,8 @@ class VariantOverride < ApplicationRecord
|
|||||||
self.attributes = { on_demand: false, count_on_hand: default_stock }
|
self.attributes = { on_demand: false, count_on_hand: default_stock }
|
||||||
save
|
save
|
||||||
else
|
else
|
||||||
Alert.raise "Attempting to reset stock level for a variant " \
|
Bugsnag.notify RuntimeError.new "Attempting to reset stock level for a variant " \
|
||||||
"with no default stock level."
|
"with no default stock level."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Voucher < ApplicationRecord
|
|||||||
class_name: 'Spree::Adjustment',
|
class_name: 'Spree::Adjustment',
|
||||||
dependent: nil
|
dependent: nil
|
||||||
|
|
||||||
validates :code, presence: true
|
validates :code, presence: true, uniqueness: { scope: :enterprise_id }
|
||||||
|
|
||||||
TYPES = ["Vouchers::FlatRate", "Vouchers::PercentageRate"].freeze
|
TYPES = ["Vouchers::FlatRate", "Vouchers::PercentageRate"].freeze
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,24 @@
|
|||||||
|
|
||||||
module Vouchers
|
module Vouchers
|
||||||
class FlatRate < Voucher
|
class FlatRate < Voucher
|
||||||
include FlatRatable
|
validates :amount,
|
||||||
|
presence: true,
|
||||||
|
numericality: { greater_than: 0 }
|
||||||
|
|
||||||
validates_with ScopedUniquenessValidator
|
def display_value
|
||||||
|
Spree::Money.new(amount)
|
||||||
|
end
|
||||||
|
|
||||||
|
# We limit adjustment to the maximum amount needed to cover the order, ie if the voucher
|
||||||
|
# covers more than the order.total we only need to create an adjustment covering the order.total
|
||||||
|
def compute_amount(order)
|
||||||
|
-amount.clamp(0, order.pre_discount_total)
|
||||||
|
end
|
||||||
|
|
||||||
|
def rate(order)
|
||||||
|
amount = compute_amount(order)
|
||||||
|
|
||||||
|
amount / order.pre_discount_total
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ module Vouchers
|
|||||||
validates :amount,
|
validates :amount,
|
||||||
presence: true,
|
presence: true,
|
||||||
numericality: { greater_than: 0, less_than_or_equal_to: 100 }
|
numericality: { greater_than: 0, less_than_or_equal_to: 100 }
|
||||||
validates_with ScopedUniquenessValidator
|
|
||||||
|
|
||||||
def display_value
|
def display_value
|
||||||
ActionController::Base.helpers.number_to_percentage(amount, precision: 2)
|
ActionController::Base.helpers.number_to_percentage(amount, precision: 2)
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
# frozen_string_literal: false
|
|
||||||
|
|
||||||
module Vouchers
|
|
||||||
class Vine < Voucher
|
|
||||||
include FlatRatable
|
|
||||||
|
|
||||||
# a VINE voucher :
|
|
||||||
# - can potentially be associated with mutiple enterprise
|
|
||||||
# - code ( "short code" in VINE ) can be recycled, but they shouldn't be linked to the same
|
|
||||||
# voucher_id
|
|
||||||
validates :code, uniqueness: { scope: [:enterprise_id, :external_voucher_id] }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -46,7 +46,7 @@ class ProductScopeQuery
|
|||||||
end
|
end
|
||||||
|
|
||||||
def product_scope
|
def product_scope
|
||||||
if @user.admin? || @user.enterprises.present?
|
if @user.has_spree_role?("admin") || @user.enterprises.present?
|
||||||
scope = Spree::Product
|
scope = Spree::Product
|
||||||
if @params[:show_deleted]
|
if @params[:show_deleted]
|
||||||
scope = scope.with_deleted
|
scope = scope.with_deleted
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module Api
|
|||||||
:terms_and_conditions_file_name, :terms_and_conditions_updated_at,
|
:terms_and_conditions_file_name, :terms_and_conditions_updated_at,
|
||||||
:preferred_invoice_order_by_supplier, :preferred_product_low_stock_display,
|
:preferred_invoice_order_by_supplier, :preferred_product_low_stock_display,
|
||||||
:visible, :hide_ofn_navigation, :white_label_logo,
|
:visible, :hide_ofn_navigation, :white_label_logo,
|
||||||
:white_label_logo_link, :external_billing_id
|
:white_label_logo_link
|
||||||
|
|
||||||
has_one :owner, serializer: Api::Admin::UserSerializer
|
has_one :owner, serializer: Api::Admin::UserSerializer
|
||||||
has_many :users, serializer: Api::Admin::UserSerializer
|
has_many :users, serializer: Api::Admin::UserSerializer
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ module Api
|
|||||||
attributes :id, :hub_id, :variant_id, :sku, :price, :count_on_hand, :on_demand,
|
attributes :id, :hub_id, :variant_id, :sku, :price, :count_on_hand, :on_demand,
|
||||||
:default_stock, :resettable, :tag_list, :tags, :import_date
|
:default_stock, :resettable, :tag_list, :tags, :import_date
|
||||||
|
|
||||||
def count_on_hand
|
|
||||||
return if object.on_demand
|
|
||||||
|
|
||||||
object.count_on_hand
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_list
|
def tag_list
|
||||||
object.tag_list.join(",")
|
object.tag_list.join(",")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def on_hand
|
def on_hand
|
||||||
return if object.on_demand
|
|
||||||
return 0 if object.on_hand.nil?
|
return 0 if object.on_hand.nil?
|
||||||
|
|
||||||
object.on_hand
|
object.on_hand
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class AddressGeocoder
|
|||||||
attr_reader :address
|
attr_reader :address
|
||||||
|
|
||||||
def geocode_address
|
def geocode_address
|
||||||
address_parts.compact_blank.join(', ')
|
address_parts.select(&:present?).join(', ')
|
||||||
end
|
end
|
||||||
|
|
||||||
def address_parts
|
def address_parts
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# A handy wrapper around error reporting libraries like Bugsnag.
|
|
||||||
#
|
|
||||||
# Bugsnag's API is great for general purpose but overly complex for our use.
|
|
||||||
# It also changes over time and we often make mistakes using it. So this class
|
|
||||||
# aims at:
|
|
||||||
#
|
|
||||||
# * Abstracting from Bugsnag, open for other services.
|
|
||||||
# * Simpler interface to reduce user error.
|
|
||||||
# * Central place to update Bugsnag API usage when it changes.
|
|
||||||
#
|
|
||||||
class Alert
|
|
||||||
# Alert Bugsnag with additional metadata to appear in tabs.
|
|
||||||
#
|
|
||||||
# Alert.raise(
|
|
||||||
# "Invalid order during checkout",
|
|
||||||
# {
|
|
||||||
# order: { number: "ABC123", state: "awaiting_return" },
|
|
||||||
# env: { referer: "example.com" }
|
|
||||||
# }
|
|
||||||
# )
|
|
||||||
def self.raise(error, metadata = {}, &block)
|
|
||||||
Bugsnag.notify(error) do |payload|
|
|
||||||
metadata.each do |name, data|
|
|
||||||
payload.add_metadata(name, data)
|
|
||||||
end
|
|
||||||
block.call(payload)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.raise_with_record(error, record, &)
|
|
||||||
metadata = {
|
|
||||||
record.class.name => record&.attributes || { record_was_nil: true }
|
|
||||||
}
|
|
||||||
self.raise(error, metadata, &)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'open_food_network/order_cycle_permissions'
|
|
||||||
|
|
||||||
# Update a backorder to reflect all local orders and stock levels
|
|
||||||
# connected to the associated order cycle.
|
|
||||||
class BackorderUpdater
|
|
||||||
# Given an OFN order was created, changed or cancelled,
|
|
||||||
# we re-calculate how much to order in for every variant.
|
|
||||||
def amend_backorder(order)
|
|
||||||
order_cycle = order.order_cycle
|
|
||||||
distributor = order.distributor
|
|
||||||
variants = distributed_linked_variants(order_cycle, distributor)
|
|
||||||
|
|
||||||
# Temporary code: once we don't need a variant link to look up the
|
|
||||||
# backorder, we don't need this check anymore.
|
|
||||||
# Then we can adjust the backorder even though there are no linked variants
|
|
||||||
# in the order cycle right now. Some variants may have been in the order
|
|
||||||
# cycle before and got ordered before being removed from the order cycle.
|
|
||||||
return unless variants.any?
|
|
||||||
|
|
||||||
# We are assuming that all variants are linked to the same wholesale
|
|
||||||
# shop and its catalog:
|
|
||||||
reference_link = variants[0].semantic_links[0].semantic_id
|
|
||||||
user = order.distributor.owner
|
|
||||||
urls = FdcUrlBuilder.new(reference_link)
|
|
||||||
orderer = FdcBackorderer.new(user, urls)
|
|
||||||
|
|
||||||
backorder = orderer.find_open_order(order)
|
|
||||||
|
|
||||||
update(backorder, user, distributor, order_cycle) if backorder
|
|
||||||
end
|
|
||||||
|
|
||||||
# Update a given backorder according to a distributor's order cycle.
|
|
||||||
def update(backorder, user, distributor, order_cycle)
|
|
||||||
variants = distributed_linked_variants(order_cycle, distributor)
|
|
||||||
|
|
||||||
# We are assuming that all variants are linked to the same wholesale
|
|
||||||
# shop and its catalog:
|
|
||||||
reference_link = variants[0].semantic_links[0].semantic_id
|
|
||||||
urls = FdcUrlBuilder.new(reference_link)
|
|
||||||
orderer = FdcBackorderer.new(user, urls)
|
|
||||||
catalog = DfcCatalog.load(user, urls.catalog_url)
|
|
||||||
broker = FdcOfferBroker.new(catalog)
|
|
||||||
|
|
||||||
updated_lines = update_order_lines(backorder, order_cycle, variants, broker, orderer)
|
|
||||||
unprocessed_lines = backorder.lines.to_set - updated_lines
|
|
||||||
managed_variants = managed_linked_variants(user, order_cycle, distributor)
|
|
||||||
cancel_stale_lines(unprocessed_lines, managed_variants, broker)
|
|
||||||
|
|
||||||
# Clean up empty lines:
|
|
||||||
backorder.lines.reject! { |line| line.quantity.zero? }
|
|
||||||
|
|
||||||
backorder
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_order_lines(backorder, order_cycle, variants, broker, orderer)
|
|
||||||
variants.map do |variant|
|
|
||||||
link = variant.semantic_links[0].semantic_id
|
|
||||||
solution = broker.best_offer(link)
|
|
||||||
|
|
||||||
next unless solution.offer
|
|
||||||
|
|
||||||
line = orderer.find_or_build_order_line(backorder, solution.offer)
|
|
||||||
if variant.on_demand
|
|
||||||
adjust_stock(variant, solution, line)
|
|
||||||
else
|
|
||||||
aggregate_final_quantities(order_cycle, line, variant, solution)
|
|
||||||
end
|
|
||||||
|
|
||||||
line
|
|
||||||
end.compact
|
|
||||||
end
|
|
||||||
|
|
||||||
def cancel_stale_lines(unprocessed_lines, managed_variants, broker)
|
|
||||||
unprocessed_lines.each do |line|
|
|
||||||
wholesale_quantity = line.quantity.to_i
|
|
||||||
wholesale_product_id = line.offer.offeredItem.semanticId
|
|
||||||
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
|
||||||
linked_variant = managed_variants.linked_to(transformation.retail_product_id)
|
|
||||||
|
|
||||||
if linked_variant.nil?
|
|
||||||
transformation.factor = 1
|
|
||||||
linked_variant = managed_variants.linked_to(wholesale_product_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adjust stock level back, we're not going to order this one.
|
|
||||||
if linked_variant&.on_demand
|
|
||||||
retail_quantity = wholesale_quantity * transformation.factor
|
|
||||||
linked_variant.on_hand -= retail_quantity
|
|
||||||
end
|
|
||||||
|
|
||||||
# We don't have any active orders for this
|
|
||||||
line.quantity = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def adjust_stock(variant, solution, line)
|
|
||||||
line.quantity = line.quantity.to_i
|
|
||||||
|
|
||||||
if variant.on_hand.negative?
|
|
||||||
needed_quantity = -1 * variant.on_hand # We need to replenish it.
|
|
||||||
|
|
||||||
# The number of wholesale packs we need to order to fulfill the
|
|
||||||
# needed quantity.
|
|
||||||
# For example, we order 2 packs of 12 cans if we need 15 cans.
|
|
||||||
wholesale_quantity = (needed_quantity.to_f / solution.factor).ceil
|
|
||||||
|
|
||||||
# The number of individual retail items we get with the wholesale order.
|
|
||||||
# For example, if we order 2 packs of 12 cans, we will get 24 cans
|
|
||||||
# and we'll account for that in our stock levels.
|
|
||||||
retail_quantity = wholesale_quantity * solution.factor
|
|
||||||
|
|
||||||
line.quantity += wholesale_quantity
|
|
||||||
variant.on_hand += retail_quantity
|
|
||||||
else
|
|
||||||
# Note that a division of integers dismisses the remainder, like `floor`:
|
|
||||||
wholesale_items_contained_in_stock = variant.on_hand / solution.factor
|
|
||||||
|
|
||||||
# But maybe we didn't actually order that much:
|
|
||||||
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
|
||||||
|
|
||||||
if deductable_quantity.positive?
|
|
||||||
line.quantity -= deductable_quantity
|
|
||||||
|
|
||||||
retail_stock_change = deductable_quantity * solution.factor
|
|
||||||
variant.on_hand -= retail_stock_change
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def managed_linked_variants(user, order_cycle, distributor)
|
|
||||||
# These permissions may be too complex. Here may be scope to optimise.
|
|
||||||
permissions = OpenFoodNetwork::OrderCyclePermissions.new(user, order_cycle)
|
|
||||||
permissions.visible_variants_for_outgoing_exchanges_to(distributor)
|
|
||||||
.where.associated(:semantic_links)
|
|
||||||
end
|
|
||||||
|
|
||||||
def distributed_linked_variants(order_cycle, distributor)
|
|
||||||
order_cycle.variants_distributed_by(distributor)
|
|
||||||
.where.associated(:semantic_links)
|
|
||||||
end
|
|
||||||
|
|
||||||
def aggregate_final_quantities(order_cycle, line, variant, transformation)
|
|
||||||
# We may want to query all these quantities in one go instead of this n+1.
|
|
||||||
orders = order_cycle.orders.invoiceable
|
|
||||||
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
|
|
||||||
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
|
|
||||||
line.quantity = wholesale_quantity
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -21,7 +21,7 @@ class FdcBackorderer
|
|||||||
|
|
||||||
# Try the new method and fall back to old method.
|
# Try the new method and fall back to old method.
|
||||||
def find_open_order(ofn_order)
|
def find_open_order(ofn_order)
|
||||||
lookup_open_order(ofn_order)
|
lookup_open_order(ofn_order) || find_last_open_order
|
||||||
end
|
end
|
||||||
|
|
||||||
def lookup_open_order(ofn_order)
|
def lookup_open_order(ofn_order)
|
||||||
@@ -36,13 +36,52 @@ class FdcBackorderer
|
|||||||
.map { |id| find_order(id) }
|
.map { |id| find_order(id) }
|
||||||
.compact
|
.compact
|
||||||
# Just in case someone completed the order without updating our database:
|
# Just in case someone completed the order without updating our database:
|
||||||
.select { |o| o.orderStatus == order_status.HELD }
|
.select { |o| o.orderStatus[:path] == "Held" }
|
||||||
.first
|
.first
|
||||||
# The DFC Connector doesn't recognise status values properly yet.
|
# The DFC Connector doesn't recognise status values properly yet.
|
||||||
# So we are overriding the value with something that can be exported.
|
# So we are overriding the value with something that can be exported.
|
||||||
&.tap { |o| o.orderStatus = "dfc-v:Held" }
|
&.tap { |o| o.orderStatus = "dfc-v:Held" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# DEPRECATED
|
||||||
|
#
|
||||||
|
# We now store links to orders we placed. So we don't need to search
|
||||||
|
# through all orders and pick a random open one.
|
||||||
|
# But for compatibility with currently open order cycles that don't have
|
||||||
|
# a stored link yet, we keep this method as well.
|
||||||
|
def find_last_open_order
|
||||||
|
graph = import(urls.orders_url)
|
||||||
|
open_orders = graph&.select do |o|
|
||||||
|
o.semanticType == "dfc-b:Order" && o.orderStatus[:path] == "Held"
|
||||||
|
end
|
||||||
|
|
||||||
|
return if open_orders.blank?
|
||||||
|
|
||||||
|
# If there are multiple open orders, we don't know which one to choose.
|
||||||
|
# We want the order we placed for the same distributor in the same order
|
||||||
|
# cycle before. So here are some assumptions for this to work:
|
||||||
|
#
|
||||||
|
# * We see only orders for our distributor. The endpoint URL contains the
|
||||||
|
# the distributor name and is currently hardcoded.
|
||||||
|
# * There's only one open order cycle at a time. Otherwise we may select
|
||||||
|
# an order of an old order cycle.
|
||||||
|
# * Orders are finalised when the order cycle closes. So _Held_ orders
|
||||||
|
# always belong to an open order cycle.
|
||||||
|
# * We see only our own orders. This assumption is wrong. The Shopify
|
||||||
|
# integration places held orders as well and they are visible to us.
|
||||||
|
#
|
||||||
|
# Unfortunately, the endpoint doesn't tell who placed the order.
|
||||||
|
# TODO: We need to remember the link to the order locally.
|
||||||
|
# Or the API is updated to include the orderer.
|
||||||
|
#
|
||||||
|
# For now, we just guess:
|
||||||
|
open_orders.last.tap do |order|
|
||||||
|
# The DFC Connector doesn't recognise status values properly yet.
|
||||||
|
# So we are overriding the value with something that can be exported.
|
||||||
|
order.orderStatus = "dfc-v:Held"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def find_order(semantic_id)
|
def find_order(semantic_id)
|
||||||
find_subject(import(semantic_id), "dfc-b:Order")
|
find_subject(import(semantic_id), "dfc-b:Order")
|
||||||
end
|
end
|
||||||
@@ -113,7 +152,7 @@ class FdcBackorderer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new?(order)
|
def new?(order)
|
||||||
order.semanticId == urls&.orders_url
|
order.semanticId == urls.orders_url
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_sale_session(order)
|
def build_sale_session(order)
|
||||||
@@ -121,10 +160,4 @@ class FdcBackorderer
|
|||||||
session.semanticId = urls.sale_session_url
|
session.semanticId = urls.sale_session_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def order_status
|
|
||||||
DfcLoader.vocabulary("vocabulary").STATES.ORDERSTATE
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,10 +6,19 @@ class FdcOfferBroker
|
|||||||
Solution = Struct.new(:product, :factor, :offer)
|
Solution = Struct.new(:product, :factor, :offer)
|
||||||
RetailSolution = Struct.new(:retail_product_id, :factor)
|
RetailSolution = Struct.new(:retail_product_id, :factor)
|
||||||
|
|
||||||
attr_reader :catalog
|
def self.load_catalog(user, urls)
|
||||||
|
api = DfcRequest.new(user)
|
||||||
|
catalog_json = api.call(urls.catalog_url)
|
||||||
|
DfcIo.import(catalog_json)
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(catalog)
|
def initialize(user, urls)
|
||||||
@catalog = catalog
|
@user = user
|
||||||
|
@urls = urls
|
||||||
|
end
|
||||||
|
|
||||||
|
def catalog
|
||||||
|
@catalog ||= self.class.load_catalog(@user, @urls)
|
||||||
end
|
end
|
||||||
|
|
||||||
def best_offer(product_id)
|
def best_offer(product_id)
|
||||||
@@ -21,18 +30,19 @@ class FdcOfferBroker
|
|||||||
end
|
end
|
||||||
|
|
||||||
def wholesale_product(product_id)
|
def wholesale_product(product_id)
|
||||||
production_flow = catalog.item("#{product_id}/AsPlannedProductionFlow")
|
production_flow = catalog_item("#{product_id}/AsPlannedProductionFlow")
|
||||||
|
|
||||||
if production_flow
|
if production_flow
|
||||||
production_flow.product
|
wholesale_product_id = production_flow.product
|
||||||
|
catalog_item(wholesale_product_id)
|
||||||
else
|
else
|
||||||
# We didn't find a wholesale variant, falling back to the given product.
|
# We didn't find a wholesale variant, falling back to the given product.
|
||||||
catalog.item(product_id)
|
catalog_item(product_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def contained_quantity(product_id)
|
def contained_quantity(product_id)
|
||||||
consumption_flow = catalog.item("#{product_id}/AsPlannedConsumptionFlow")
|
consumption_flow = catalog_item("#{product_id}/AsPlannedConsumptionFlow")
|
||||||
|
|
||||||
# If we don't find a transformation, we return the original product,
|
# If we don't find a transformation, we return the original product,
|
||||||
# which contains exactly one of itself (identity).
|
# which contains exactly one of itself (identity).
|
||||||
@@ -44,10 +54,10 @@ class FdcOfferBroker
|
|||||||
|
|
||||||
return RetailSolution.new(wholesale_product_id, 1) if production_flow.nil?
|
return RetailSolution.new(wholesale_product_id, 1) if production_flow.nil?
|
||||||
|
|
||||||
consumption_flow = catalog.item(
|
consumption_flow = catalog_item(
|
||||||
production_flow.semanticId.sub("AsPlannedProductionFlow", "AsPlannedConsumptionFlow")
|
production_flow.semanticId.sub("AsPlannedProductionFlow", "AsPlannedConsumptionFlow")
|
||||||
)
|
)
|
||||||
retail_product_id = consumption_flow.product.semanticId
|
retail_product_id = consumption_flow.product
|
||||||
|
|
||||||
contained_quantity = consumption_flow.quantity.value.to_i
|
contained_quantity = consumption_flow.quantity.value.to_i
|
||||||
|
|
||||||
@@ -61,12 +71,19 @@ class FdcOfferBroker
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def catalog_item(id)
|
||||||
|
@catalog_by_id ||= catalog.index_by(&:semanticId)
|
||||||
|
@catalog_by_id[id]
|
||||||
|
end
|
||||||
|
|
||||||
def flow_producing(wholesale_product_id)
|
def flow_producing(wholesale_product_id)
|
||||||
@production_flows_by_product_id ||= production_flows.index_by { |flow| flow.product.semanticId }
|
@production_flows_by_product_id ||= production_flows.index_by(&:product)
|
||||||
@production_flows_by_product_id[wholesale_product_id]
|
@production_flows_by_product_id[wholesale_product_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
def production_flows
|
def production_flows
|
||||||
@production_flows ||= catalog.select_type("dfc-b:AsPlannedProductionFlow")
|
@production_flows ||= catalog.select do |i|
|
||||||
|
i.semanticType == "dfc-b:AsPlannedProductionFlow"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module Orders
|
|||||||
order.send_cancellation_email = @send_cancellation_email
|
order.send_cancellation_email = @send_cancellation_email
|
||||||
order.restock_items = @restock_items
|
order.restock_items = @restock_items
|
||||||
order.cancel
|
order.cancel
|
||||||
end.tap { |orders| AmendBackorderJob.schedule_bulk_update_for(orders) }
|
end
|
||||||
# rubocop:enable Rails/FindEach
|
# rubocop:enable Rails/FindEach
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ module Orders
|
|||||||
return unless order.cancel
|
return unless order.cancel
|
||||||
|
|
||||||
Spree::OrderMailer.cancel_email_for_shop(order).deliver_later
|
Spree::OrderMailer.cancel_email_for_shop(order).deliver_later
|
||||||
AmendBackorderJob.perform_later(order)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ module PermittedAttributes
|
|||||||
:preferred_invoice_order_by_supplier,
|
:preferred_invoice_order_by_supplier,
|
||||||
:preferred_product_low_stock_display,
|
:preferred_product_low_stock_display,
|
||||||
:hide_ofn_navigation, :white_label_logo, :white_label_logo_link,
|
:hide_ofn_navigation, :white_label_logo, :white_label_logo_link,
|
||||||
:hide_groups_tab, :external_billing_id,
|
:hide_groups_tab
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ class PlaceProxyOrder
|
|||||||
send_placement_email
|
send_placement_email
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
summarizer.record_and_log_error(:processing, order, e.message)
|
summarizer.record_and_log_error(:processing, order, e.message)
|
||||||
Alert.raise_with_record(e, order)
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata :order, :order, order
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -54,7 +56,9 @@ class PlaceProxyOrder
|
|||||||
|
|
||||||
true
|
true
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Alert.raise(e, { proxy_order: { subscription:, proxy_order: } })
|
Bugsnag.notify(e) do |payload|
|
||||||
|
payload.add_metadata(:proxy_order, { subscription:, proxy_order: })
|
||||||
|
end
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ module Sets
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notify_bugsnag(error, product, variant, variant_attributes)
|
def notify_bugsnag(error, product, variant, variant_attributes)
|
||||||
Alert.raise(error) do |report|
|
Bugsnag.notify(error) do |report|
|
||||||
report.add_metadata( :product_set,
|
report.add_metadata( :product_set,
|
||||||
{ product: product.attributes, variant_attributes:,
|
{ product: product.attributes, variant_attributes:,
|
||||||
variant: variant.attributes } )
|
variant: variant.attributes } )
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ module VariantUnits
|
|||||||
def option_value_value_unit_scaled
|
def option_value_value_unit_scaled
|
||||||
unit_scale, unit_name = scale_for_unit_value
|
unit_scale, unit_name = scale_for_unit_value
|
||||||
|
|
||||||
value = (@nameable.unit_value.to_d / unit_scale).round(2)
|
value = (@nameable.unit_value / unit_scale).to_d.truncate(2)
|
||||||
|
|
||||||
[value, unit_name]
|
[value, unit_name]
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ module VariantUnits
|
|||||||
def unit_value_attributes
|
def unit_value_attributes
|
||||||
units = { unit_presentation: option_value_name }
|
units = { unit_presentation: option_value_name }
|
||||||
units.merge!(variant_unit:) if has_attribute?(:variant_unit)
|
units.merge!(variant_unit:) if has_attribute?(:variant_unit)
|
||||||
units.merge!(variant_unit_name: '') if reset_variant_unit_name?
|
|
||||||
units
|
units
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -80,9 +79,5 @@ module VariantUnits
|
|||||||
|
|
||||||
VariantUnits::OptionValueNamer.new(self).name
|
VariantUnits::OptionValueNamer.new(self).name
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_variant_unit_name?
|
|
||||||
has_attribute?(:variant_unit_name) && has_attribute?(:variant_unit) && variant_unit != 'items'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user