mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-04 02:31:33 +00:00
Compare commits
106 Commits
7caaeffe80
...
v4.4.35
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42520216aa | ||
|
|
ce24e6ecd6 | ||
|
|
4c1268b3ce | ||
|
|
3455ffd507 | ||
|
|
26f3b5603d | ||
|
|
50acf2f484 | ||
|
|
220e459da2 | ||
|
|
4ada3edc4e | ||
|
|
579965c62c | ||
|
|
e85e606667 | ||
|
|
b4b8e99c7b | ||
|
|
2d8cd2b1a5 | ||
|
|
1e826e8308 | ||
|
|
d81fc44597 | ||
|
|
cb47624702 | ||
|
|
ccdd428b57 | ||
|
|
eb7e65a707 | ||
|
|
25e3f30f97 | ||
|
|
214f7ec23c | ||
|
|
b679a20f23 | ||
|
|
eb2213bd10 | ||
|
|
b07bf9989a | ||
|
|
13f387e0a4 | ||
|
|
fc24a830a5 | ||
|
|
dd86222391 | ||
|
|
794f92d9f5 | ||
|
|
e061dbb86b | ||
|
|
526069dbb3 | ||
|
|
acb53a6ddc | ||
|
|
b623ecab26 | ||
|
|
476825251d | ||
|
|
0d17230dd2 | ||
|
|
73eeaaabc2 | ||
|
|
cad1140b18 | ||
|
|
67a5aa6877 | ||
|
|
c4fa936f15 | ||
|
|
ad7d19a0be | ||
|
|
69df56ae76 | ||
|
|
63549b3dca | ||
|
|
8a84e0084f | ||
|
|
6dc0988933 | ||
|
|
e2a53b57d4 | ||
|
|
3c3f65c271 | ||
|
|
bdcb0856af | ||
|
|
778ed46d50 | ||
|
|
9d919938f3 | ||
|
|
8858ed86ac | ||
|
|
8efc215a14 | ||
|
|
d2d2db8489 | ||
|
|
3af7fa7521 | ||
|
|
b5c47b099e | ||
|
|
d47d3eba8f | ||
|
|
2e101c5fe6 | ||
|
|
1c09b5d16c | ||
|
|
d6da52929f | ||
|
|
30e8f9eb28 | ||
|
|
7abe455899 | ||
|
|
931ee2f9d2 | ||
|
|
477336c660 | ||
|
|
96ccea3691 | ||
|
|
c6a83588fe | ||
|
|
fcef8e8d7d | ||
|
|
696559200f | ||
|
|
c497b37452 | ||
|
|
acc52cc45f | ||
|
|
38b832cec2 | ||
|
|
261cb2d81b | ||
|
|
f59c43b8e9 | ||
|
|
29dad44f9f | ||
|
|
c4a8bf2490 | ||
|
|
82c444b8d8 | ||
|
|
d7fca66433 | ||
|
|
43d13253e7 | ||
|
|
84551068ee | ||
|
|
163c45d301 | ||
|
|
63a1931ce8 | ||
|
|
98457b2fff | ||
|
|
e072823071 | ||
|
|
d253effc29 | ||
|
|
331894017a | ||
|
|
1674d8ab5c | ||
|
|
85a47e61fd | ||
|
|
a4b7a8f95d | ||
|
|
462c763cd1 | ||
|
|
4f77ad40a3 | ||
|
|
a33eb80f56 | ||
|
|
fb8c86a9a7 | ||
|
|
b53be15fda | ||
|
|
f755aff23c | ||
|
|
ff52a66e75 | ||
|
|
2cac8471fc | ||
|
|
5653d542f6 | ||
|
|
7f3953882d | ||
|
|
f126b8b316 | ||
|
|
7849f30f46 | ||
|
|
b82496b8a1 | ||
|
|
bd6b0ddbf3 | ||
|
|
4a423e3275 | ||
|
|
1472749da8 | ||
|
|
27d1a9ee09 | ||
|
|
45f4a06263 | ||
|
|
8370a5fed0 | ||
|
|
64b42b1284 | ||
|
|
f582bffbc5 | ||
|
|
b669b804c4 | ||
|
|
6e9089ad47 |
@@ -14,3 +14,4 @@ SITE_URL="test.host"
|
||||
|
||||
OPENID_APP_ID="test-provider"
|
||||
OPENID_APP_SECRET="12345"
|
||||
OPENID_REFRESH_TOKEN="dummy-refresh-token"
|
||||
|
||||
@@ -19,3 +19,6 @@ Capybara/NegationMatcher:
|
||||
RSpec/ExpectChange:
|
||||
Enabled: true
|
||||
EnforcedStyle: block
|
||||
|
||||
RSpec/NotToNot:
|
||||
Enabled: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 1400 --no-auto-gen-timestamp`
|
||||
# using RuboCop version 1.60.2.
|
||||
# using RuboCop version 1.62.1.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
@@ -113,6 +113,7 @@ Lint/SelfAssignment:
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AutoCorrect.
|
||||
Lint/UselessMethodDefinition:
|
||||
Exclude:
|
||||
- 'app/models/spree/gateway.rb'
|
||||
@@ -141,7 +142,7 @@ Metrics/AbcSize:
|
||||
- 'lib/open_food_network/order_cycle_permissions.rb'
|
||||
- 'lib/spree/core/controller_helpers/order.rb'
|
||||
- 'lib/tasks/enterprises.rake'
|
||||
- 'spec/services/order_checkout_restart_spec.rb'
|
||||
- 'spec/services/orders/checkout_restart_service_spec.rb'
|
||||
|
||||
# Offense count: 9
|
||||
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
|
||||
@@ -200,8 +201,8 @@ Metrics/ClassLength:
|
||||
- 'app/serializers/api/cached_enterprise_serializer.rb'
|
||||
- 'app/serializers/api/enterprise_shopfront_serializer.rb'
|
||||
- 'app/services/cart_service.rb'
|
||||
- 'app/services/order_cycle_form.rb'
|
||||
- 'app/services/order_syncer.rb'
|
||||
- 'app/services/orders/sync_service.rb'
|
||||
- 'app/services/order_cycles/form_service.rb'
|
||||
- 'engines/order_management/app/services/order_management/order/updater.rb'
|
||||
- 'lib/open_food_network/enterprise_fee_calculator.rb'
|
||||
- 'lib/open_food_network/order_cycle_form_applicator.rb'
|
||||
@@ -389,21 +390,6 @@ Naming/VariableNumber:
|
||||
- 'spec/models/spree/tax_rate_spec.rb'
|
||||
- 'spec/requests/api/orders_spec.rb'
|
||||
|
||||
# Offense count: 11
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
# AllowedMethods: order, limit, select, lock
|
||||
Rails/FindEach:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/order_cycles_controller.rb'
|
||||
- 'app/jobs/subscription_confirm_job.rb'
|
||||
- 'app/services/orders_bulk_cancel_service.rb'
|
||||
- 'app/services/products_renderer.rb'
|
||||
- 'lib/tasks/data.rake'
|
||||
- 'lib/tasks/subscriptions/debug.rake'
|
||||
- 'spec/system/admin/bulk_order_management_spec.rb'
|
||||
- 'spec/system/admin/enterprise_relationships_spec.rb'
|
||||
|
||||
# Offense count: 11
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
@@ -602,14 +588,6 @@ Rails/SkipsModelValidations:
|
||||
- 'app/models/variant_override.rb'
|
||||
- 'spec/models/spree/line_item_spec.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Rails/SquishedSQLHeredocs:
|
||||
Exclude:
|
||||
- 'app/queries/customers_with_balance.rb'
|
||||
- 'app/queries/outstanding_balance.rb'
|
||||
- 'spec/queries/outstanding_balance_spec.rb'
|
||||
|
||||
# Offense count: 7
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
@@ -618,8 +596,8 @@ Rails/TimeZone:
|
||||
Exclude:
|
||||
- 'app/models/spree/gateway/pay_pal_express.rb'
|
||||
- 'spec/controllers/spree/credit_cards_controller_spec.rb'
|
||||
- 'spec/services/customer_order_cancellation_spec.rb'
|
||||
- 'spec/services/order_cycle_webhook_service_spec.rb'
|
||||
- 'spec/services/orders/customer_cancellation_service_spec.rb'
|
||||
- 'spec/services/order_cycles/webhook_service_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: TransactionMethods.
|
||||
@@ -656,7 +634,7 @@ Rails/UnusedRenderContent:
|
||||
- 'app/controllers/api/v0/taxons_controller.rb'
|
||||
- 'app/controllers/api/v0/variants_controller.rb'
|
||||
|
||||
# Offense count: 55
|
||||
# Offense count: 54
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Rails/WhereEquals:
|
||||
Exclude:
|
||||
@@ -678,7 +656,6 @@ Rails/WhereEquals:
|
||||
- 'app/models/spree/shipping_method.rb'
|
||||
- 'app/models/spree/variant.rb'
|
||||
- 'app/models/subscription.rb'
|
||||
- 'app/queries/payments_requiring_action.rb'
|
||||
- 'app/serializers/api/enterprise_shopfront_serializer.rb'
|
||||
- 'app/serializers/api/order_serializer.rb'
|
||||
- 'lib/open_food_network/enterprise_fee_calculator.rb'
|
||||
@@ -883,7 +860,7 @@ Style/PreferredHashMethods:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v0/shipments_controller.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: Methods.
|
||||
Style/RedundantArgument:
|
||||
@@ -898,7 +875,7 @@ Style/RedundantAssignment:
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AllowComments.
|
||||
# Configuration parameters: AutoCorrect, AllowComments.
|
||||
Style/RedundantInitialize:
|
||||
Exclude:
|
||||
- 'spec/models/spree/gateway_spec.rb'
|
||||
@@ -910,6 +887,12 @@ Style/RedundantInterpolation:
|
||||
- 'lib/tasks/karma.rake'
|
||||
- 'spec/base_spec_helper.rb'
|
||||
|
||||
# Offense count: 8
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
Style/RedundantLineContinuation:
|
||||
Exclude:
|
||||
- 'lib/reporting/reports/enterprise_fee_summary/scope.rb'
|
||||
|
||||
# Offense count: 19
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
|
||||
@@ -6,28 +6,9 @@ This is a general guide to setting up an Open Food Network **development environ
|
||||
|
||||
Head to our wiki on [Learning Rails](https://github.com/openfoodfoundation/openfoodnetwork/wiki/Learning-Rails) to find some good starting points.
|
||||
|
||||
### Requirements
|
||||
|
||||
The fastest way to make it work locally is to use Docker, you only need to setup git, see the [Docker setup guide](docker/README.md).
|
||||
Otherwise, for a local setup you will need:
|
||||
* Ruby and bundler (check current Ruby version in [.ruby-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.ruby-version) file)
|
||||
- To manage versions, it's recommended to use [rbenv](https://github.com/rbenv/rbenv) or [RVM](https://rvm.io/)
|
||||
* Node and yarn (check current Node version in [.node-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.node-version) file)
|
||||
- [nodevn](https://github.com/nodenv/nodenv) is recommended.
|
||||
* PostgreSQL database
|
||||
* Redis (for background jobs)
|
||||
* Chrome (for testing)
|
||||
|
||||
The following guides will provide OS-specific step-by-step instructions to get these requirements installed:
|
||||
- [Ubuntu Setup Guide][ubuntu]
|
||||
- [Debian Setup Guide][debian]
|
||||
- [OSX Setup Guide][osx]
|
||||
|
||||
For those new to Rails, the following tutorial will help get you up to speed with configuring a [Rails environment](http://guides.rubyonrails.org/getting_started.html).
|
||||
|
||||
### Get it
|
||||
|
||||
So you have set up your local environment according to the requirements listed above. If you're planning on contributing code to the project (which we [LOVE](CONTRIBUTING.md)), it is a good idea to begin by forking this repo using the `Fork` button in the top-right corner of this screen. You should then be able to use `git clone` to copy your fork onto your local machine:
|
||||
If you're planning on contributing code to the project (which we [LOVE](CONTRIBUTING.md)), it is a good idea to begin by forking this repo using the `Fork` button in the top-right corner of this screen. You should then be able to use `git clone` to copy your fork onto your local machine:
|
||||
|
||||
git clone git@github.com:YOUR_GITHUB_USERNAME_HERE/openfoodnetwork.git
|
||||
|
||||
@@ -43,6 +24,27 @@ Fetch the latest version of `master` from `upstream` (ie. the main repo):
|
||||
|
||||
git fetch upstream master
|
||||
|
||||
### Installation
|
||||
|
||||
This project needs specific ruby/bundler versions as well as node/yarn specific versions. For a local setup you will need:
|
||||
|
||||
* Install or change your Ruby version according to the one specified at [.ruby-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.ruby-version) file.
|
||||
- To manage versions, it's recommended to use [rbenv](https://github.com/rbenv/rbenv).
|
||||
* Install [nodenv](https://github.com/nodenv/nodenv) to ensure the correct [.node-version](https://github.com/openfoodfoundation/openfoodnetwork/blob/master/.node-version) is used.
|
||||
- [nodevn](https://github.com/nodenv/nodenv) is recommended as a node version manager.
|
||||
* PostgreSQL database
|
||||
* Redis (for background jobs)
|
||||
* Chrome (for testing)
|
||||
|
||||
The following guides will provide OS-specific step-by-step instructions to get these requirements installed:
|
||||
- [Ubuntu Setup Guide][ubuntu]
|
||||
- [Debian Setup Guide][debian]
|
||||
- [OSX Setup Guide][osx]
|
||||
|
||||
For those new to Rails, the following tutorial will help get you up to speed with configuring a [Rails environment](http://guides.rubyonrails.org/getting_started.html).
|
||||
|
||||
Another way to make it work locally would be using Docker. See the [Docker setup guide](docker/README.md).
|
||||
|
||||
### Get it running
|
||||
|
||||
First, you need to create the database user the app will use by manually typing the following in your terminal:
|
||||
@@ -53,7 +55,8 @@ sudo --login --user=postgres psql -c "CREATE USER ofn WITH SUPERUSER CREATEDB PA
|
||||
|
||||
This will create the "ofn" user as superuser and allowing it to create databases. If this command fails, check the [troubleshooting section](#creating-the-database) for an alternative.
|
||||
|
||||
Next, it is _strongly recommended_ to run the setup script.
|
||||
Next, it is _strongly recommended_ to run the setup script:
|
||||
|
||||
```sh
|
||||
./script/setup
|
||||
```
|
||||
|
||||
69
Gemfile.lock
69
Gemfile.lock
@@ -109,7 +109,7 @@ GEM
|
||||
activerecord (7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activerecord-import (1.5.1)
|
||||
activerecord-import (1.6.0)
|
||||
activerecord (>= 4.2)
|
||||
activerecord-postgresql-adapter (0.0.1)
|
||||
pg
|
||||
@@ -156,16 +156,16 @@ GEM
|
||||
awesome_nested_set (3.6.0)
|
||||
activerecord (>= 4.0.0, < 7.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.883.0)
|
||||
aws-sdk-core (3.191.0)
|
||||
aws-partitions (1.899.0)
|
||||
aws-sdk-core (3.191.4)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.77.0)
|
||||
aws-sdk-kms (1.78.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.143.0)
|
||||
aws-sdk-s3 (1.146.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
@@ -256,7 +256,7 @@ GEM
|
||||
devise (>= 4.9.0)
|
||||
devise-token_authenticatable (1.1.0)
|
||||
devise (>= 4.0.0, < 5.0.0)
|
||||
diff-lcs (1.5.0)
|
||||
diff-lcs (1.5.1)
|
||||
digest (3.1.1)
|
||||
docile (1.4.0)
|
||||
dotenv (3.1.0)
|
||||
@@ -335,9 +335,8 @@ GEM
|
||||
hashie (5.0.0)
|
||||
highline (2.0.3)
|
||||
htmlentities (4.3.4)
|
||||
i18n (1.14.3)
|
||||
i18n (1.14.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
racc (~> 1.7)
|
||||
i18n-js (3.9.2)
|
||||
i18n (>= 0.6.6)
|
||||
image_processing (1.12.2)
|
||||
@@ -415,7 +414,7 @@ GEM
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.22.2)
|
||||
minitest (5.22.3)
|
||||
monetize (1.13.0)
|
||||
money (~> 6.12)
|
||||
money (6.16.0)
|
||||
@@ -436,7 +435,7 @@ GEM
|
||||
net-protocol
|
||||
newrelic_rpm (9.7.1)
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.2)
|
||||
nokogiri (1.16.3)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oauth2 (1.4.11)
|
||||
@@ -555,7 +554,7 @@ GEM
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails-i18n (7.0.8)
|
||||
rails-i18n (7.0.9)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
rails_safe_tasks (1.0.0)
|
||||
@@ -606,32 +605,32 @@ GEM
|
||||
roo (2.10.1)
|
||||
nokogiri (~> 1)
|
||||
rubyzip (>= 1.3.0, < 3.0.0)
|
||||
rspec (3.12.0)
|
||||
rspec-core (~> 3.12.0)
|
||||
rspec-expectations (~> 3.12.0)
|
||||
rspec-mocks (~> 3.12.0)
|
||||
rspec-core (3.12.2)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-expectations (3.12.3)
|
||||
rspec (3.13.0)
|
||||
rspec-core (~> 3.13.0)
|
||||
rspec-expectations (~> 3.13.0)
|
||||
rspec-mocks (~> 3.13.0)
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-mocks (3.12.6)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-rails (6.1.1)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.2)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-core (~> 3.12)
|
||||
rspec-expectations (~> 3.12)
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-retry (0.6.2)
|
||||
rspec-core (> 3.3)
|
||||
rspec-sql (0.0.1)
|
||||
activesupport
|
||||
rspec
|
||||
rspec-support (3.12.1)
|
||||
rspec-support (3.13.1)
|
||||
rswag (2.13.0)
|
||||
rswag-api (= 2.13.0)
|
||||
rswag-specs (= 2.13.0)
|
||||
@@ -647,7 +646,7 @@ GEM
|
||||
rswag-ui (2.13.0)
|
||||
actionpack (>= 3.1, < 7.2)
|
||||
railties (>= 3.1, < 7.2)
|
||||
rubocop (1.60.2)
|
||||
rubocop (1.62.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
@@ -655,20 +654,20 @@ GEM
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.30.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.31.1)
|
||||
rubocop-ast (1.31.2)
|
||||
parser (>= 3.3.0.4)
|
||||
rubocop-capybara (2.20.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.25.1)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-rails (2.23.1)
|
||||
rubocop-rails (2.24.0)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-ast (>= 1.30.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (2.27.1)
|
||||
rubocop (~> 1.40)
|
||||
rubocop-capybara (~> 2.17)
|
||||
@@ -694,7 +693,7 @@ GEM
|
||||
tilt (>= 1.1, < 3)
|
||||
sd_notify (0.1.1)
|
||||
semantic_range (3.0.0)
|
||||
shoulda-matchers (6.1.0)
|
||||
shoulda-matchers (6.2.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.2.2)
|
||||
concurrent-ruby (< 2)
|
||||
@@ -747,14 +746,14 @@ GEM
|
||||
stimulus_reflex (>= 3.3.0)
|
||||
stringex (2.8.6)
|
||||
stringio (3.1.0)
|
||||
stripe (10.10.0)
|
||||
stripe (10.12.0)
|
||||
swd (2.0.3)
|
||||
activesupport (>= 3)
|
||||
attr_required (>= 0.0.5)
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
temple (0.8.2)
|
||||
thor (1.3.0)
|
||||
thor (1.3.1)
|
||||
thread-local (1.1.0)
|
||||
tilt (2.3.0)
|
||||
timecop (0.9.8)
|
||||
|
||||
50
app/controllers/admin/dfc_product_imports_controller.rb
Normal file
50
app/controllers/admin/dfc_product_imports_controller.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "private_address_check"
|
||||
require "private_address_check/tcpsocket_ext"
|
||||
|
||||
module Admin
|
||||
class DfcProductImportsController < Spree::Admin::BaseController
|
||||
# Define model class for `can?` permissions:
|
||||
def model_class
|
||||
self.class
|
||||
end
|
||||
|
||||
def index
|
||||
# The plan:
|
||||
#
|
||||
# * Fetch DFC catalog as JSON from URL.
|
||||
enterprise = OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
.managed_product_enterprises.is_primary_producer
|
||||
.find(params.require(:enterprise_id))
|
||||
|
||||
catalog_url = params.require(:catalog_url)
|
||||
|
||||
json_catalog = DfcRequest.new(spree_current_user).get(catalog_url)
|
||||
graph = DfcIo.import(json_catalog)
|
||||
|
||||
# * First step: import all products for given enterprise.
|
||||
# * Second step: render table and let user decide which ones to import.
|
||||
imported = graph.map do |subject|
|
||||
import_product(subject, enterprise)
|
||||
end
|
||||
|
||||
@count = imported.compact.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Most of this code is the same as in the DfcProvider::SuppliedProductsController.
|
||||
def import_product(subject, enterprise)
|
||||
return unless subject.is_a? DataFoodConsortium::Connector::SuppliedProduct
|
||||
|
||||
variant = SuppliedProductBuilder.import_variant(subject, enterprise)
|
||||
product = variant.product
|
||||
|
||||
product.save! if product.new_record?
|
||||
variant.save! if variant.new_record?
|
||||
|
||||
variant
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -45,7 +45,8 @@ module Admin
|
||||
end
|
||||
|
||||
def create
|
||||
@order_cycle_form = OrderCycleForm.new(@order_cycle, order_cycle_params, spree_current_user)
|
||||
@order_cycle_form = OrderCycles::FormService.new(@order_cycle, order_cycle_params,
|
||||
spree_current_user)
|
||||
|
||||
if @order_cycle_form.save
|
||||
flash[:success] = t('.success')
|
||||
@@ -61,7 +62,8 @@ module Admin
|
||||
end
|
||||
|
||||
def update
|
||||
@order_cycle_form = OrderCycleForm.new(@order_cycle, order_cycle_params, spree_current_user)
|
||||
@order_cycle_form = OrderCycles::FormService.new(@order_cycle, order_cycle_params,
|
||||
spree_current_user)
|
||||
|
||||
if @order_cycle_form.save
|
||||
update_nil_subscription_line_items_price_estimate(@order_cycle)
|
||||
@@ -98,7 +100,7 @@ module Admin
|
||||
|
||||
def update_nil_subscription_line_items_price_estimate(order_cycle)
|
||||
order_cycle.schedules.each do |schedule|
|
||||
Subscription.where(schedule_id: schedule.id).each do |subscription|
|
||||
Subscription.where(schedule_id: schedule.id).find_each do |subscription|
|
||||
shop = Enterprise.managed_by(spree_current_user).find_by(id: subscription.shop_id)
|
||||
fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(shop, order_cycle)
|
||||
subscription.subscription_line_items.nil_price_estimate.each do |line_item|
|
||||
|
||||
@@ -10,6 +10,8 @@ module Admin
|
||||
@product_categories = Spree::Taxon.order('name ASC').pluck(:name).uniq
|
||||
@tax_categories = Spree::TaxCategory.order('name ASC').pluck(:name)
|
||||
@shipping_categories = Spree::ShippingCategory.order('name ASC').pluck(:name)
|
||||
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).
|
||||
managed_product_enterprises.is_primary_producer.by_name.to_a
|
||||
end
|
||||
|
||||
def import
|
||||
|
||||
@@ -101,7 +101,8 @@ module Api
|
||||
end
|
||||
|
||||
def distributed_products
|
||||
OrderCycleDistributedProducts.new(distributor, order_cycle, customer).products_relation
|
||||
OrderCycles::DistributedProductsService.new(distributor, order_cycle,
|
||||
customer).products_relation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +47,7 @@ module Api
|
||||
def capture
|
||||
authorize! :admin, order
|
||||
|
||||
payment_capture = OrderCaptureService.new(order)
|
||||
payment_capture = Orders::CaptureService.new(order)
|
||||
|
||||
if payment_capture.call
|
||||
render json: order.reload, serializer: Api::Admin::OrderSerializer, status: :ok
|
||||
|
||||
@@ -21,7 +21,7 @@ module Api
|
||||
@shipment.refresh_rates
|
||||
@shipment.save!
|
||||
|
||||
OrderWorkflow.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!
|
||||
|
||||
@@ -85,6 +85,8 @@ module Api
|
||||
@order.contents.remove(variant, quantity, @shipment, restock_item)
|
||||
@shipment.reload if @shipment.persisted?
|
||||
|
||||
@order.recreate_all_fees!
|
||||
|
||||
render json: @shipment, serializer: Api::ShipmentSerializer, status: :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class CheckoutController < BaseController
|
||||
def advance_order_state
|
||||
return if @order.complete?
|
||||
|
||||
OrderWorkflow.new(@order).advance_checkout(raw_params.slice(:shipping_method_id))
|
||||
Orders::WorkflowService.new(@order).advance_checkout(raw_params.slice(:shipping_method_id))
|
||||
end
|
||||
|
||||
def order_params
|
||||
|
||||
@@ -65,7 +65,7 @@ module CheckoutCallbacks
|
||||
|
||||
def valid_order_line_items?
|
||||
@order.insufficient_stock_lines.empty? &&
|
||||
OrderCycleDistributedVariants.new(@order.order_cycle, @order.distributor).
|
||||
OrderCycles::DistributedVariantsService.new(@order.order_cycle, @order.distributor).
|
||||
distributes_order_variants?(@order)
|
||||
end
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ module OrderCompletion
|
||||
return redirect_to order_failed_route(step: 'payment')
|
||||
end
|
||||
|
||||
if OrderWorkflow.new(@order).next && @order.complete?
|
||||
if Orders::WorkflowService.new(@order).next && @order.complete?
|
||||
processing_succeeded
|
||||
redirect_to order_completion_route
|
||||
else
|
||||
|
||||
@@ -6,7 +6,7 @@ module OrderStockCheck
|
||||
|
||||
def valid_order_line_items?
|
||||
@order.insufficient_stock_lines.empty? &&
|
||||
OrderCycleDistributedVariants.new(@order.order_cycle, @order.distributor).
|
||||
OrderCycles::DistributedVariantsService.new(@order.order_cycle, @order.distributor).
|
||||
distributes_order_variants?(@order)
|
||||
end
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class EnterprisesController < BaseController
|
||||
order = current_order(true)
|
||||
|
||||
# reset_distributor must be called before any call to current_customer or current_distributor
|
||||
order_cart_reset = OrderCartReset.new(order, params[:id])
|
||||
order_cart_reset = Orders::CartResetService.new(order, params[:id])
|
||||
order_cart_reset.reset_distributor
|
||||
order_cart_reset.reset_other!(spree_current_user, current_customer)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
|
||||
@@ -79,7 +79,7 @@ module PaymentGateways
|
||||
end
|
||||
|
||||
def last_payment
|
||||
@last_payment ||= OrderPaymentFinder.new(@order).last_payment
|
||||
@last_payment ||= Orders::FindPaymentService.new(@order).last_payment
|
||||
end
|
||||
|
||||
def cancel_incomplete_payments
|
||||
|
||||
@@ -26,7 +26,7 @@ module Spree
|
||||
def warn_invalid_order_cycles
|
||||
return if flash[:notice].present?
|
||||
|
||||
warning = OrderCycleWarning.new(spree_current_user).call
|
||||
warning = OrderCycles::WarningService.new(spree_current_user).call
|
||||
flash[:notice] = warning if warning.present?
|
||||
end
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ module Spree
|
||||
def generate
|
||||
@order = Order.find_by(number: params[:order_id])
|
||||
authorize! :invoice, @order
|
||||
OrderInvoiceGenerator.new(@order).generate_or_update_latest_invoice
|
||||
::Orders::GenerateInvoiceService.new(@order).generate_or_update_latest_invoice
|
||||
redirect_back(fallback_location: spree.admin_dashboard_path)
|
||||
end
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ module Spree
|
||||
end
|
||||
|
||||
refresh_shipment_rates
|
||||
OrderWorkflow.new(@order).advance_to_payment
|
||||
::Orders::WorkflowService.new(@order).advance_to_payment
|
||||
|
||||
flash[:success] = Spree.t('customer_details_updated')
|
||||
redirect_to spree.admin_order_customer_path(@order)
|
||||
|
||||
@@ -50,7 +50,7 @@ module Spree
|
||||
return redirect_to spree.edit_admin_order_path(@order)
|
||||
end
|
||||
|
||||
OrderWorkflow.new(@order).advance_to_payment
|
||||
::Orders::WorkflowService.new(@order).advance_to_payment
|
||||
|
||||
if @order.complete?
|
||||
redirect_to spree.edit_admin_order_path(@order)
|
||||
@@ -104,7 +104,7 @@ module Spree
|
||||
@order = if params[:invoice_id].present?
|
||||
@order.invoices.find(params[:invoice_id]).presenter
|
||||
else
|
||||
OrderInvoiceGenerator.new(@order).generate_or_update_latest_invoice
|
||||
::Orders::GenerateInvoiceService.new(@order).generate_or_update_latest_invoice
|
||||
@order.invoices.first.presenter
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ module Spree
|
||||
return
|
||||
end
|
||||
|
||||
OrderWorkflow.new(@order).complete! unless @order.completed?
|
||||
::Orders::WorkflowService.new(@order).complete! unless @order.completed?
|
||||
|
||||
authorize_stripe_sca_payment
|
||||
@payment.process_offline!
|
||||
|
||||
@@ -42,7 +42,7 @@ module Spree
|
||||
# Patching to redirect to shop if order is empty
|
||||
def edit
|
||||
@insufficient_stock_lines = @order.insufficient_stock_lines
|
||||
@unavailable_order_variants = OrderCycleDistributedVariants.
|
||||
@unavailable_order_variants = OrderCycles::DistributedVariantsService.
|
||||
new(current_order_cycle, current_distributor).unavailable_order_variants(@order)
|
||||
|
||||
if @order.line_items.empty?
|
||||
@@ -102,7 +102,7 @@ module Spree
|
||||
@order = Spree::Order.find_by!(number: params[:id])
|
||||
authorize! :cancel, @order
|
||||
|
||||
if CustomerOrderCancellation.new(@order).call
|
||||
if Orders::CustomerCancellationService.new(@order).call
|
||||
flash[:success] = I18n.t(:orders_your_order_has_been_cancelled)
|
||||
else
|
||||
flash[:error] = I18n.t(:orders_could_not_cancel)
|
||||
|
||||
@@ -59,7 +59,7 @@ module CheckoutHelper
|
||||
end
|
||||
|
||||
def display_checkout_taxes_hash(order)
|
||||
totals = OrderTaxAdjustmentsFetcher.new(order).totals
|
||||
totals = Orders::FetchTaxAdjustmentsService.new(order).totals
|
||||
|
||||
totals.map do |tax_rate, tax_amount|
|
||||
{
|
||||
|
||||
@@ -12,11 +12,11 @@ module EnterprisesHelper
|
||||
end
|
||||
|
||||
def available_shipping_methods
|
||||
OrderAvailableShippingMethods.new(current_order, current_customer).to_a
|
||||
Orders::AvailableShippingMethodsService.new(current_order, current_customer).to_a
|
||||
end
|
||||
|
||||
def available_payment_methods
|
||||
OrderAvailablePaymentMethods.new(current_order, current_customer).to_a
|
||||
Orders::AvailablePaymentMethodsService.new(current_order, current_customer).to_a
|
||||
end
|
||||
|
||||
def managed_enterprises
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module OrderHelper
|
||||
def last_payment_method(order)
|
||||
OrderPaymentFinder.new(order).last_payment&.payment_method
|
||||
Orders::FindPaymentService.new(order).last_payment&.payment_method
|
||||
end
|
||||
|
||||
def outstanding_balance_label(order)
|
||||
@@ -16,6 +16,6 @@ module OrderHelper
|
||||
end
|
||||
|
||||
def order_comparator(order)
|
||||
OrderInvoiceComparator.new(order)
|
||||
Orders::CompareInvoiceService.new(order)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ class BulkInvoiceJob < ApplicationJob
|
||||
|
||||
def generate_invoice(order)
|
||||
renderer_data = if OpenFoodNetwork::FeatureToggle.enabled?(:invoices, current_user)
|
||||
OrderInvoiceGenerator.new(order).generate_or_update_latest_invoice
|
||||
Orders::GenerateInvoiceService.new(order).generate_or_update_latest_invoice
|
||||
order.invoices.first.presenter
|
||||
else
|
||||
order
|
||||
|
||||
@@ -5,7 +5,7 @@ class OrderCycleOpenedJob < ApplicationJob
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
recently_opened_order_cycles.find_each do |order_cycle|
|
||||
OrderCycleWebhookService.create_webhook_job(order_cycle, 'order_cycle.opened')
|
||||
OrderCycles::WebhookService.create_webhook_job(order_cycle, 'order_cycle.opened')
|
||||
end
|
||||
mark_as_opened(recently_opened_order_cycles)
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ class SubscriptionConfirmJob < ApplicationJob
|
||||
unconfirmed_proxy_orders.update_all(confirmed_at: Time.zone.now)
|
||||
|
||||
# Confirm these proxy orders
|
||||
ProxyOrder.where(id: unconfirmed_proxy_orders_ids).each do |proxy_order|
|
||||
ProxyOrder.where(id: unconfirmed_proxy_orders_ids).find_each do |proxy_order|
|
||||
JobLogger.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
|
||||
confirm_order!(proxy_order.order)
|
||||
end
|
||||
|
||||
@@ -52,7 +52,8 @@ module Spree
|
||||
find_user(options[:current_user_id])
|
||||
end
|
||||
renderer_data = if OpenFoodNetwork::FeatureToggle.enabled?(:invoices, current_user)
|
||||
OrderInvoiceGenerator.new(@order).generate_or_update_latest_invoice
|
||||
::Orders::GenerateInvoiceService
|
||||
.new(@order).generate_or_update_latest_invoice
|
||||
@order.invoices.first.presenter
|
||||
else
|
||||
@order
|
||||
|
||||
@@ -15,7 +15,7 @@ module OrderValidations
|
||||
|
||||
# Check that line_items in the current order are available from a newly selected distribution
|
||||
def products_available_from_new_distribution
|
||||
return if OrderCycleDistributedVariants.new(order_cycle, distributor)
|
||||
return if OrderCycles::DistributedVariantsService.new(order_cycle, distributor)
|
||||
.distributes_order_variants?(self)
|
||||
|
||||
errors.add(:base, I18n.t(:spree_order_availability_error))
|
||||
|
||||
@@ -175,7 +175,7 @@ class OrderCycle < ApplicationRecord
|
||||
end
|
||||
|
||||
def clone!
|
||||
OrderCycleClone.new(self).create
|
||||
OrderCycles::CloneService.new(self).create
|
||||
end
|
||||
|
||||
def variants
|
||||
|
||||
@@ -58,7 +58,7 @@ class ProxyOrder < ApplicationRecord
|
||||
def initialise_order!
|
||||
return order if order.present?
|
||||
|
||||
factory = OrderFactory.new(order_attrs, skip_stock_check: true)
|
||||
factory = Orders::FactoryService.new(order_attrs, skip_stock_check: true)
|
||||
self.order = factory.create
|
||||
save!
|
||||
order
|
||||
|
||||
8
app/models/semantic_link.rb
Normal file
8
app/models/semantic_link.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Link a Spree::Variant to an external DFC SuppliedProduct.
|
||||
class SemanticLink < ApplicationRecord
|
||||
belongs_to :variant, class_name: "Spree::Variant"
|
||||
|
||||
validates :semantic_id, presence: true
|
||||
end
|
||||
@@ -239,6 +239,8 @@ module Spree
|
||||
can [:admin, :index, :guide, :import, :save, :save_data,
|
||||
:validate_data, :reset_absent_products], ProductImport::ProductImporter
|
||||
|
||||
can [:admin, :index], ::Admin::DfcProductImportsController
|
||||
|
||||
# Reports page
|
||||
can [:admin, :index, :show], ::Admin::ReportsController
|
||||
can [:admin, :show, :customers, :orders_and_distributors, :group_buys, :payments,
|
||||
|
||||
@@ -652,7 +652,7 @@ module Spree
|
||||
end
|
||||
|
||||
def fee_handler
|
||||
@fee_handler ||= OrderFeesHandler.new(self)
|
||||
@fee_handler ||= Orders::HandleFeesService.new(self)
|
||||
end
|
||||
|
||||
def clear_legacy_taxes!
|
||||
@@ -701,7 +701,7 @@ module Spree
|
||||
end
|
||||
|
||||
def adjustments_fetcher
|
||||
@adjustments_fetcher ||= OrderAdjustmentsFetcher.new(self)
|
||||
@adjustments_fetcher ||= Orders::FetchAdjustmentsService.new(self)
|
||||
end
|
||||
|
||||
def skip_payment_for_subscription?
|
||||
|
||||
@@ -48,7 +48,7 @@ module Spree
|
||||
end
|
||||
|
||||
def capture_and_complete_order!
|
||||
OrderWorkflow.new(order).complete!
|
||||
Orders::WorkflowService.new(order).complete!
|
||||
capture!
|
||||
end
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ module Spree
|
||||
has_many :exchanges, through: :exchange_variants
|
||||
has_many :variant_overrides, dependent: :destroy
|
||||
has_many :inventory_items, dependent: :destroy
|
||||
has_many :semantic_links, dependent: :delete_all
|
||||
|
||||
localize_number :price, :weight
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ module Admin
|
||||
before_reflex :authorize_order, only: [:capture, :ship]
|
||||
|
||||
def capture
|
||||
payment_capture = OrderCaptureService.new(@order)
|
||||
payment_capture = Orders::CaptureService.new(@order)
|
||||
|
||||
if payment_capture.call
|
||||
cable_ready.replace(selector: dom_id(@order),
|
||||
@@ -32,13 +32,20 @@ module Admin
|
||||
end
|
||||
|
||||
def bulk_invoice(params)
|
||||
visible_orders = editable_orders.where(id: params[:bulk_ids]).filter(&:invoiceable?)
|
||||
if Spree::Config.enterprise_number_required_on_invoices? &&
|
||||
!all_distributors_can_invoice?(visible_orders)
|
||||
render_business_number_required_error(visible_orders)
|
||||
return
|
||||
end
|
||||
|
||||
cable_ready.append(
|
||||
selector: "#orders-index",
|
||||
html: render(partial: "spree/admin/orders/bulk/invoice_modal")
|
||||
).broadcast
|
||||
|
||||
BulkInvoiceJob.perform_later(
|
||||
params[:bulk_ids],
|
||||
visible_orders.pluck(:id),
|
||||
"tmp/invoices/#{Time.zone.now.to_i}-#{SecureRandom.hex(2)}.pdf",
|
||||
channel: SessionChannel.for_request(request),
|
||||
current_user_id: current_user.id
|
||||
@@ -48,7 +55,7 @@ module Admin
|
||||
end
|
||||
|
||||
def cancel_orders(params)
|
||||
cancelled_orders = OrdersBulkCancelService.new(params, current_user).call
|
||||
cancelled_orders = Orders::BulkCancelService.new(params, current_user).call
|
||||
|
||||
cable_ready.dispatch_event(name: "modal:close")
|
||||
|
||||
@@ -106,5 +113,19 @@ module Admin
|
||||
def set_param_for_controller
|
||||
params[:id] = @order.number
|
||||
end
|
||||
|
||||
def all_distributors_can_invoice?(orders)
|
||||
distributor_ids = orders.map(&:distributor_id)
|
||||
Enterprise.where(id: distributor_ids, abn: nil).empty?
|
||||
end
|
||||
|
||||
def render_business_number_required_error(orders)
|
||||
distributor_ids = orders.map(&:distributor_id)
|
||||
distributor_names = Enterprise.where(id: distributor_ids, abn: nil).pluck(:name)
|
||||
|
||||
flash[:error] = I18n.t(:must_have_valid_business_number,
|
||||
enterprise_name: distributor_names.join(", "))
|
||||
morph_admin_flashes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,11 +116,11 @@ class ProductsReflex < ApplicationReflex
|
||||
producer_options: producers, producer_id: @producer_id,
|
||||
category_options: categories, category_id: @category_id,
|
||||
flashes: flash })
|
||||
).broadcast
|
||||
)
|
||||
|
||||
cable_ready.replace_state(
|
||||
url: current_url,
|
||||
).broadcast_later
|
||||
)
|
||||
|
||||
morph :nothing
|
||||
end
|
||||
@@ -133,12 +133,11 @@ class ProductsReflex < ApplicationReflex
|
||||
cable_ready.replace(
|
||||
selector: "#products-form",
|
||||
html: render(partial: "admin/products_v3/table", locals:)
|
||||
).broadcast
|
||||
)
|
||||
morph :nothing
|
||||
|
||||
# dunno why this doesn't work.
|
||||
# morph "#products-form", render(partial: "admin/products_v3/table",
|
||||
# locals: { products: products })
|
||||
# dunno why this doesn't work. The HTML stops after the first `<col>` element, wtf?!
|
||||
# morph "#products-form", render(partial: "admin/products_v3/table", locals:)
|
||||
end
|
||||
|
||||
def producers
|
||||
|
||||
@@ -46,6 +46,7 @@ class CapQuantity
|
||||
end
|
||||
|
||||
def available_variants_for
|
||||
OrderCycleDistributedVariants.new(order.order_cycle, order.distributor).available_variants
|
||||
OrderCycles::DistributedVariantsService.new(order.order_cycle,
|
||||
order.distributor).available_variants
|
||||
end
|
||||
end
|
||||
|
||||
@@ -154,7 +154,7 @@ class CartService
|
||||
end
|
||||
|
||||
def check_variant_available_under_distribution(variant)
|
||||
return true if OrderCycleDistributedVariants.new(@order_cycle, @distributor)
|
||||
return true if OrderCycles::DistributedVariantsService.new(@order_cycle, @distributor)
|
||||
.available_variants.include? variant
|
||||
|
||||
errors.add(:base, I18n.t(:spree_order_populator_availability_error))
|
||||
|
||||
@@ -14,7 +14,7 @@ module Checkout
|
||||
|
||||
def failure
|
||||
@order.updater.shipping_address_from_distributor
|
||||
OrderCheckoutRestart.new(@order).call
|
||||
Orders::CheckoutRestartService.new(@order).call
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Creates an order cycle for the provided enterprise and selecting all the
|
||||
# variants specified for both incoming and outgoing exchanges
|
||||
class CreateOrderCycle
|
||||
# Constructor
|
||||
#
|
||||
# @param enterprise [Enterprise]
|
||||
# @param variants [Array<Spree::Variant>]
|
||||
def initialize(enterprise, variants)
|
||||
@enterprise = enterprise
|
||||
@variants = variants
|
||||
end
|
||||
|
||||
# Creates the order cycle
|
||||
def call
|
||||
incoming_exchange.order_cycle = order_cycle
|
||||
incoming_exchange.variants << variants
|
||||
|
||||
outgoing_exchange.order_cycle = order_cycle
|
||||
outgoing_exchange.variants << variants
|
||||
|
||||
order_cycle.exchanges << incoming_exchange
|
||||
order_cycle.exchanges << outgoing_exchange
|
||||
|
||||
order_cycle.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :enterprise, :variants
|
||||
|
||||
# Builds an order cycle for the next month, starting now
|
||||
#
|
||||
# @return [OrderCycle]
|
||||
def order_cycle
|
||||
@order_cycle ||= OrderCycle.new(
|
||||
coordinator_id: enterprise.id,
|
||||
name: 'Monthly order cycle',
|
||||
orders_open_at: Time.zone.now,
|
||||
orders_close_at: 1.month.from_now
|
||||
)
|
||||
end
|
||||
|
||||
# Builds an exchange with the enterprise both as sender and receiver
|
||||
#
|
||||
# @return [Exchange]
|
||||
def incoming_exchange
|
||||
@incoming_exchange ||= Exchange.new(
|
||||
sender_id: enterprise.id,
|
||||
receiver_id: enterprise.id,
|
||||
incoming: true
|
||||
)
|
||||
end
|
||||
|
||||
# Builds an exchange with the enterprise both as sender and receiver
|
||||
#
|
||||
# @return [Exchange]
|
||||
def outgoing_exchange
|
||||
@outgoing_exchange ||= Exchange.new(
|
||||
sender_id: enterprise.id,
|
||||
receiver_id: enterprise.id,
|
||||
pickup_time: '8 am',
|
||||
incoming: false
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CustomerOrderCancellation
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def call
|
||||
return unless order.cancel
|
||||
|
||||
Spree::OrderMailer.cancel_email_for_shop(order).deliver_later
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
end
|
||||
@@ -1,79 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This class allows orders with eager-loaded adjustment objects to calculate various adjustment
|
||||
# types without triggering additional queries.
|
||||
#
|
||||
# For example; `order.adjustments.shipping.sum(:amount)` would normally trigger a new query
|
||||
# regardless of whether or not adjustments have been preloaded, as `#shipping` is an adjustment
|
||||
# scope, eg; `scope :shipping, where(originator_type: 'Spree::ShippingMethod')`.
|
||||
#
|
||||
# Here the adjustment scopes are moved to a shared module, and `adjustments.loaded?` is used to
|
||||
# check if the objects have already been fetched and initialized. If they have, `order.adjustments`
|
||||
# will be an Array, and we can select the required objects without hitting the database. If not, it
|
||||
# will fetch the adjustments via their scopes as normal.
|
||||
|
||||
class OrderAdjustmentsFetcher
|
||||
include AdjustmentScopes
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def admin_and_handling_total
|
||||
admin_and_handling_fees.map(&:amount).sum
|
||||
end
|
||||
|
||||
def payment_fee
|
||||
sum_adjustments "payment_fee"
|
||||
end
|
||||
|
||||
def ship_total
|
||||
sum_adjustments "shipping"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
|
||||
def adjustments
|
||||
order.all_adjustments
|
||||
end
|
||||
|
||||
def adjustments_eager_loaded?
|
||||
adjustments.loaded?
|
||||
end
|
||||
|
||||
def sum_adjustments(scope)
|
||||
collect_adjustments(scope).map(&:amount).sum
|
||||
end
|
||||
|
||||
def collect_adjustments(scope)
|
||||
if adjustments_eager_loaded?
|
||||
adjustment_scope = public_send("#{scope}_scope")
|
||||
|
||||
# Adjustments are already loaded here, this block is using `Array#select`
|
||||
adjustments.select do |adjustment|
|
||||
match_by_scope(adjustment, adjustment_scope) && match_by_scope(adjustment, eligible_scope)
|
||||
end
|
||||
else
|
||||
adjustments.where(nil).eligible.public_send scope
|
||||
end
|
||||
end
|
||||
|
||||
def admin_and_handling_fees
|
||||
if adjustments_eager_loaded?
|
||||
adjustments.select do |adjustment|
|
||||
match_by_scope(adjustment, eligible_scope) &&
|
||||
adjustment.originator_type == 'EnterpriseFee' &&
|
||||
adjustment.adjustable_type != 'Spree::LineItem'
|
||||
end
|
||||
else
|
||||
adjustments.eligible.
|
||||
where("originator_type = ? AND adjustable_type != ?", 'EnterpriseFee', 'Spree::LineItem')
|
||||
end
|
||||
end
|
||||
|
||||
def match_by_scope(adjustment, scope)
|
||||
adjustment.public_send(scope.keys.first) == scope.values.first
|
||||
end
|
||||
end
|
||||
@@ -1,43 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/tag_rule_applicator'
|
||||
|
||||
class OrderAvailablePaymentMethods
|
||||
attr_reader :order, :customer
|
||||
|
||||
delegate :distributor,
|
||||
:order_cycle,
|
||||
to: :order
|
||||
|
||||
def initialize(order, customer = nil)
|
||||
@order, @customer = order, customer
|
||||
end
|
||||
|
||||
def to_a
|
||||
return [] if distributor.blank?
|
||||
|
||||
payment_methods = payment_methods_before_tag_rules_applied
|
||||
|
||||
applicator = OpenFoodNetwork::TagRuleApplicator.new(distributor,
|
||||
"FilterPaymentMethods", customer&.tag_list)
|
||||
applicator.filter(payment_methods)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def payment_methods_before_tag_rules_applied
|
||||
if order_cycle.nil? || order_cycle.simple?
|
||||
distributor.payment_methods
|
||||
else
|
||||
distributor.payment_methods.where(
|
||||
id: available_distributor_payment_methods_ids
|
||||
)
|
||||
end.available.select(&:configured?).uniq
|
||||
end
|
||||
|
||||
def available_distributor_payment_methods_ids
|
||||
order_cycle.distributor_payment_methods
|
||||
.where(distributor_id: distributor.id)
|
||||
.select(:payment_method_id)
|
||||
end
|
||||
end
|
||||
@@ -1,56 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/tag_rule_applicator'
|
||||
|
||||
class OrderAvailableShippingMethods
|
||||
attr_reader :order, :customer
|
||||
|
||||
delegate :distributor, :order_cycle, to: :order
|
||||
|
||||
def initialize(order, customer = nil)
|
||||
@order, @customer = order, customer
|
||||
end
|
||||
|
||||
def to_a
|
||||
return [] if distributor.blank?
|
||||
|
||||
filter_by_category(tag_rules.filter(shipping_methods))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_by_category(methods)
|
||||
return methods unless OpenFoodNetwork::FeatureToggle.enabled?(:match_shipping_categories,
|
||||
distributor&.owner)
|
||||
|
||||
required_category_ids = order.variants.pluck(:shipping_category_id).to_set
|
||||
return methods if required_category_ids.empty?
|
||||
|
||||
methods.select do |method|
|
||||
provided_category_ids = method.shipping_categories.pluck(:id).to_set
|
||||
required_category_ids.subset?(provided_category_ids)
|
||||
end
|
||||
end
|
||||
|
||||
def shipping_methods
|
||||
if order_cycle.nil? || order_cycle.simple?
|
||||
distributor.shipping_methods
|
||||
else
|
||||
distributor.shipping_methods.where(
|
||||
id: available_distributor_shipping_methods_ids
|
||||
)
|
||||
end.frontend.to_a.uniq
|
||||
end
|
||||
|
||||
def available_distributor_shipping_methods_ids
|
||||
order_cycle.distributor_shipping_methods
|
||||
.where(distributor_id: distributor.id)
|
||||
.select(:shipping_method_id)
|
||||
end
|
||||
|
||||
def tag_rules
|
||||
OpenFoodNetwork::TagRuleApplicator.new(
|
||||
distributor, "FilterShippingMethods", customer&.tag_list
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Use `authorize! :admin order` before calling this service
|
||||
|
||||
class OrderCaptureService
|
||||
attr_reader :gateway_error
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
@gateway_error = nil
|
||||
end
|
||||
|
||||
def call
|
||||
return false unless @order.payment_required?
|
||||
return false unless (pending_payment = @order.pending_payments.first)
|
||||
|
||||
pending_payment.capture!
|
||||
rescue Spree::Core::GatewayError => e
|
||||
@gateway_error = e
|
||||
false
|
||||
end
|
||||
end
|
||||
@@ -1,56 +0,0 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
# Resets an order by verifying it's state and fixing any issues
|
||||
class OrderCartReset
|
||||
def initialize(order, distributor_id)
|
||||
@order = order
|
||||
@distributor ||= Enterprise.is_distributor.find_by(permalink: distributor_id) ||
|
||||
Enterprise.is_distributor.find(distributor_id)
|
||||
end
|
||||
|
||||
def reset_distributor
|
||||
if order.distributor && order.distributor != distributor
|
||||
order.empty!
|
||||
order.set_order_cycle! nil
|
||||
end
|
||||
order.distributor = distributor
|
||||
end
|
||||
|
||||
def reset_other!(current_user, current_customer)
|
||||
reset_user_and_customer(current_user)
|
||||
reset_order_cycle(current_customer)
|
||||
order.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order, :distributor, :current_user
|
||||
|
||||
def reset_user_and_customer(current_user)
|
||||
return unless current_user
|
||||
|
||||
order.associate_user!(current_user) if order.user.blank? || order.email.blank?
|
||||
end
|
||||
|
||||
def reset_order_cycle(current_customer)
|
||||
listed_order_cycles = Shop::OrderCyclesList.active_for(distributor, current_customer)
|
||||
|
||||
if order_cycle_not_listed?(order.order_cycle, listed_order_cycles)
|
||||
order.order_cycle = nil
|
||||
order.empty!
|
||||
end
|
||||
|
||||
select_default_order_cycle(order, listed_order_cycles)
|
||||
end
|
||||
|
||||
def order_cycle_not_listed?(order_cycle, listed_order_cycles)
|
||||
order_cycle.present? && !listed_order_cycles.include?(order_cycle)
|
||||
end
|
||||
|
||||
# If no OC is selected and there is only one in the list of OCs, selects it
|
||||
def select_default_order_cycle(order, listed_order_cycles)
|
||||
return unless order.order_cycle.blank? && listed_order_cycles.size == 1
|
||||
|
||||
order.order_cycle = listed_order_cycles.first
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Resets the passed order to cart state while clearing associated payments and shipments
|
||||
class OrderCheckoutRestart
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def call
|
||||
return if order.cart?
|
||||
|
||||
reset_state_to_cart
|
||||
clear_shipments
|
||||
clear_payments
|
||||
|
||||
order.reload.update_order!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
|
||||
def reset_state_to_cart
|
||||
order.restart_checkout!
|
||||
end
|
||||
|
||||
def clear_shipments
|
||||
order.shipments.with_state(:pending).destroy_all
|
||||
end
|
||||
|
||||
def clear_payments
|
||||
order.payments.with_state(:checkout).destroy_all
|
||||
end
|
||||
end
|
||||
@@ -1,43 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrderCycleClone
|
||||
def initialize(order_cycle)
|
||||
@original_order_cycle = order_cycle
|
||||
end
|
||||
|
||||
def create
|
||||
oc = @original_order_cycle.dup
|
||||
oc.name = I18n.t("models.order_cycle.cloned_order_cycle_name", order_cycle: oc.name)
|
||||
oc.orders_open_at = oc.orders_close_at = oc.mails_sent = oc.processed_at = nil
|
||||
oc.coordinator_fee_ids = @original_order_cycle.coordinator_fee_ids
|
||||
oc.preferred_product_selection_from_coordinator_inventory_only =
|
||||
@original_order_cycle.preferred_product_selection_from_coordinator_inventory_only
|
||||
oc.schedule_ids = @original_order_cycle.schedule_ids
|
||||
oc.save!
|
||||
@original_order_cycle.exchanges.each { |e| e.clone!(oc) }
|
||||
oc.selected_distributor_payment_method_ids = selected_distributor_payment_method_ids
|
||||
oc.selected_distributor_shipping_method_ids = selected_distributor_shipping_method_ids
|
||||
sync_subscriptions
|
||||
oc.reload
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def selected_distributor_payment_method_ids
|
||||
@original_order_cycle.attachable_distributor_payment_methods.map(&:id) &
|
||||
@original_order_cycle.selected_distributor_payment_method_ids
|
||||
end
|
||||
|
||||
def selected_distributor_shipping_method_ids
|
||||
@original_order_cycle.attachable_distributor_shipping_methods.map(&:id) &
|
||||
@original_order_cycle.selected_distributor_shipping_method_ids
|
||||
end
|
||||
|
||||
def sync_subscriptions
|
||||
return unless @original_order_cycle.schedule_ids.any?
|
||||
|
||||
OrderManagement::Subscriptions::ProxyOrderSyncer.new(
|
||||
Subscription.where(schedule_id: @original_order_cycle.schedule_ids)
|
||||
).sync!
|
||||
end
|
||||
end
|
||||
@@ -1,83 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Returns a (paginatable) AR object for the products or variants in stock for a given shop and OC.
|
||||
# The stock-checking includes on_demand and stock level overrides from variant_overrides.
|
||||
class OrderCycleDistributedProducts
|
||||
def initialize(distributor, order_cycle, customer)
|
||||
@distributor = distributor
|
||||
@order_cycle = order_cycle
|
||||
@customer = customer
|
||||
end
|
||||
|
||||
def products_relation
|
||||
Spree::Product.where(id: stocked_products).group("spree_products.id")
|
||||
end
|
||||
|
||||
def variants_relation
|
||||
order_cycle.
|
||||
variants_distributed_by(distributor).
|
||||
merge(stocked_variants_and_overrides).
|
||||
select("DISTINCT spree_variants.*")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :distributor, :order_cycle, :customer
|
||||
|
||||
def stocked_products
|
||||
order_cycle.
|
||||
variants_distributed_by(distributor).
|
||||
merge(stocked_variants_and_overrides).
|
||||
select("DISTINCT spree_variants.product_id")
|
||||
end
|
||||
|
||||
def stocked_variants_and_overrides
|
||||
stocked_variants = Spree::Variant.
|
||||
joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.variant_id = spree_variants.id
|
||||
AND variant_overrides.hub_id = #{distributor.id}").
|
||||
joins(:stock_items).
|
||||
where(query_stock_with_overrides)
|
||||
|
||||
ProductTagRulesFilterer.new(distributor, customer, stocked_variants).call
|
||||
end
|
||||
|
||||
def query_stock_with_overrides
|
||||
"( #{variant_not_overriden} AND ( #{variant_on_demand} OR #{variant_in_stock} ) )
|
||||
OR ( #{variant_overriden} AND ( #{override_on_demand} OR #{override_in_stock} ) )
|
||||
OR ( #{variant_overriden} AND ( #{override_on_demand_null} AND #{variant_on_demand} ) )
|
||||
OR ( #{variant_overriden} AND ( #{override_on_demand_null}
|
||||
AND #{variant_not_on_demand} AND #{variant_in_stock} ) )"
|
||||
end
|
||||
|
||||
def variant_not_overriden
|
||||
"variant_overrides.id IS NULL"
|
||||
end
|
||||
|
||||
def variant_overriden
|
||||
"variant_overrides.id IS NOT NULL"
|
||||
end
|
||||
|
||||
def variant_in_stock
|
||||
"spree_stock_items.count_on_hand > 0"
|
||||
end
|
||||
|
||||
def variant_on_demand
|
||||
"spree_stock_items.backorderable IS TRUE"
|
||||
end
|
||||
|
||||
def variant_not_on_demand
|
||||
"spree_stock_items.backorderable IS FALSE"
|
||||
end
|
||||
|
||||
def override_on_demand
|
||||
"variant_overrides.on_demand IS TRUE"
|
||||
end
|
||||
|
||||
def override_in_stock
|
||||
"variant_overrides.count_on_hand > 0"
|
||||
end
|
||||
|
||||
def override_on_demand_null
|
||||
"variant_overrides.on_demand IS NULL"
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrderCycleDistributedVariants
|
||||
def initialize(order_cycle, distributor)
|
||||
@order_cycle = order_cycle
|
||||
@distributor = distributor
|
||||
end
|
||||
|
||||
def distributes_order_variants?(order)
|
||||
unavailable_order_variants(order).empty?
|
||||
end
|
||||
|
||||
def unavailable_order_variants(order)
|
||||
order.line_item_variants - available_variants
|
||||
end
|
||||
|
||||
def available_variants
|
||||
return [] unless @order_cycle
|
||||
|
||||
@order_cycle.variants_distributed_by(@distributor)
|
||||
end
|
||||
end
|
||||
@@ -1,230 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/permissions'
|
||||
require 'open_food_network/order_cycle_form_applicator'
|
||||
|
||||
class OrderCycleForm
|
||||
def initialize(order_cycle, order_cycle_params, user)
|
||||
@order_cycle = order_cycle
|
||||
@order_cycle_params = order_cycle_params
|
||||
@specified_params = order_cycle_params.keys
|
||||
@user = user
|
||||
@permissions = OpenFoodNetwork::Permissions.new(user)
|
||||
@schedule_ids = order_cycle_params.delete(:schedule_ids)
|
||||
@selected_distributor_payment_method_ids = order_cycle_params.delete(
|
||||
:selected_distributor_payment_method_ids
|
||||
)
|
||||
@selected_distributor_shipping_method_ids = order_cycle_params.delete(
|
||||
:selected_distributor_shipping_method_ids
|
||||
)
|
||||
end
|
||||
|
||||
def save
|
||||
schedule_ids = build_schedule_ids
|
||||
order_cycle.assign_attributes(order_cycle_params)
|
||||
return false unless order_cycle.valid?
|
||||
|
||||
order_cycle.transaction do
|
||||
order_cycle.save!
|
||||
order_cycle.schedule_ids = schedule_ids if parameter_specified?(:schedule_ids)
|
||||
order_cycle.save!
|
||||
apply_exchange_changes
|
||||
if can_update_selected_payment_or_shipping_methods?
|
||||
attach_selected_distributor_payment_methods
|
||||
attach_selected_distributor_shipping_methods
|
||||
end
|
||||
sync_subscriptions
|
||||
true
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
add_exception_to_order_cycle_errors(e)
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_accessor :order_cycle, :order_cycle_params, :user, :permissions
|
||||
|
||||
def add_exception_to_order_cycle_errors(exception)
|
||||
error = exception.message.split(":").last.strip
|
||||
order_cycle.errors.add(:base, error) if order_cycle.errors.to_a.exclude?(error)
|
||||
end
|
||||
|
||||
def apply_exchange_changes
|
||||
return if exchanges_unchanged?
|
||||
|
||||
OpenFoodNetwork::OrderCycleFormApplicator.new(order_cycle, user).go!
|
||||
|
||||
# reload so outgoing exchanges are up-to-date for shipping/payment method validations
|
||||
order_cycle.reload
|
||||
end
|
||||
|
||||
def attach_selected_distributor_payment_methods
|
||||
return if @selected_distributor_payment_method_ids.nil?
|
||||
|
||||
if distributor_only?
|
||||
payment_method_ids = order_cycle.selected_distributor_payment_method_ids
|
||||
payment_method_ids -= user_distributor_payment_method_ids
|
||||
payment_method_ids += user_only_selected_distributor_payment_method_ids
|
||||
order_cycle.selected_distributor_payment_method_ids = payment_method_ids
|
||||
else
|
||||
order_cycle.selected_distributor_payment_method_ids = selected_distributor_payment_method_ids
|
||||
end
|
||||
order_cycle.save!
|
||||
end
|
||||
|
||||
def attach_selected_distributor_shipping_methods
|
||||
return if @selected_distributor_shipping_method_ids.nil?
|
||||
|
||||
if distributor_only?
|
||||
# A distributor can only update methods associated with their own
|
||||
# enterprise, so we load all previously selected methods, and replace
|
||||
# only the distributor's methods with their selection (not touching other
|
||||
# distributor's methods).
|
||||
shipping_method_ids = order_cycle.selected_distributor_shipping_method_ids
|
||||
shipping_method_ids -= user_distributor_shipping_method_ids
|
||||
shipping_method_ids += user_only_selected_distributor_shipping_method_ids
|
||||
order_cycle.selected_distributor_shipping_method_ids = shipping_method_ids
|
||||
else
|
||||
order_cycle.selected_distributor_shipping_method_ids =
|
||||
selected_distributor_shipping_method_ids
|
||||
end
|
||||
|
||||
order_cycle.save!
|
||||
end
|
||||
|
||||
def attachable_distributor_payment_method_ids
|
||||
@attachable_distributor_payment_method_ids ||=
|
||||
order_cycle.attachable_distributor_payment_methods.map(&:id)
|
||||
end
|
||||
|
||||
def attachable_distributor_shipping_method_ids
|
||||
@attachable_distributor_shipping_method_ids ||=
|
||||
order_cycle.attachable_distributor_shipping_methods.map(&:id)
|
||||
end
|
||||
|
||||
def exchanges_unchanged?
|
||||
[:incoming_exchanges, :outgoing_exchanges].all? do |direction|
|
||||
order_cycle_params[direction].nil?
|
||||
end
|
||||
end
|
||||
|
||||
def selected_distributor_payment_method_ids
|
||||
@selected_distributor_payment_method_ids = (
|
||||
attachable_distributor_payment_method_ids &
|
||||
@selected_distributor_payment_method_ids.compact_blank.map(&:to_i)
|
||||
)
|
||||
|
||||
if attachable_distributor_payment_method_ids.sort ==
|
||||
@selected_distributor_payment_method_ids.sort
|
||||
@selected_distributor_payment_method_ids = []
|
||||
end
|
||||
|
||||
@selected_distributor_payment_method_ids
|
||||
end
|
||||
|
||||
def user_only_selected_distributor_payment_method_ids
|
||||
user_distributor_payment_method_ids.intersection(selected_distributor_payment_method_ids)
|
||||
end
|
||||
|
||||
def selected_distributor_shipping_method_ids
|
||||
@selected_distributor_shipping_method_ids = (
|
||||
attachable_distributor_shipping_method_ids &
|
||||
@selected_distributor_shipping_method_ids.compact_blank.map(&:to_i)
|
||||
)
|
||||
|
||||
if attachable_distributor_shipping_method_ids.sort ==
|
||||
@selected_distributor_shipping_method_ids.sort
|
||||
@selected_distributor_shipping_method_ids = []
|
||||
end
|
||||
|
||||
@selected_distributor_shipping_method_ids
|
||||
end
|
||||
|
||||
def user_only_selected_distributor_shipping_method_ids
|
||||
user_distributor_shipping_method_ids.intersection(selected_distributor_shipping_method_ids)
|
||||
end
|
||||
|
||||
def build_schedule_ids
|
||||
return unless parameter_specified?(:schedule_ids)
|
||||
|
||||
result = existing_schedule_ids
|
||||
result |= (requested_schedule_ids & permitted_schedule_ids) # Add permitted and requested
|
||||
# Remove permitted but not requested
|
||||
result -= ((result & permitted_schedule_ids) - requested_schedule_ids)
|
||||
result
|
||||
end
|
||||
|
||||
def sync_subscriptions
|
||||
return unless parameter_specified?(:schedule_ids)
|
||||
return unless schedule_sync_required?
|
||||
|
||||
OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
|
||||
end
|
||||
|
||||
def schedule_sync_required?
|
||||
removed_schedule_ids.any? || new_schedule_ids.any?
|
||||
end
|
||||
|
||||
def subscriptions_to_sync
|
||||
Subscription.where(schedule_id: removed_schedule_ids + new_schedule_ids)
|
||||
end
|
||||
|
||||
def requested_schedule_ids
|
||||
@schedule_ids.map(&:to_i)
|
||||
end
|
||||
|
||||
def parameter_specified?(key)
|
||||
@specified_params.map(&:to_s).include?(key.to_s)
|
||||
end
|
||||
|
||||
def permitted_schedule_ids
|
||||
Schedule.where(id: requested_schedule_ids | existing_schedule_ids)
|
||||
.merge(permissions.editable_schedules).pluck(:id)
|
||||
end
|
||||
|
||||
def existing_schedule_ids
|
||||
@existing_schedule_ids ||= order_cycle.persisted? ? order_cycle.schedule_ids : []
|
||||
end
|
||||
|
||||
def removed_schedule_ids
|
||||
existing_schedule_ids - order_cycle.schedule_ids
|
||||
end
|
||||
|
||||
def new_schedule_ids
|
||||
@order_cycle.schedule_ids - existing_schedule_ids
|
||||
end
|
||||
|
||||
def can_update_selected_payment_or_shipping_methods?
|
||||
@user.admin? || coordinator? || distributor?
|
||||
end
|
||||
|
||||
def coordinator?
|
||||
@user.enterprises.include?(@order_cycle.coordinator)
|
||||
end
|
||||
|
||||
def distributor?
|
||||
!user_distributors_ids.empty?
|
||||
end
|
||||
|
||||
def distributor_only?
|
||||
distributor? && !@user.admin? && !coordinator?
|
||||
end
|
||||
|
||||
def user_distributors_ids
|
||||
@user_distributors_ids ||= @user.enterprises.pluck(:id)
|
||||
.intersection(@order_cycle.distributors.pluck(:id))
|
||||
end
|
||||
|
||||
def user_distributor_payment_method_ids
|
||||
@user_distributor_payment_method_ids ||=
|
||||
DistributorPaymentMethod.where(distributor_id: user_distributors_ids)
|
||||
.pluck(:id)
|
||||
end
|
||||
|
||||
def user_distributor_shipping_method_ids
|
||||
@user_distributor_shipping_method_ids ||=
|
||||
DistributorShippingMethod.where(distributor_id: user_distributors_ids)
|
||||
.pluck(:id)
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrderCycleWarning
|
||||
def initialize(current_user)
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def call
|
||||
distributors = active_distributors_not_ready_for_checkout
|
||||
|
||||
return if distributors.empty?
|
||||
|
||||
active_distributors_not_ready_for_checkout_message(distributors)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user
|
||||
|
||||
def active_distributors_not_ready_for_checkout
|
||||
ocs = OrderCycle.managed_by(current_user).active
|
||||
distributors = ocs.includes(:distributors).map(&:distributors).flatten.uniq
|
||||
Enterprise.where(id: distributors.map(&:id)).not_ready_for_checkout
|
||||
end
|
||||
|
||||
def active_distributors_not_ready_for_checkout_message(distributors)
|
||||
distributor_names = distributors.map(&:name).join ', '
|
||||
|
||||
if distributors.count > 1
|
||||
I18n.t(:active_distributors_not_ready_for_checkout_message_plural,
|
||||
distributor_names:)
|
||||
else
|
||||
I18n.t(:active_distributors_not_ready_for_checkout_message_singular,
|
||||
distributor_names:)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,21 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Create a webhook payload for an order cycle event.
|
||||
# The payload will be delivered asynchronously.
|
||||
class OrderCycleWebhookService
|
||||
def self.create_webhook_job(order_cycle, event)
|
||||
webhook_payload = order_cycle
|
||||
.slice(:id, :name, :orders_open_at, :orders_close_at, :coordinator_id)
|
||||
.merge(coordinator_name: order_cycle.coordinator.name)
|
||||
|
||||
# Endpoints for coordinator owner
|
||||
webhook_endpoints = order_cycle.coordinator.owner.webhook_endpoints
|
||||
|
||||
# Plus unique endpoints for distributor owners (ignore duplicates)
|
||||
webhook_endpoints |= order_cycle.distributors.map(&:owner).flat_map(&:webhook_endpoints)
|
||||
|
||||
webhook_endpoints.each do |endpoint|
|
||||
WebhookDeliveryJob.perform_later(endpoint.url, event, webhook_payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
45
app/services/order_cycles/clone_service.rb
Normal file
45
app/services/order_cycles/clone_service.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderCycles
|
||||
class CloneService
|
||||
def initialize(order_cycle)
|
||||
@original_order_cycle = order_cycle
|
||||
end
|
||||
|
||||
def create
|
||||
oc = @original_order_cycle.dup
|
||||
oc.name = I18n.t("models.order_cycle.cloned_order_cycle_name", order_cycle: oc.name)
|
||||
oc.orders_open_at = oc.orders_close_at = oc.mails_sent = oc.processed_at = nil
|
||||
oc.coordinator_fee_ids = @original_order_cycle.coordinator_fee_ids
|
||||
oc.preferred_product_selection_from_coordinator_inventory_only =
|
||||
@original_order_cycle.preferred_product_selection_from_coordinator_inventory_only
|
||||
oc.schedule_ids = @original_order_cycle.schedule_ids
|
||||
oc.save!
|
||||
@original_order_cycle.exchanges.each { |e| e.clone!(oc) }
|
||||
oc.selected_distributor_payment_method_ids = selected_distributor_payment_method_ids
|
||||
oc.selected_distributor_shipping_method_ids = selected_distributor_shipping_method_ids
|
||||
sync_subscriptions
|
||||
oc.reload
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def selected_distributor_payment_method_ids
|
||||
@original_order_cycle.attachable_distributor_payment_methods.map(&:id) &
|
||||
@original_order_cycle.selected_distributor_payment_method_ids
|
||||
end
|
||||
|
||||
def selected_distributor_shipping_method_ids
|
||||
@original_order_cycle.attachable_distributor_shipping_methods.map(&:id) &
|
||||
@original_order_cycle.selected_distributor_shipping_method_ids
|
||||
end
|
||||
|
||||
def sync_subscriptions
|
||||
return unless @original_order_cycle.schedule_ids.any?
|
||||
|
||||
OrderManagement::Subscriptions::ProxyOrderSyncer.new(
|
||||
Subscription.where(schedule_id: @original_order_cycle.schedule_ids)
|
||||
).sync!
|
||||
end
|
||||
end
|
||||
end
|
||||
86
app/services/order_cycles/distributed_products_service.rb
Normal file
86
app/services/order_cycles/distributed_products_service.rb
Normal file
@@ -0,0 +1,86 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Returns a (paginatable) AR object for the products or variants in stock for a given shop and OC.
|
||||
# The stock-checking includes on_demand and stock level overrides from variant_overrides.
|
||||
|
||||
module OrderCycles
|
||||
class DistributedProductsService
|
||||
def initialize(distributor, order_cycle, customer)
|
||||
@distributor = distributor
|
||||
@order_cycle = order_cycle
|
||||
@customer = customer
|
||||
end
|
||||
|
||||
def products_relation
|
||||
Spree::Product.where(id: stocked_products).group("spree_products.id")
|
||||
end
|
||||
|
||||
def variants_relation
|
||||
order_cycle.
|
||||
variants_distributed_by(distributor).
|
||||
merge(stocked_variants_and_overrides).
|
||||
select("DISTINCT spree_variants.*")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :distributor, :order_cycle, :customer
|
||||
|
||||
def stocked_products
|
||||
order_cycle.
|
||||
variants_distributed_by(distributor).
|
||||
merge(stocked_variants_and_overrides).
|
||||
select("DISTINCT spree_variants.product_id")
|
||||
end
|
||||
|
||||
def stocked_variants_and_overrides
|
||||
stocked_variants = Spree::Variant.
|
||||
joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.variant_id = spree_variants.id
|
||||
AND variant_overrides.hub_id = #{distributor.id}").
|
||||
joins(:stock_items).
|
||||
where(query_stock_with_overrides)
|
||||
|
||||
ProductTagRulesFilterer.new(distributor, customer, stocked_variants).call
|
||||
end
|
||||
|
||||
def query_stock_with_overrides
|
||||
"( #{variant_not_overriden} AND ( #{variant_on_demand} OR #{variant_in_stock} ) )
|
||||
OR ( #{variant_overriden} AND ( #{override_on_demand} OR #{override_in_stock} ) )
|
||||
OR ( #{variant_overriden} AND ( #{override_on_demand_null} AND #{variant_on_demand} ) )
|
||||
OR ( #{variant_overriden} AND ( #{override_on_demand_null}
|
||||
AND #{variant_not_on_demand} AND #{variant_in_stock} ) )"
|
||||
end
|
||||
|
||||
def variant_not_overriden
|
||||
"variant_overrides.id IS NULL"
|
||||
end
|
||||
|
||||
def variant_overriden
|
||||
"variant_overrides.id IS NOT NULL"
|
||||
end
|
||||
|
||||
def variant_in_stock
|
||||
"spree_stock_items.count_on_hand > 0"
|
||||
end
|
||||
|
||||
def variant_on_demand
|
||||
"spree_stock_items.backorderable IS TRUE"
|
||||
end
|
||||
|
||||
def variant_not_on_demand
|
||||
"spree_stock_items.backorderable IS FALSE"
|
||||
end
|
||||
|
||||
def override_on_demand
|
||||
"variant_overrides.on_demand IS TRUE"
|
||||
end
|
||||
|
||||
def override_in_stock
|
||||
"variant_overrides.count_on_hand > 0"
|
||||
end
|
||||
|
||||
def override_on_demand_null
|
||||
"variant_overrides.on_demand IS NULL"
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/services/order_cycles/distributed_variants_service.rb
Normal file
24
app/services/order_cycles/distributed_variants_service.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderCycles
|
||||
class DistributedVariantsService
|
||||
def initialize(order_cycle, distributor)
|
||||
@order_cycle = order_cycle
|
||||
@distributor = distributor
|
||||
end
|
||||
|
||||
def distributes_order_variants?(order)
|
||||
unavailable_order_variants(order).empty?
|
||||
end
|
||||
|
||||
def unavailable_order_variants(order)
|
||||
order.line_item_variants - available_variants
|
||||
end
|
||||
|
||||
def available_variants
|
||||
return [] unless @order_cycle
|
||||
|
||||
@order_cycle.variants_distributed_by(@distributor)
|
||||
end
|
||||
end
|
||||
end
|
||||
233
app/services/order_cycles/form_service.rb
Normal file
233
app/services/order_cycles/form_service.rb
Normal file
@@ -0,0 +1,233 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/permissions'
|
||||
require 'open_food_network/order_cycle_form_applicator'
|
||||
|
||||
module OrderCycles
|
||||
class FormService
|
||||
def initialize(order_cycle, order_cycle_params, user)
|
||||
@order_cycle = order_cycle
|
||||
@order_cycle_params = order_cycle_params
|
||||
@specified_params = order_cycle_params.keys
|
||||
@user = user
|
||||
@permissions = OpenFoodNetwork::Permissions.new(user)
|
||||
@schedule_ids = order_cycle_params.delete(:schedule_ids)
|
||||
@selected_distributor_payment_method_ids = order_cycle_params.delete(
|
||||
:selected_distributor_payment_method_ids
|
||||
)
|
||||
@selected_distributor_shipping_method_ids = order_cycle_params.delete(
|
||||
:selected_distributor_shipping_method_ids
|
||||
)
|
||||
end
|
||||
|
||||
def save
|
||||
schedule_ids = build_schedule_ids
|
||||
order_cycle.assign_attributes(order_cycle_params)
|
||||
return false unless order_cycle.valid?
|
||||
|
||||
order_cycle.transaction do
|
||||
order_cycle.save!
|
||||
order_cycle.schedule_ids = schedule_ids if parameter_specified?(:schedule_ids)
|
||||
order_cycle.save!
|
||||
apply_exchange_changes
|
||||
if can_update_selected_payment_or_shipping_methods?
|
||||
attach_selected_distributor_payment_methods
|
||||
attach_selected_distributor_shipping_methods
|
||||
end
|
||||
sync_subscriptions
|
||||
true
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
add_exception_to_order_cycle_errors(e)
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_accessor :order_cycle, :order_cycle_params, :user, :permissions
|
||||
|
||||
def add_exception_to_order_cycle_errors(exception)
|
||||
error = exception.message.split(":").last.strip
|
||||
order_cycle.errors.add(:base, error) if order_cycle.errors.to_a.exclude?(error)
|
||||
end
|
||||
|
||||
def apply_exchange_changes
|
||||
return if exchanges_unchanged?
|
||||
|
||||
OpenFoodNetwork::OrderCycleFormApplicator.new(order_cycle, user).go!
|
||||
|
||||
# reload so outgoing exchanges are up-to-date for shipping/payment method validations
|
||||
order_cycle.reload
|
||||
end
|
||||
|
||||
def attach_selected_distributor_payment_methods
|
||||
return if @selected_distributor_payment_method_ids.nil?
|
||||
|
||||
if distributor_only?
|
||||
payment_method_ids = order_cycle.selected_distributor_payment_method_ids
|
||||
payment_method_ids -= user_distributor_payment_method_ids
|
||||
payment_method_ids += user_only_selected_distributor_payment_method_ids
|
||||
order_cycle.selected_distributor_payment_method_ids = payment_method_ids
|
||||
else
|
||||
order_cycle
|
||||
.selected_distributor_payment_method_ids = selected_distributor_payment_method_ids
|
||||
end
|
||||
order_cycle.save!
|
||||
end
|
||||
|
||||
def attach_selected_distributor_shipping_methods
|
||||
return if @selected_distributor_shipping_method_ids.nil?
|
||||
|
||||
if distributor_only?
|
||||
# A distributor can only update methods associated with their own
|
||||
# enterprise, so we load all previously selected methods, and replace
|
||||
# only the distributor's methods with their selection (not touching other
|
||||
# distributor's methods).
|
||||
shipping_method_ids = order_cycle.selected_distributor_shipping_method_ids
|
||||
shipping_method_ids -= user_distributor_shipping_method_ids
|
||||
shipping_method_ids += user_only_selected_distributor_shipping_method_ids
|
||||
order_cycle.selected_distributor_shipping_method_ids = shipping_method_ids
|
||||
else
|
||||
order_cycle.selected_distributor_shipping_method_ids =
|
||||
selected_distributor_shipping_method_ids
|
||||
end
|
||||
|
||||
order_cycle.save!
|
||||
end
|
||||
|
||||
def attachable_distributor_payment_method_ids
|
||||
@attachable_distributor_payment_method_ids ||=
|
||||
order_cycle.attachable_distributor_payment_methods.map(&:id)
|
||||
end
|
||||
|
||||
def attachable_distributor_shipping_method_ids
|
||||
@attachable_distributor_shipping_method_ids ||=
|
||||
order_cycle.attachable_distributor_shipping_methods.map(&:id)
|
||||
end
|
||||
|
||||
def exchanges_unchanged?
|
||||
[:incoming_exchanges, :outgoing_exchanges].all? do |direction|
|
||||
order_cycle_params[direction].nil?
|
||||
end
|
||||
end
|
||||
|
||||
def selected_distributor_payment_method_ids
|
||||
@selected_distributor_payment_method_ids = (
|
||||
attachable_distributor_payment_method_ids &
|
||||
@selected_distributor_payment_method_ids.compact_blank.map(&:to_i)
|
||||
)
|
||||
|
||||
if attachable_distributor_payment_method_ids.sort ==
|
||||
@selected_distributor_payment_method_ids.sort
|
||||
@selected_distributor_payment_method_ids = []
|
||||
end
|
||||
|
||||
@selected_distributor_payment_method_ids
|
||||
end
|
||||
|
||||
def user_only_selected_distributor_payment_method_ids
|
||||
user_distributor_payment_method_ids.intersection(selected_distributor_payment_method_ids)
|
||||
end
|
||||
|
||||
def selected_distributor_shipping_method_ids
|
||||
@selected_distributor_shipping_method_ids = (
|
||||
attachable_distributor_shipping_method_ids &
|
||||
@selected_distributor_shipping_method_ids.compact_blank.map(&:to_i)
|
||||
)
|
||||
|
||||
if attachable_distributor_shipping_method_ids.sort ==
|
||||
@selected_distributor_shipping_method_ids.sort
|
||||
@selected_distributor_shipping_method_ids = []
|
||||
end
|
||||
|
||||
@selected_distributor_shipping_method_ids
|
||||
end
|
||||
|
||||
def user_only_selected_distributor_shipping_method_ids
|
||||
user_distributor_shipping_method_ids.intersection(selected_distributor_shipping_method_ids)
|
||||
end
|
||||
|
||||
def build_schedule_ids
|
||||
return unless parameter_specified?(:schedule_ids)
|
||||
|
||||
result = existing_schedule_ids
|
||||
result |= (requested_schedule_ids & permitted_schedule_ids) # Add permitted and requested
|
||||
# Remove permitted but not requested
|
||||
result -= ((result & permitted_schedule_ids) - requested_schedule_ids)
|
||||
result
|
||||
end
|
||||
|
||||
def sync_subscriptions
|
||||
return unless parameter_specified?(:schedule_ids)
|
||||
return unless schedule_sync_required?
|
||||
|
||||
OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
|
||||
end
|
||||
|
||||
def schedule_sync_required?
|
||||
removed_schedule_ids.any? || new_schedule_ids.any?
|
||||
end
|
||||
|
||||
def subscriptions_to_sync
|
||||
Subscription.where(schedule_id: removed_schedule_ids + new_schedule_ids)
|
||||
end
|
||||
|
||||
def requested_schedule_ids
|
||||
@schedule_ids.map(&:to_i)
|
||||
end
|
||||
|
||||
def parameter_specified?(key)
|
||||
@specified_params.map(&:to_s).include?(key.to_s)
|
||||
end
|
||||
|
||||
def permitted_schedule_ids
|
||||
Schedule.where(id: requested_schedule_ids | existing_schedule_ids)
|
||||
.merge(permissions.editable_schedules).pluck(:id)
|
||||
end
|
||||
|
||||
def existing_schedule_ids
|
||||
@existing_schedule_ids ||= order_cycle.persisted? ? order_cycle.schedule_ids : []
|
||||
end
|
||||
|
||||
def removed_schedule_ids
|
||||
existing_schedule_ids - order_cycle.schedule_ids
|
||||
end
|
||||
|
||||
def new_schedule_ids
|
||||
@order_cycle.schedule_ids - existing_schedule_ids
|
||||
end
|
||||
|
||||
def can_update_selected_payment_or_shipping_methods?
|
||||
@user.admin? || coordinator? || distributor?
|
||||
end
|
||||
|
||||
def coordinator?
|
||||
@user.enterprises.include?(@order_cycle.coordinator)
|
||||
end
|
||||
|
||||
def distributor?
|
||||
!user_distributors_ids.empty?
|
||||
end
|
||||
|
||||
def distributor_only?
|
||||
distributor? && !@user.admin? && !coordinator?
|
||||
end
|
||||
|
||||
def user_distributors_ids
|
||||
@user_distributors_ids ||= @user.enterprises.pluck(:id)
|
||||
.intersection(@order_cycle.distributors.pluck(:id))
|
||||
end
|
||||
|
||||
def user_distributor_payment_method_ids
|
||||
@user_distributor_payment_method_ids ||=
|
||||
DistributorPaymentMethod.where(distributor_id: user_distributors_ids)
|
||||
.pluck(:id)
|
||||
end
|
||||
|
||||
def user_distributor_shipping_method_ids
|
||||
@user_distributor_shipping_method_ids ||=
|
||||
DistributorShippingMethod.where(distributor_id: user_distributors_ids)
|
||||
.pluck(:id)
|
||||
end
|
||||
end
|
||||
end
|
||||
39
app/services/order_cycles/warning_service.rb
Normal file
39
app/services/order_cycles/warning_service.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module OrderCycles
|
||||
class WarningService
|
||||
def initialize(current_user)
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def call
|
||||
distributors = active_distributors_not_ready_for_checkout
|
||||
|
||||
return if distributors.empty?
|
||||
|
||||
active_distributors_not_ready_for_checkout_message(distributors)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :current_user
|
||||
|
||||
def active_distributors_not_ready_for_checkout
|
||||
ocs = OrderCycle.managed_by(current_user).active
|
||||
distributors = ocs.includes(:distributors).map(&:distributors).flatten.uniq
|
||||
Enterprise.where(id: distributors.map(&:id)).not_ready_for_checkout
|
||||
end
|
||||
|
||||
def active_distributors_not_ready_for_checkout_message(distributors)
|
||||
distributor_names = distributors.map(&:name).join ', '
|
||||
|
||||
if distributors.count > 1
|
||||
I18n.t(:active_distributors_not_ready_for_checkout_message_plural,
|
||||
distributor_names:)
|
||||
else
|
||||
I18n.t(:active_distributors_not_ready_for_checkout_message_singular,
|
||||
distributor_names:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/services/order_cycles/webhook_service.rb
Normal file
24
app/services/order_cycles/webhook_service.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Create a webhook payload for an order cycle event.
|
||||
# The payload will be delivered asynchronously.
|
||||
|
||||
module OrderCycles
|
||||
class WebhookService
|
||||
def self.create_webhook_job(order_cycle, event)
|
||||
webhook_payload = order_cycle
|
||||
.slice(:id, :name, :orders_open_at, :orders_close_at, :coordinator_id)
|
||||
.merge(coordinator_name: order_cycle.coordinator.name)
|
||||
|
||||
# Endpoints for coordinator owner
|
||||
webhook_endpoints = order_cycle.coordinator.owner.webhook_endpoints
|
||||
|
||||
# Plus unique endpoints for distributor owners (ignore duplicates)
|
||||
webhook_endpoints |= order_cycle.distributors.map(&:owner).flat_map(&:webhook_endpoints)
|
||||
|
||||
webhook_endpoints.each do |endpoint|
|
||||
WebhookDeliveryJob.perform_later(endpoint.url, event, webhook_payload)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,35 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrderDataMasker
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def call
|
||||
mask_customer_names unless customer_names_allowed?
|
||||
mask_contact_data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_accessor :order
|
||||
|
||||
def customer_names_allowed?
|
||||
order.distributor.show_customer_names_to_suppliers
|
||||
end
|
||||
|
||||
def mask_customer_names
|
||||
order.bill_address&.assign_attributes(firstname: I18n.t('admin.reports.hidden'),
|
||||
lastname: "")
|
||||
order.ship_address&.assign_attributes(firstname: I18n.t('admin.reports.hidden'),
|
||||
lastname: "")
|
||||
end
|
||||
|
||||
def mask_contact_data
|
||||
order.bill_address&.assign_attributes(phone: "", address1: "", address2: "",
|
||||
city: "", zipcode: "", state: nil)
|
||||
order.ship_address&.assign_attributes(phone: "", address1: "", address2: "",
|
||||
city: "", zipcode: "", state: nil)
|
||||
order.assign_attributes(email: I18n.t('admin.reports.hidden'))
|
||||
end
|
||||
end
|
||||
@@ -1,97 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
|
||||
# Builds orders based on a set of attributes
|
||||
# There are some idiosyncracies in the order creation process,
|
||||
# and it is nice to have them dealt with in one place.
|
||||
|
||||
class OrderFactory
|
||||
def initialize(attrs, opts = {})
|
||||
@attrs = attrs.with_indifferent_access
|
||||
@opts = opts.with_indifferent_access
|
||||
end
|
||||
|
||||
def create
|
||||
create_order
|
||||
set_user
|
||||
build_line_items
|
||||
set_addresses
|
||||
create_shipment
|
||||
set_shipping_method
|
||||
create_payment
|
||||
|
||||
@order
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :attrs, :opts
|
||||
|
||||
def customer
|
||||
@customer ||= Customer.find(attrs[:customer_id])
|
||||
end
|
||||
|
||||
def shop
|
||||
@shop ||= Enterprise.find(attrs[:distributor_id])
|
||||
end
|
||||
|
||||
def create_order
|
||||
@order = Spree::Order.create!(create_attrs)
|
||||
end
|
||||
|
||||
def create_attrs
|
||||
create_attrs = attrs.slice(:customer_id, :order_cycle_id, :distributor_id)
|
||||
create_attrs[:email] = customer.email
|
||||
create_attrs
|
||||
end
|
||||
|
||||
def build_line_items
|
||||
attrs[:line_items].each do |li|
|
||||
next unless variant = Spree::Variant.find_by(id: li[:variant_id])
|
||||
|
||||
scoper.scope(variant)
|
||||
li[:quantity] = stock_limited_quantity(variant.on_demand, variant.on_hand, li[:quantity])
|
||||
li[:price] = variant.price
|
||||
build_item_from(li)
|
||||
end
|
||||
end
|
||||
|
||||
def build_item_from(attrs)
|
||||
@order.line_items.build(
|
||||
attrs.merge(skip_stock_check: opts[:skip_stock_check])
|
||||
)
|
||||
end
|
||||
|
||||
def set_user
|
||||
@order.update_attribute(:user_id, customer.user_id)
|
||||
end
|
||||
|
||||
def set_addresses
|
||||
@order.update(attrs.slice(:bill_address_attributes, :ship_address_attributes))
|
||||
end
|
||||
|
||||
def create_shipment
|
||||
@order.create_proposed_shipments
|
||||
end
|
||||
|
||||
def set_shipping_method
|
||||
@order.select_shipping_method(attrs[:shipping_method_id])
|
||||
end
|
||||
|
||||
def create_payment
|
||||
@order.recreate_all_fees!
|
||||
@order.payments.create(payment_method_id: attrs[:payment_method_id],
|
||||
amount: @order.reload.total)
|
||||
end
|
||||
|
||||
def stock_limited_quantity(variant_on_demand, variant_on_hand, requested)
|
||||
return requested if opts[:skip_stock_check] || variant_on_demand
|
||||
|
||||
[variant_on_hand, requested].min
|
||||
end
|
||||
|
||||
def scoper
|
||||
@scoper ||= OpenFoodNetwork::ScopeVariantToHub.new(shop)
|
||||
end
|
||||
end
|
||||
@@ -1,68 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrderFeesHandler
|
||||
attr_reader :order
|
||||
|
||||
delegate :distributor, :order_cycle, to: :order
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def recreate_all_fees!
|
||||
# `with_lock` acquires an exclusive row lock on order so no other
|
||||
# requests can update it until the transaction is commited.
|
||||
# See https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/locking/pessimistic.rb#L69
|
||||
# and https://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE
|
||||
order.with_lock do
|
||||
EnterpriseFee.clear_all_adjustments order
|
||||
|
||||
create_line_item_fees!
|
||||
create_order_fees!
|
||||
end
|
||||
|
||||
tax_enterprise_fees! unless order.before_payment_state?
|
||||
order.update_order!
|
||||
end
|
||||
|
||||
def create_line_item_fees!
|
||||
order.line_items.includes(variant: :product).each do |line_item|
|
||||
if provided_by_order_cycle? line_item
|
||||
calculator.create_line_item_adjustments_for line_item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_order_fees!
|
||||
return unless order_cycle
|
||||
|
||||
calculator.create_order_adjustments_for order
|
||||
end
|
||||
|
||||
def tax_enterprise_fees!
|
||||
Spree::TaxRate.adjust(order, order.all_adjustments.enterprise_fee)
|
||||
end
|
||||
|
||||
def update_line_item_fees!(line_item)
|
||||
line_item.adjustments.enterprise_fee.each do |fee|
|
||||
fee.update_adjustment!(line_item, force: true)
|
||||
end
|
||||
end
|
||||
|
||||
def update_order_fees!
|
||||
order.adjustments.enterprise_fee.where(adjustable_type: 'Spree::Order').each do |fee|
|
||||
fee.update_adjustment!(order, force: true)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def calculator
|
||||
@calculator ||= OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle)
|
||||
end
|
||||
|
||||
def provided_by_order_cycle?(line_item)
|
||||
@order_cycle_variant_ids ||= order_cycle&.variants&.map(&:id) || []
|
||||
@order_cycle_variant_ids.include? line_item.variant_id
|
||||
end
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrderInvoiceComparator
|
||||
attr_reader :order
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def can_generate_new_invoice?
|
||||
return true if invoices.empty?
|
||||
|
||||
# We'll use a recursive BFS algorithm to find if the invoice is outdated
|
||||
# the root will be the order
|
||||
# On each node, we'll a list of relevant attributes that will be used on the comparison
|
||||
different?(current_state_invoice, latest_invoice, invoice_generation_selector)
|
||||
end
|
||||
|
||||
def can_update_latest_invoice?
|
||||
return false if invoices.empty?
|
||||
|
||||
different?(current_state_invoice, latest_invoice, invoice_update_selector)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def different?(node1, node2, attributes_selector)
|
||||
simple_values1, presenters1 = attributes_selector.call(node1)
|
||||
simple_values2, presenters2 = attributes_selector.call(node2)
|
||||
return true if simple_values1 != simple_values2
|
||||
|
||||
return true if presenters1.size != presenters2.size
|
||||
|
||||
presenters1.zip(presenters2).any? do |presenter1, presenter2|
|
||||
different?(presenter1, presenter2, attributes_selector)
|
||||
end
|
||||
end
|
||||
|
||||
def invoice_generation_selector
|
||||
values_selector(:invoice_generation_values)
|
||||
end
|
||||
|
||||
def invoice_update_selector
|
||||
values_selector(:invoice_update_values)
|
||||
end
|
||||
|
||||
def values_selector(attribute)
|
||||
proc do |node|
|
||||
return [[], []] unless node.respond_to?(attribute)
|
||||
|
||||
grouped = node.public_send(attribute).group_by(&grouper)
|
||||
[grouped[:simple] || [], grouped[:presenters]&.flatten || []]
|
||||
end
|
||||
end
|
||||
|
||||
def grouper
|
||||
proc do |value|
|
||||
if value.is_a?(Array) || value.class.to_s.starts_with?("Invoice::DataPresenter")
|
||||
:presenters
|
||||
else
|
||||
:simple
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def current_state_invoice
|
||||
@current_state_invoice ||= Invoice.new(
|
||||
order:,
|
||||
data: serialize_for_invoice,
|
||||
date: Time.zone.today,
|
||||
number: invoices.count + 1
|
||||
).presenter
|
||||
end
|
||||
|
||||
def invoices
|
||||
order.invoices
|
||||
end
|
||||
|
||||
def latest_invoice
|
||||
@latest_invoice ||= invoices.first.presenter
|
||||
end
|
||||
|
||||
def serialize_for_invoice
|
||||
InvoiceDataGenerator.new(order).serialize_for_invoice
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrderInvoiceGenerator
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def generate_or_update_latest_invoice
|
||||
if comparator.can_generate_new_invoice?
|
||||
order.invoices.create!(
|
||||
date: Time.zone.today,
|
||||
number: total_invoices_created_by_distributor + 1,
|
||||
data: invoice_data
|
||||
)
|
||||
elsif comparator.can_update_latest_invoice?
|
||||
order.invoices.latest.update!(
|
||||
date: Time.zone.today,
|
||||
data: invoice_data
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
|
||||
def comparator
|
||||
@comparator ||= OrderInvoiceComparator.new(order)
|
||||
end
|
||||
|
||||
def invoice_data
|
||||
@invoice_data ||= InvoiceDataGenerator.new(order).generate
|
||||
end
|
||||
|
||||
def total_invoices_created_by_distributor
|
||||
Invoice.joins(:order).where(order: { distributor: order.distributor }).count
|
||||
end
|
||||
end
|
||||
@@ -1,28 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrderPaymentFinder
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def last_payment
|
||||
last(@order.payments)
|
||||
end
|
||||
|
||||
def last_pending_payment
|
||||
last(@order.pending_payments)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# `max_by` avoids additional database queries when payments are loaded
|
||||
# already. There is usually only one payment and this shouldn't cause
|
||||
# any overhead compared to `order(:created_at).last`. Using `last`
|
||||
# without order is not deterministic.
|
||||
#
|
||||
# We are not using `updated_at` because all payments are touched when the
|
||||
# order is updated and then all payments have the same `updated_at` value.
|
||||
def last(payments)
|
||||
payments.max_by(&:created_at)
|
||||
end
|
||||
end
|
||||
@@ -1,138 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Responsible for ensuring that any updates to a Subscription are propagated to any
|
||||
# orders belonging to that Subscription which have been instantiated
|
||||
class OrderSyncer
|
||||
attr_reader :order_update_issues
|
||||
|
||||
def initialize(subscription)
|
||||
@subscription = subscription
|
||||
@order_update_issues = OrderUpdateIssues.new
|
||||
@line_item_syncer = LineItemSyncer.new(subscription, order_update_issues)
|
||||
end
|
||||
|
||||
def sync!
|
||||
orders_in_order_cycles_not_closed.all? do |order|
|
||||
order.assign_attributes(customer_id:, email: customer&.email,
|
||||
distributor_id: shop_id)
|
||||
update_associations_for(order)
|
||||
line_item_syncer.sync!(order)
|
||||
order.update_order!
|
||||
order.save
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :subscription, :line_item_syncer
|
||||
|
||||
delegate :orders, :bill_address, :ship_address, :subscription_line_items, to: :subscription
|
||||
delegate :shop_id, :customer, :customer_id, to: :subscription
|
||||
delegate :shipping_method, :shipping_method_id,
|
||||
:payment_method, :payment_method_id, to: :subscription
|
||||
delegate :shipping_method_id_changed?, :shipping_method_id_was, to: :subscription
|
||||
delegate :payment_method_id_changed?, :payment_method_id_was, to: :subscription
|
||||
|
||||
def update_associations_for(order)
|
||||
update_bill_address_for(order) if (bill_address.changes.keys & relevant_address_attrs).any?
|
||||
update_shipment_for(order) if shipping_method_id_changed?
|
||||
update_ship_address_for(order)
|
||||
update_payment_for(order) if payment_method_id_changed?
|
||||
end
|
||||
|
||||
def orders_in_order_cycles_not_closed
|
||||
return @orders_in_order_cycles_not_closed unless @orders_in_order_cycles_not_closed.nil?
|
||||
|
||||
@orders_in_order_cycles_not_closed = orders.joins(:order_cycle).
|
||||
merge(OrderCycle.not_closed).readonly(false)
|
||||
end
|
||||
|
||||
def update_bill_address_for(order)
|
||||
unless addresses_match?(order.bill_address, bill_address)
|
||||
return order_update_issues.add(order, I18n.t('bill_address'))
|
||||
end
|
||||
|
||||
order.bill_address.update(bill_address.attributes.slice(*relevant_address_attrs))
|
||||
end
|
||||
|
||||
def update_payment_for(order)
|
||||
payment = order.payments.
|
||||
with_state('checkout').where(payment_method_id: payment_method_id_was).last
|
||||
if payment
|
||||
payment&.void_transaction!
|
||||
order.payments.create(payment_method_id:, amount: order.reload.total)
|
||||
else
|
||||
unless order.payments.with_state('checkout').where(payment_method_id:).any?
|
||||
order_update_issues.add(order, I18n.t('admin.payment_method'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_shipment_for(order)
|
||||
return if pending_shipment_with?(order, shipping_method_id) # No need to do anything.
|
||||
|
||||
if pending_shipment_with?(order, shipping_method_id_was)
|
||||
order.select_shipping_method(shipping_method_id)
|
||||
else
|
||||
order_update_issues.add(order, I18n.t('admin.shipping_method'))
|
||||
end
|
||||
end
|
||||
|
||||
def update_ship_address_for(order)
|
||||
# The conditions here are to achieve the same behaviour in earlier versions of Spree, where
|
||||
# switching from pick-up to delivery affects whether simultaneous changes to shipping address
|
||||
# are ignored or not.
|
||||
pickup_to_delivery = force_ship_address_required?(order)
|
||||
if (!pickup_to_delivery || order.shipment.present?) &&
|
||||
(ship_address.changes.keys & relevant_address_attrs).any?
|
||||
save_ship_address_in_order(order)
|
||||
end
|
||||
return unless !pickup_to_delivery || order.shipment.blank?
|
||||
|
||||
order.updater.shipping_address_from_distributor
|
||||
end
|
||||
|
||||
def relevant_address_attrs
|
||||
["firstname", "lastname", "address1", "zipcode", "city", "state_id", "country_id", "phone"]
|
||||
end
|
||||
|
||||
def addresses_match?(order_address, subscription_address)
|
||||
relevant_address_attrs.all? do |attr|
|
||||
order_address[attr] == subscription_address.public_send("#{attr}_was") ||
|
||||
order_address[attr] == subscription_address[attr]
|
||||
end
|
||||
end
|
||||
|
||||
def ship_address_updatable?(order)
|
||||
return true if force_ship_address_required?(order)
|
||||
return false unless order.shipping_method.require_ship_address?
|
||||
return true if addresses_match?(order.ship_address, ship_address)
|
||||
|
||||
order_update_issues.add(order, I18n.t('ship_address'))
|
||||
false
|
||||
end
|
||||
|
||||
# This returns true when the shipping method on the subscription has changed
|
||||
# to a delivery (ie. a shipping address is required) AND the existing shipping
|
||||
# address on the order matches the shop's address
|
||||
def force_ship_address_required?(order)
|
||||
return false unless shipping_method.require_ship_address?
|
||||
|
||||
distributor_address = order.address_from_distributor
|
||||
relevant_address_attrs.all? do |attr|
|
||||
order.ship_address[attr] == distributor_address[attr]
|
||||
end
|
||||
end
|
||||
|
||||
def save_ship_address_in_order(order)
|
||||
return unless ship_address_updatable?(order)
|
||||
|
||||
order.ship_address.update(ship_address.attributes.slice(*relevant_address_attrs))
|
||||
end
|
||||
|
||||
def pending_shipment_with?(order, shipping_method_id)
|
||||
return false unless order.shipment.present? && order.shipment.state == "pending"
|
||||
|
||||
order.shipping_method.id == shipping_method_id
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Collects Tax Adjustments related to an order, and returns a hash with a total for each rate.
|
||||
|
||||
class OrderTaxAdjustmentsFetcher
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def totals(tax_adjustments = order.all_adjustments.tax)
|
||||
tax_adjustments.each_with_object({}) do |adjustment, hash|
|
||||
tax_rate = adjustment.originator
|
||||
hash[tax_rate] = hash[tax_rate].to_f + adjustment.amount
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
end
|
||||
@@ -1,21 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Wrapper for a hash of issues encountered by instances of OrderSyncer and LineItemSyncer
|
||||
# Used to report issues to the user when they attempt to update a subscription
|
||||
|
||||
class OrderUpdateIssues
|
||||
def initialize
|
||||
@issues = {}
|
||||
end
|
||||
|
||||
delegate :[], :keys, to: :issues
|
||||
|
||||
def add(order, issue)
|
||||
@issues[order.id] ||= []
|
||||
@issues[order.id] << issue
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :issues
|
||||
end
|
||||
@@ -1,95 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrderWorkflow
|
||||
attr_reader :order
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def complete
|
||||
advance_to_state("complete", advance_order_options)
|
||||
end
|
||||
|
||||
def complete!
|
||||
advance_order!(advance_order_options)
|
||||
end
|
||||
|
||||
def next(options = {})
|
||||
result = advance_order_one_step
|
||||
|
||||
after_transition_hook(options)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def advance_to_payment
|
||||
return unless order.before_payment_state?
|
||||
|
||||
advance_to_state("payment", advance_order_options)
|
||||
end
|
||||
|
||||
def advance_checkout(options = {})
|
||||
advance_to = order.before_payment_state? ? "payment" : "confirmation"
|
||||
|
||||
advance_to_state(advance_to, advance_order_options.merge(options))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def advance_order_options
|
||||
shipping_method_id = order.shipping_method.id if order.shipping_method.present?
|
||||
{ "shipping_method_id" => shipping_method_id }
|
||||
end
|
||||
|
||||
def advance_to_state(target_state, options = {})
|
||||
until order.state == target_state
|
||||
break unless order.next
|
||||
|
||||
after_transition_hook(options)
|
||||
end
|
||||
|
||||
order.state == target_state
|
||||
end
|
||||
|
||||
def advance_order!(options)
|
||||
until order.completed?
|
||||
order.next!
|
||||
after_transition_hook(options)
|
||||
end
|
||||
end
|
||||
|
||||
def advance_order_one_step
|
||||
tries ||= 3
|
||||
order.next
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
retry unless (tries -= 1).zero?
|
||||
false
|
||||
end
|
||||
|
||||
def after_transition_hook(options)
|
||||
if order.state == "delivery"
|
||||
order.select_shipping_method(options["shipping_method_id"])
|
||||
end
|
||||
|
||||
persist_all_payments if order.state == "payment"
|
||||
end
|
||||
|
||||
# When a payment fails, the order state machine stays in 'payment' and rollbacks all transactions
|
||||
# This rollback also reverts the payment state from 'failed', 'void' or 'invalid' to 'pending'
|
||||
# Despite the rollback, the in-memory payment still has the correct state, so we persist it
|
||||
def persist_all_payments
|
||||
order.payments.each do |payment|
|
||||
in_memory_payment_state = payment.state
|
||||
if different_from_db_payment_state?(in_memory_payment_state, payment.id)
|
||||
payment.reload.update(state: in_memory_payment_state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Verifies if the in-memory payment state is different from the one stored in the database
|
||||
# This is be done without reloading the payment so that in-memory data is not changed
|
||||
def different_from_db_payment_state?(in_memory_payment_state, payment_id)
|
||||
in_memory_payment_state != Spree::Payment.find(payment_id).state
|
||||
end
|
||||
end
|
||||
45
app/services/orders/available_payment_methods_service.rb
Normal file
45
app/services/orders/available_payment_methods_service.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/tag_rule_applicator'
|
||||
|
||||
module Orders
|
||||
class AvailablePaymentMethodsService
|
||||
attr_reader :order, :customer
|
||||
|
||||
delegate :distributor,
|
||||
:order_cycle,
|
||||
to: :order
|
||||
|
||||
def initialize(order, customer = nil)
|
||||
@order, @customer = order, customer
|
||||
end
|
||||
|
||||
def to_a
|
||||
return [] if distributor.blank?
|
||||
|
||||
payment_methods = payment_methods_before_tag_rules_applied
|
||||
|
||||
applicator = OpenFoodNetwork::TagRuleApplicator
|
||||
.new(distributor, "FilterPaymentMethods", customer&.tag_list)
|
||||
applicator.filter(payment_methods)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def payment_methods_before_tag_rules_applied
|
||||
if order_cycle.nil? || order_cycle.simple?
|
||||
distributor.payment_methods
|
||||
else
|
||||
distributor.payment_methods.where(
|
||||
id: available_distributor_payment_methods_ids
|
||||
)
|
||||
end.available.select(&:configured?).uniq
|
||||
end
|
||||
|
||||
def available_distributor_payment_methods_ids
|
||||
order_cycle.distributor_payment_methods
|
||||
.where(distributor_id: distributor.id)
|
||||
.select(:payment_method_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
58
app/services/orders/available_shipping_methods_service.rb
Normal file
58
app/services/orders/available_shipping_methods_service.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/tag_rule_applicator'
|
||||
|
||||
module Orders
|
||||
class AvailableShippingMethodsService
|
||||
attr_reader :order, :customer
|
||||
|
||||
delegate :distributor, :order_cycle, to: :order
|
||||
|
||||
def initialize(order, customer = nil)
|
||||
@order, @customer = order, customer
|
||||
end
|
||||
|
||||
def to_a
|
||||
return [] if distributor.blank?
|
||||
|
||||
filter_by_category(tag_rules.filter(shipping_methods))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_by_category(methods)
|
||||
return methods unless OpenFoodNetwork::FeatureToggle.enabled?(:match_shipping_categories,
|
||||
distributor&.owner)
|
||||
|
||||
required_category_ids = order.variants.pluck(:shipping_category_id).to_set
|
||||
return methods if required_category_ids.empty?
|
||||
|
||||
methods.select do |method|
|
||||
provided_category_ids = method.shipping_categories.pluck(:id).to_set
|
||||
required_category_ids.subset?(provided_category_ids)
|
||||
end
|
||||
end
|
||||
|
||||
def shipping_methods
|
||||
if order_cycle.nil? || order_cycle.simple?
|
||||
distributor.shipping_methods
|
||||
else
|
||||
distributor.shipping_methods.where(
|
||||
id: available_distributor_shipping_methods_ids
|
||||
)
|
||||
end.frontend.to_a.uniq
|
||||
end
|
||||
|
||||
def available_distributor_shipping_methods_ids
|
||||
order_cycle.distributor_shipping_methods
|
||||
.where(distributor_id: distributor.id)
|
||||
.select(:shipping_method_id)
|
||||
end
|
||||
|
||||
def tag_rules
|
||||
OpenFoodNetwork::TagRuleApplicator.new(
|
||||
distributor, "FilterShippingMethods", customer&.tag_list
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
28
app/services/orders/bulk_cancel_service.rb
Normal file
28
app/services/orders/bulk_cancel_service.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Orders
|
||||
class BulkCancelService
|
||||
def initialize(params, current_user)
|
||||
@order_ids = params[:bulk_ids]
|
||||
@current_user = current_user
|
||||
@send_cancellation_email = params[:send_cancellation_email]
|
||||
@restock_items = params[:restock_items]
|
||||
end
|
||||
|
||||
def call
|
||||
# rubocop:disable Rails/FindEach # .each returns an array, .find_each returns nil
|
||||
editable_orders.where(id: @order_ids).each do |order|
|
||||
order.send_cancellation_email = @send_cancellation_email
|
||||
order.restock_items = @restock_items
|
||||
order.cancel
|
||||
end
|
||||
# rubocop:enable Rails/FindEach
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def editable_orders
|
||||
Permissions::Order.new(@current_user).editable_orders
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/services/orders/capture_service.rb
Normal file
24
app/services/orders/capture_service.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Use `authorize! :admin order` before calling this service
|
||||
|
||||
module Orders
|
||||
class CaptureService
|
||||
attr_reader :gateway_error
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
@gateway_error = nil
|
||||
end
|
||||
|
||||
def call
|
||||
return false unless @order.payment_required?
|
||||
return false unless (pending_payment = @order.pending_payments.first)
|
||||
|
||||
pending_payment.capture!
|
||||
rescue Spree::Core::GatewayError => e
|
||||
@gateway_error = e
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
59
app/services/orders/cart_reset_service.rb
Normal file
59
app/services/orders/cart_reset_service.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
# Resets an order by verifying it's state and fixing any issues
|
||||
|
||||
module Orders
|
||||
class CartResetService
|
||||
def initialize(order, distributor_id)
|
||||
@order = order
|
||||
@distributor ||= Enterprise.is_distributor.find_by(permalink: distributor_id) ||
|
||||
Enterprise.is_distributor.find(distributor_id)
|
||||
end
|
||||
|
||||
def reset_distributor
|
||||
if order.distributor && order.distributor != distributor
|
||||
order.empty!
|
||||
order.set_order_cycle! nil
|
||||
end
|
||||
order.distributor = distributor
|
||||
end
|
||||
|
||||
def reset_other!(current_user, current_customer)
|
||||
reset_user_and_customer(current_user)
|
||||
reset_order_cycle(current_customer)
|
||||
order.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order, :distributor, :current_user
|
||||
|
||||
def reset_user_and_customer(current_user)
|
||||
return unless current_user
|
||||
|
||||
order.associate_user!(current_user) if order.user.blank? || order.email.blank?
|
||||
end
|
||||
|
||||
def reset_order_cycle(current_customer)
|
||||
listed_order_cycles = Shop::OrderCyclesList.active_for(distributor, current_customer)
|
||||
|
||||
if order_cycle_not_listed?(order.order_cycle, listed_order_cycles)
|
||||
order.order_cycle = nil
|
||||
order.empty!
|
||||
end
|
||||
|
||||
select_default_order_cycle(order, listed_order_cycles)
|
||||
end
|
||||
|
||||
def order_cycle_not_listed?(order_cycle, listed_order_cycles)
|
||||
order_cycle.present? && listed_order_cycles.exclude?(order_cycle)
|
||||
end
|
||||
|
||||
# If no OC is selected and there is only one in the list of OCs, selects it
|
||||
def select_default_order_cycle(order, listed_order_cycles)
|
||||
return unless order.order_cycle.blank? && listed_order_cycles.size == 1
|
||||
|
||||
order.order_cycle = listed_order_cycles.first
|
||||
end
|
||||
end
|
||||
end
|
||||
37
app/services/orders/checkout_restart_service.rb
Normal file
37
app/services/orders/checkout_restart_service.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Resets the passed order to cart state while clearing associated payments and shipments
|
||||
|
||||
module Orders
|
||||
class CheckoutRestartService
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def call
|
||||
return if order.cart?
|
||||
|
||||
reset_state_to_cart
|
||||
clear_shipments
|
||||
clear_payments
|
||||
|
||||
order.reload.update_order!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
|
||||
def reset_state_to_cart
|
||||
order.restart_checkout!
|
||||
end
|
||||
|
||||
def clear_shipments
|
||||
order.shipments.with_state(:pending).destroy_all
|
||||
end
|
||||
|
||||
def clear_payments
|
||||
order.payments.with_state(:checkout).destroy_all
|
||||
end
|
||||
end
|
||||
end
|
||||
88
app/services/orders/compare_invoice_service.rb
Normal file
88
app/services/orders/compare_invoice_service.rb
Normal file
@@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Orders
|
||||
class CompareInvoiceService
|
||||
attr_reader :order
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def can_generate_new_invoice?
|
||||
return true if invoices.empty?
|
||||
|
||||
# We'll use a recursive BFS algorithm to find if the invoice is outdated
|
||||
# the root will be the order
|
||||
# On each node, we'll a list of relevant attributes that will be used on the comparison
|
||||
different?(current_state_invoice, latest_invoice, invoice_generation_selector)
|
||||
end
|
||||
|
||||
def can_update_latest_invoice?
|
||||
return false if invoices.empty?
|
||||
|
||||
different?(current_state_invoice, latest_invoice, invoice_update_selector)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def different?(node1, node2, attributes_selector)
|
||||
simple_values1, presenters1 = attributes_selector.call(node1)
|
||||
simple_values2, presenters2 = attributes_selector.call(node2)
|
||||
return true if simple_values1 != simple_values2
|
||||
|
||||
return true if presenters1.size != presenters2.size
|
||||
|
||||
presenters1.zip(presenters2).any? do |presenter1, presenter2|
|
||||
different?(presenter1, presenter2, attributes_selector)
|
||||
end
|
||||
end
|
||||
|
||||
def invoice_generation_selector
|
||||
values_selector(:invoice_generation_values)
|
||||
end
|
||||
|
||||
def invoice_update_selector
|
||||
values_selector(:invoice_update_values)
|
||||
end
|
||||
|
||||
def values_selector(attribute)
|
||||
proc do |node|
|
||||
return [[], []] unless node.respond_to?(attribute)
|
||||
|
||||
grouped = node.public_send(attribute).group_by(&grouper)
|
||||
[grouped[:simple] || [], grouped[:presenters]&.flatten || []]
|
||||
end
|
||||
end
|
||||
|
||||
def grouper
|
||||
proc do |value|
|
||||
if value.is_a?(Array) || value.class.to_s.starts_with?("Invoice::DataPresenter")
|
||||
:presenters
|
||||
else
|
||||
:simple
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def current_state_invoice
|
||||
@current_state_invoice ||= Invoice.new(
|
||||
order:,
|
||||
data: serialize_for_invoice,
|
||||
date: Time.zone.today,
|
||||
number: invoices.count + 1
|
||||
).presenter
|
||||
end
|
||||
|
||||
def invoices
|
||||
order.invoices
|
||||
end
|
||||
|
||||
def latest_invoice
|
||||
@latest_invoice ||= invoices.first.presenter
|
||||
end
|
||||
|
||||
def serialize_for_invoice
|
||||
InvoiceDataGenerator.new(order).serialize_for_invoice
|
||||
end
|
||||
end
|
||||
end
|
||||
19
app/services/orders/customer_cancellation_service.rb
Normal file
19
app/services/orders/customer_cancellation_service.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Orders
|
||||
class CustomerCancellationService
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def call
|
||||
return unless order.cancel
|
||||
|
||||
Spree::OrderMailer.cancel_email_for_shop(order).deliver_later
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
end
|
||||
end
|
||||
99
app/services/orders/factory_service.rb
Normal file
99
app/services/orders/factory_service.rb
Normal file
@@ -0,0 +1,99 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
|
||||
# Builds orders based on a set of attributes
|
||||
# There are some idiosyncracies in the order creation process,
|
||||
# and it is nice to have them dealt with in one place.
|
||||
|
||||
module Orders
|
||||
class FactoryService
|
||||
def initialize(attrs, opts = {})
|
||||
@attrs = attrs.with_indifferent_access
|
||||
@opts = opts.with_indifferent_access
|
||||
end
|
||||
|
||||
def create
|
||||
create_order
|
||||
set_user
|
||||
build_line_items
|
||||
set_addresses
|
||||
create_shipment
|
||||
set_shipping_method
|
||||
create_payment
|
||||
|
||||
@order
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :attrs, :opts
|
||||
|
||||
def customer
|
||||
@customer ||= Customer.find(attrs[:customer_id])
|
||||
end
|
||||
|
||||
def shop
|
||||
@shop ||= Enterprise.find(attrs[:distributor_id])
|
||||
end
|
||||
|
||||
def create_order
|
||||
@order = Spree::Order.create!(create_attrs)
|
||||
end
|
||||
|
||||
def create_attrs
|
||||
create_attrs = attrs.slice(:customer_id, :order_cycle_id, :distributor_id)
|
||||
create_attrs[:email] = customer.email
|
||||
create_attrs
|
||||
end
|
||||
|
||||
def build_line_items
|
||||
attrs[:line_items].each do |li|
|
||||
next unless variant = Spree::Variant.find_by(id: li[:variant_id])
|
||||
|
||||
scoper.scope(variant)
|
||||
li[:quantity] = stock_limited_quantity(variant.on_demand, variant.on_hand, li[:quantity])
|
||||
li[:price] = variant.price
|
||||
build_item_from(li)
|
||||
end
|
||||
end
|
||||
|
||||
def build_item_from(attrs)
|
||||
@order.line_items.build(
|
||||
attrs.merge(skip_stock_check: opts[:skip_stock_check])
|
||||
)
|
||||
end
|
||||
|
||||
def set_user
|
||||
@order.update_attribute(:user_id, customer.user_id)
|
||||
end
|
||||
|
||||
def set_addresses
|
||||
@order.update(attrs.slice(:bill_address_attributes, :ship_address_attributes))
|
||||
end
|
||||
|
||||
def create_shipment
|
||||
@order.create_proposed_shipments
|
||||
end
|
||||
|
||||
def set_shipping_method
|
||||
@order.select_shipping_method(attrs[:shipping_method_id])
|
||||
end
|
||||
|
||||
def create_payment
|
||||
@order.recreate_all_fees!
|
||||
@order.payments.create(payment_method_id: attrs[:payment_method_id],
|
||||
amount: @order.reload.total)
|
||||
end
|
||||
|
||||
def stock_limited_quantity(variant_on_demand, variant_on_hand, requested)
|
||||
return requested if opts[:skip_stock_check] || variant_on_demand
|
||||
|
||||
[variant_on_hand, requested].min
|
||||
end
|
||||
|
||||
def scoper
|
||||
@scoper ||= OpenFoodNetwork::ScopeVariantToHub.new(shop)
|
||||
end
|
||||
end
|
||||
end
|
||||
81
app/services/orders/fetch_adjustments_service.rb
Normal file
81
app/services/orders/fetch_adjustments_service.rb
Normal file
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This service allows orders with eager-loaded adjustment objects to calculate various adjustment
|
||||
# types without triggering additional queries.
|
||||
#
|
||||
# For example; `order.adjustments.shipping.sum(:amount)` would normally trigger a new query
|
||||
# regardless of whether or not adjustments have been preloaded, as `#shipping` is an adjustment
|
||||
# scope, eg; `scope :shipping, where(originator_type: 'Spree::ShippingMethod')`.
|
||||
#
|
||||
# Here the adjustment scopes are moved to a shared module, and `adjustments.loaded?` is used to
|
||||
# check if the objects have already been fetched and initialized. If they have, `order.adjustments`
|
||||
# will be an Array, and we can select the required objects without hitting the database. If not, it
|
||||
# will fetch the adjustments via their scopes as normal.
|
||||
|
||||
module Orders
|
||||
class FetchAdjustmentsService
|
||||
include AdjustmentScopes
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def admin_and_handling_total
|
||||
admin_and_handling_fees.map(&:amount).sum
|
||||
end
|
||||
|
||||
def payment_fee
|
||||
sum_adjustments "payment_fee"
|
||||
end
|
||||
|
||||
def ship_total
|
||||
sum_adjustments "shipping"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
|
||||
def adjustments
|
||||
order.all_adjustments
|
||||
end
|
||||
|
||||
def adjustments_eager_loaded?
|
||||
adjustments.loaded?
|
||||
end
|
||||
|
||||
def sum_adjustments(scope)
|
||||
collect_adjustments(scope).map(&:amount).sum
|
||||
end
|
||||
|
||||
def collect_adjustments(scope)
|
||||
if adjustments_eager_loaded?
|
||||
adjustment_scope = public_send("#{scope}_scope")
|
||||
|
||||
# Adjustments are already loaded here, this block is using `Array#select`
|
||||
adjustments.select do |adjustment|
|
||||
match_by_scope(adjustment, adjustment_scope) && match_by_scope(adjustment, eligible_scope)
|
||||
end
|
||||
else
|
||||
adjustments.where(nil).eligible.public_send scope
|
||||
end
|
||||
end
|
||||
|
||||
def admin_and_handling_fees
|
||||
if adjustments_eager_loaded?
|
||||
adjustments.select do |adjustment|
|
||||
match_by_scope(adjustment, eligible_scope) &&
|
||||
adjustment.originator_type == 'EnterpriseFee' &&
|
||||
adjustment.adjustable_type != 'Spree::LineItem'
|
||||
end
|
||||
else
|
||||
adjustments.eligible.
|
||||
where("originator_type = ? AND adjustable_type != ?", 'EnterpriseFee', 'Spree::LineItem')
|
||||
end
|
||||
end
|
||||
|
||||
def match_by_scope(adjustment, scope)
|
||||
adjustment.public_send(scope.keys.first) == scope.values.first
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/services/orders/fetch_tax_adjustments_service.rb
Normal file
22
app/services/orders/fetch_tax_adjustments_service.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Collects Tax Adjustments related to an order, and returns a hash with a total for each rate.
|
||||
|
||||
module Orders
|
||||
class FetchTaxAdjustmentsService
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def totals(tax_adjustments = order.all_adjustments.tax)
|
||||
tax_adjustments.each_with_object({}) do |adjustment, hash|
|
||||
tax_rate = adjustment.originator
|
||||
hash[tax_rate] = hash[tax_rate].to_f + adjustment.amount
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
end
|
||||
end
|
||||
30
app/services/orders/find_payment_service.rb
Normal file
30
app/services/orders/find_payment_service.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Orders
|
||||
class FindPaymentService
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def last_payment
|
||||
last(@order.payments)
|
||||
end
|
||||
|
||||
def last_pending_payment
|
||||
last(@order.pending_payments)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# `max_by` avoids additional database queries when payments are loaded
|
||||
# already. There is usually only one payment and this shouldn't cause
|
||||
# any overhead compared to `order(:created_at).last`. Using `last`
|
||||
# without order is not deterministic.
|
||||
#
|
||||
# We are not using `updated_at` because all payments are touched when the
|
||||
# order is updated and then all payments have the same `updated_at` value.
|
||||
def last(payments)
|
||||
payments.max_by(&:created_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
40
app/services/orders/generate_invoice_service.rb
Normal file
40
app/services/orders/generate_invoice_service.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Orders
|
||||
class GenerateInvoiceService
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def generate_or_update_latest_invoice
|
||||
if comparator.can_generate_new_invoice?
|
||||
order.invoices.create!(
|
||||
date: Time.zone.today,
|
||||
number: total_invoices_created_by_distributor + 1,
|
||||
data: invoice_data
|
||||
)
|
||||
elsif comparator.can_update_latest_invoice?
|
||||
order.invoices.latest.update!(
|
||||
date: Time.zone.today,
|
||||
data: invoice_data
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :order
|
||||
|
||||
def comparator
|
||||
@comparator ||= Orders::CompareInvoiceService.new(order)
|
||||
end
|
||||
|
||||
def invoice_data
|
||||
@invoice_data ||= InvoiceDataGenerator.new(order).generate
|
||||
end
|
||||
|
||||
def total_invoices_created_by_distributor
|
||||
Invoice.joins(:order).where(order: { distributor: order.distributor }).count
|
||||
end
|
||||
end
|
||||
end
|
||||
70
app/services/orders/handle_fees_service.rb
Normal file
70
app/services/orders/handle_fees_service.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Orders
|
||||
class HandleFeesService
|
||||
attr_reader :order
|
||||
|
||||
delegate :distributor, :order_cycle, to: :order
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def recreate_all_fees!
|
||||
# `with_lock` acquires an exclusive row lock on order so no other
|
||||
# requests can update it until the transaction is commited.
|
||||
# See https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/locking/pessimistic.rb#L69
|
||||
# and https://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE
|
||||
order.with_lock do
|
||||
EnterpriseFee.clear_all_adjustments order
|
||||
|
||||
create_line_item_fees!
|
||||
create_order_fees!
|
||||
end
|
||||
|
||||
tax_enterprise_fees! unless order.before_payment_state?
|
||||
order.update_order!
|
||||
end
|
||||
|
||||
def create_line_item_fees!
|
||||
order.line_items.includes(variant: :product).each do |line_item|
|
||||
if provided_by_order_cycle? line_item
|
||||
calculator.create_line_item_adjustments_for line_item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_order_fees!
|
||||
return unless order_cycle
|
||||
|
||||
calculator.create_order_adjustments_for order
|
||||
end
|
||||
|
||||
def tax_enterprise_fees!
|
||||
Spree::TaxRate.adjust(order, order.all_adjustments.enterprise_fee)
|
||||
end
|
||||
|
||||
def update_line_item_fees!(line_item)
|
||||
line_item.adjustments.enterprise_fee.each do |fee|
|
||||
fee.update_adjustment!(line_item, force: true)
|
||||
end
|
||||
end
|
||||
|
||||
def update_order_fees!
|
||||
order.adjustments.enterprise_fee.where(adjustable_type: 'Spree::Order').each do |fee|
|
||||
fee.update_adjustment!(order, force: true)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def calculator
|
||||
@calculator ||= OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle)
|
||||
end
|
||||
|
||||
def provided_by_order_cycle?(line_item)
|
||||
@order_cycle_variant_ids ||= order_cycle&.variants&.map(&:id) || []
|
||||
@order_cycle_variant_ids.include? line_item.variant_id
|
||||
end
|
||||
end
|
||||
end
|
||||
37
app/services/orders/mask_data_service.rb
Normal file
37
app/services/orders/mask_data_service.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Orders
|
||||
class MaskDataService
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def call
|
||||
mask_customer_names unless customer_names_allowed?
|
||||
mask_contact_data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_accessor :order
|
||||
|
||||
def customer_names_allowed?
|
||||
order.distributor.show_customer_names_to_suppliers
|
||||
end
|
||||
|
||||
def mask_customer_names
|
||||
order.bill_address&.assign_attributes(firstname: I18n.t('admin.reports.hidden'),
|
||||
lastname: "")
|
||||
order.ship_address&.assign_attributes(firstname: I18n.t('admin.reports.hidden'),
|
||||
lastname: "")
|
||||
end
|
||||
|
||||
def mask_contact_data
|
||||
order.bill_address&.assign_attributes(phone: "", address1: "", address2: "",
|
||||
city: "", zipcode: "", state: nil)
|
||||
order.ship_address&.assign_attributes(phone: "", address1: "", address2: "",
|
||||
city: "", zipcode: "", state: nil)
|
||||
order.assign_attributes(email: I18n.t('admin.reports.hidden'))
|
||||
end
|
||||
end
|
||||
end
|
||||
141
app/services/orders/sync_service.rb
Normal file
141
app/services/orders/sync_service.rb
Normal file
@@ -0,0 +1,141 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Responsible for ensuring that any updates to a Subscription are propagated to any
|
||||
# orders belonging to that Subscription which have been instantiated
|
||||
|
||||
module Orders
|
||||
class SyncService
|
||||
attr_reader :order_update_issues
|
||||
|
||||
def initialize(subscription)
|
||||
@subscription = subscription
|
||||
@order_update_issues = Orders::UpdateIssuesService.new
|
||||
@line_item_syncer = LineItemSyncer.new(subscription, order_update_issues)
|
||||
end
|
||||
|
||||
def sync!
|
||||
orders_in_order_cycles_not_closed.all? do |order|
|
||||
order.assign_attributes(customer_id:, email: customer&.email,
|
||||
distributor_id: shop_id)
|
||||
update_associations_for(order)
|
||||
line_item_syncer.sync!(order)
|
||||
order.update_order!
|
||||
order.save
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :subscription, :line_item_syncer
|
||||
|
||||
delegate :orders, :bill_address, :ship_address, :subscription_line_items, to: :subscription
|
||||
delegate :shop_id, :customer, :customer_id, to: :subscription
|
||||
delegate :shipping_method, :shipping_method_id,
|
||||
:payment_method, :payment_method_id, to: :subscription
|
||||
delegate :shipping_method_id_changed?, :shipping_method_id_was, to: :subscription
|
||||
delegate :payment_method_id_changed?, :payment_method_id_was, to: :subscription
|
||||
|
||||
def update_associations_for(order)
|
||||
update_bill_address_for(order) if bill_address.changes.keys.intersect?(relevant_address_attrs)
|
||||
update_shipment_for(order) if shipping_method_id_changed?
|
||||
update_ship_address_for(order)
|
||||
update_payment_for(order) if payment_method_id_changed?
|
||||
end
|
||||
|
||||
def orders_in_order_cycles_not_closed
|
||||
return @orders_in_order_cycles_not_closed unless @orders_in_order_cycles_not_closed.nil?
|
||||
|
||||
@orders_in_order_cycles_not_closed = orders.joins(:order_cycle).
|
||||
merge(OrderCycle.not_closed).readonly(false)
|
||||
end
|
||||
|
||||
def update_bill_address_for(order)
|
||||
unless addresses_match?(order.bill_address, bill_address)
|
||||
return order_update_issues.add(order, I18n.t('bill_address'))
|
||||
end
|
||||
|
||||
order.bill_address.update(bill_address.attributes.slice(*relevant_address_attrs))
|
||||
end
|
||||
|
||||
def update_payment_for(order)
|
||||
payment = order.payments.
|
||||
with_state('checkout').where(payment_method_id: payment_method_id_was).last
|
||||
if payment
|
||||
payment&.void_transaction!
|
||||
order.payments.create(payment_method_id:, amount: order.reload.total)
|
||||
else
|
||||
unless order.payments.with_state('checkout').where(payment_method_id:).any?
|
||||
order_update_issues.add(order, I18n.t('admin.payment_method'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_shipment_for(order)
|
||||
return if pending_shipment_with?(order, shipping_method_id) # No need to do anything.
|
||||
|
||||
if pending_shipment_with?(order, shipping_method_id_was)
|
||||
order.select_shipping_method(shipping_method_id)
|
||||
else
|
||||
order_update_issues.add(order, I18n.t('admin.shipping_method'))
|
||||
end
|
||||
end
|
||||
|
||||
def update_ship_address_for(order)
|
||||
# The conditions here are to achieve the same behaviour in earlier versions of Spree, where
|
||||
# switching from pick-up to delivery affects whether simultaneous changes to shipping address
|
||||
# are ignored or not.
|
||||
pickup_to_delivery = force_ship_address_required?(order)
|
||||
if (!pickup_to_delivery || order.shipment.present?) &&
|
||||
ship_address.changes.keys.intersect?(relevant_address_attrs)
|
||||
save_ship_address_in_order(order)
|
||||
end
|
||||
return unless !pickup_to_delivery || order.shipment.blank?
|
||||
|
||||
order.updater.shipping_address_from_distributor
|
||||
end
|
||||
|
||||
def relevant_address_attrs
|
||||
["firstname", "lastname", "address1", "zipcode", "city", "state_id", "country_id", "phone"]
|
||||
end
|
||||
|
||||
def addresses_match?(order_address, subscription_address)
|
||||
relevant_address_attrs.all? do |attr|
|
||||
order_address[attr] == subscription_address.public_send("#{attr}_was") ||
|
||||
order_address[attr] == subscription_address[attr]
|
||||
end
|
||||
end
|
||||
|
||||
def ship_address_updatable?(order)
|
||||
return true if force_ship_address_required?(order)
|
||||
return false unless order.shipping_method.require_ship_address?
|
||||
return true if addresses_match?(order.ship_address, ship_address)
|
||||
|
||||
order_update_issues.add(order, I18n.t('ship_address'))
|
||||
false
|
||||
end
|
||||
|
||||
# This returns true when the shipping method on the subscription has changed
|
||||
# to a delivery (ie. a shipping address is required) AND the existing shipping
|
||||
# address on the order matches the shop's address
|
||||
def force_ship_address_required?(order)
|
||||
return false unless shipping_method.require_ship_address?
|
||||
|
||||
distributor_address = order.address_from_distributor
|
||||
relevant_address_attrs.all? do |attr|
|
||||
order.ship_address[attr] == distributor_address[attr]
|
||||
end
|
||||
end
|
||||
|
||||
def save_ship_address_in_order(order)
|
||||
return unless ship_address_updatable?(order)
|
||||
|
||||
order.ship_address.update(ship_address.attributes.slice(*relevant_address_attrs))
|
||||
end
|
||||
|
||||
def pending_shipment_with?(order, shipping_method_id)
|
||||
return false unless order.shipment.present? && order.shipment.state == "pending"
|
||||
|
||||
order.shipping_method.id == shipping_method_id
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/services/orders/update_issues_service.rb
Normal file
23
app/services/orders/update_issues_service.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Wrapper for a hash of issues encountered by instances of Orders::SyncService and LineItemSyncer
|
||||
# Used to report issues to the user when they attempt to update a subscription
|
||||
|
||||
module Orders
|
||||
class UpdateIssuesService
|
||||
def initialize
|
||||
@issues = {}
|
||||
end
|
||||
|
||||
delegate :[], :keys, to: :issues
|
||||
|
||||
def add(order, issue)
|
||||
@issues[order.id] ||= []
|
||||
@issues[order.id] << issue
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :issues
|
||||
end
|
||||
end
|
||||
98
app/services/orders/workflow_service.rb
Normal file
98
app/services/orders/workflow_service.rb
Normal file
@@ -0,0 +1,98 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Orders
|
||||
class WorkflowService
|
||||
attr_reader :order
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
def complete
|
||||
advance_to_state("complete", advance_order_options)
|
||||
end
|
||||
|
||||
def complete!
|
||||
advance_order!(advance_order_options)
|
||||
end
|
||||
|
||||
def next(options = {})
|
||||
result = advance_order_one_step
|
||||
|
||||
after_transition_hook(options)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def advance_to_payment
|
||||
return unless order.before_payment_state?
|
||||
|
||||
advance_to_state("payment", advance_order_options)
|
||||
end
|
||||
|
||||
def advance_checkout(options = {})
|
||||
advance_to = order.before_payment_state? ? "payment" : "confirmation"
|
||||
|
||||
advance_to_state(advance_to, advance_order_options.merge(options))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def advance_order_options
|
||||
shipping_method_id = order.shipping_method.id if order.shipping_method.present?
|
||||
{ "shipping_method_id" => shipping_method_id }
|
||||
end
|
||||
|
||||
def advance_to_state(target_state, options = {})
|
||||
until order.state == target_state
|
||||
break unless order.next
|
||||
|
||||
after_transition_hook(options)
|
||||
end
|
||||
|
||||
order.state == target_state
|
||||
end
|
||||
|
||||
def advance_order!(options)
|
||||
until order.completed?
|
||||
order.next!
|
||||
after_transition_hook(options)
|
||||
end
|
||||
end
|
||||
|
||||
def advance_order_one_step
|
||||
tries ||= 3
|
||||
order.next
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
retry unless (tries -= 1).zero?
|
||||
false
|
||||
end
|
||||
|
||||
def after_transition_hook(options)
|
||||
if order.state == "delivery"
|
||||
order.select_shipping_method(options["shipping_method_id"])
|
||||
end
|
||||
|
||||
persist_all_payments if order.state == "payment"
|
||||
end
|
||||
|
||||
# When a payment fails, the order state machine stays in 'payment'
|
||||
# and rollbacks all transactions
|
||||
# This rollback also reverts the payment state from 'failed', 'void' or 'invalid' to 'pending'
|
||||
# Despite the rollback, the in-memory payment still has the correct state, so we persist it
|
||||
def persist_all_payments
|
||||
order.payments.each do |payment|
|
||||
in_memory_payment_state = payment.state
|
||||
if different_from_db_payment_state?(in_memory_payment_state, payment.id)
|
||||
payment.reload.update(state: in_memory_payment_state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Verifies if the in-memory payment state is different from the one stored in the database
|
||||
# This is be done without reloading the payment so that in-memory data is not changed
|
||||
def different_from_db_payment_state?(in_memory_payment_state, payment_id)
|
||||
in_memory_payment_state != Spree::Payment.find(payment_id).state
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class OrdersBulkCancelService
|
||||
def initialize(params, current_user)
|
||||
@order_ids = params[:bulk_ids]
|
||||
@current_user = current_user
|
||||
@send_cancellation_email = params[:send_cancellation_email]
|
||||
@restock_items = params[:restock_items]
|
||||
end
|
||||
|
||||
def call
|
||||
editable_orders.where(id: @order_ids).each do |order|
|
||||
order.send_cancellation_email = @send_cancellation_email
|
||||
order.restock_items = @restock_items
|
||||
order.cancel
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def editable_orders
|
||||
Permissions::Order.new(@current_user).editable_orders
|
||||
end
|
||||
end
|
||||
@@ -87,7 +87,7 @@ class PlaceProxyOrder
|
||||
end
|
||||
|
||||
def move_to_completion
|
||||
OrderWorkflow.new(order).complete!
|
||||
Orders::WorkflowService.new(order).complete!
|
||||
end
|
||||
|
||||
def send_placement_email
|
||||
|
||||
@@ -58,7 +58,7 @@ class ProcessPaymentIntent
|
||||
def process_payment
|
||||
return unless order.process_payments!
|
||||
|
||||
OrderWorkflow.new(order).complete
|
||||
Orders::WorkflowService.new(order).complete
|
||||
end
|
||||
|
||||
def ready_for_capture?
|
||||
|
||||
@@ -64,7 +64,7 @@ class ProductsRenderer
|
||||
end
|
||||
|
||||
def distributed_products
|
||||
OrderCycleDistributedProducts.new(distributor, order_cycle, customer)
|
||||
OrderCycles::DistributedProductsService.new(distributor, order_cycle, customer)
|
||||
end
|
||||
|
||||
def products_order
|
||||
@@ -89,10 +89,12 @@ class ProductsRenderer
|
||||
@variants_for_shop ||= begin
|
||||
scoper = OpenFoodNetwork::ScopeVariantToHub.new(distributor)
|
||||
|
||||
# rubocop:disable Rails/FindEach # .each returns an array, .find_each returns nil
|
||||
distributed_products.variants_relation.
|
||||
includes(:default_price, :stock_locations, :product).
|
||||
where(product_id: products).
|
||||
each { |v| scoper.scope(v) } # Scope results with variant_overrides
|
||||
# rubocop:enable Rails/FindEach
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ module Shop
|
||||
def self.ready_for_checkout_for(distributor, customer)
|
||||
new(distributor, customer).call.select do |order_cycle|
|
||||
order = Spree::Order.new(distributor:, order_cycle:)
|
||||
OrderAvailablePaymentMethods.new(order, customer).to_a.any? &&
|
||||
OrderAvailableShippingMethods.new(order, customer).to_a.any?
|
||||
Orders::AvailablePaymentMethodsService.new(order, customer).to_a.any? &&
|
||||
Orders::AvailableShippingMethodsService.new(order, customer).to_a.any?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
7
app/views/admin/dfc_product_imports/index.html.haml
Normal file
7
app/views/admin/dfc_product_imports/index.html.haml
Normal file
@@ -0,0 +1,7 @@
|
||||
- content_for :page_title do
|
||||
#{t(".title")}
|
||||
|
||||
= render partial: 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
%p= t(".imported_products")
|
||||
= @count
|
||||
16
app/views/admin/product_import/_dfc_import_form.html.haml
Normal file
16
app/views/admin/product_import/_dfc_import_form.html.haml
Normal file
@@ -0,0 +1,16 @@
|
||||
%h3= t(".title")
|
||||
%br
|
||||
|
||||
= form_with url: main_app.admin_dfc_product_imports_path, method: :get do |form|
|
||||
= form.label :enterprise_id, t(".enterprise")
|
||||
%span.required *
|
||||
%br
|
||||
= form.select :enterprise_id, options_from_collection_for_select(@producers, :id, :name, @producers.first&.id), { "data-controller": "tom-select", class: "primary" }
|
||||
%br
|
||||
%br
|
||||
= form.label :catalog_url, t(".catalog_url")
|
||||
%br
|
||||
= form.text_field :catalog_url, size: 60
|
||||
%br
|
||||
%br
|
||||
= form.submit t(".import")
|
||||
@@ -13,3 +13,5 @@
|
||||
%br
|
||||
|
||||
= render 'upload_form'
|
||||
|
||||
= render 'dfc_import_form' if spree_current_user.oidc_account.present?
|
||||
|
||||
1
app/webpacker/channels/scoped_channel.js
Normal file
1
app/webpacker/channels/scoped_channel.js
Normal file
@@ -0,0 +1 @@
|
||||
// ScopedChannel is created with a specific ID in ../controllers/scoped_channel_controller.js
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user