Compare commits

...

106 Commits

Author SHA1 Message Date
Maikel Linke
42520216aa Update all locales with the latest Transifex translations 2024-03-22 15:34:25 +11:00
Maikel
ce24e6ecd6 Merge pull request #12257 from openfoodfoundation/dependabot/npm_and_yarn/hotwired/turbo-8.0.4
chore(deps): bump @hotwired/turbo from 8.0.3 to 8.0.4
2024-03-22 15:32:41 +11:00
Maikel
4c1268b3ce Merge pull request #12274 from mkllnk/dfc-product-import
Import products from DFC catalog
2024-03-22 09:25:02 +11:00
dependabot[bot]
3455ffd507 chore(deps): bump @hotwired/turbo from 8.0.3 to 8.0.4
Bumps [@hotwired/turbo](https://github.com/hotwired/turbo) from 8.0.3 to 8.0.4.
- [Release notes](https://github.com/hotwired/turbo/releases)
- [Commits](https://github.com/hotwired/turbo/compare/v8.0.3...v8.0.4)

---
updated-dependencies:
- dependency-name: "@hotwired/turbo"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-21 09:57:20 +00:00
Konrad
26f3b5603d Merge pull request #12246 from dacook/cable_ready-downgrade
Downgrade cable_ready JS to 5.0.1
2024-03-21 10:54:15 +01:00
Maikel
50acf2f484 Merge pull request #12299 from mkllnk/social-media-links
Publish full URLs of social media links on DFC API
2024-03-21 13:20:35 +11:00
Maikel Linke
220e459da2 Publish full URLs of social media links on DFC API
We have a quirky way of storing social media links in our database. The
saved format results from the UI, validations and overridden getter
methods.
2024-03-21 12:16:10 +11:00
David Cook
4ada3edc4e Merge pull request #12293 from Pauloparakleto/readme-node-advise
chore(README.md): change the order the instalation guide appears and add advise about specific ruby and node versions.
2024-03-21 09:41:10 +11:00
David Cook
579965c62c Merge pull request #12289 from anthonyms/11482-fix-rubocop-rails-issue-find_each
Fix Rubocop Rails issue: Rails/FindEach
2024-03-21 09:36:18 +11:00
Pauloparakleto
e85e606667 chore(GETTING_STARTED.md): remove mention to git and aditional steps when mentioning docker alternative. Let docker section be its job 2024-03-20 18:35:40 -03:00
Konrad
b4b8e99c7b Merge pull request #12271 from abdellani/set-variant-processor-to-mini_magick
set variant_processor to mini_magick
2024-03-20 18:20:45 +01:00
Pauloparakleto
2d8cd2b1a5 chore(GETTING_STARTED.md): remove redundant advise about rbenv and node version 2024-03-20 11:43:43 -03:00
Pauloparakleto
1e826e8308 chore(GETTING_STARTED.md): close parentheses 2024-03-20 11:37:52 -03:00
Pauloparakleto
d81fc44597 chore(GETTING_STARTED.md): change instruction to nodenv, make it mandatory. 2024-03-20 11:03:16 -03:00
Pauloparakleto
cb47624702 chore(GETTING_STARTED.md): fix spelling 2024-03-20 10:48:02 -03:00
Pauloparakleto
ccdd428b57 chore(GETTING_STARTED.md): Mention docker at the bottom of the section preventing the contributor about aditional steps 2024-03-20 10:45:24 -03:00
Pauloparakleto
eb7e65a707 chore(GETTING_STARTED.md): remove mention to RVM 2024-03-20 10:33:08 -03:00
Anthony Musyoki
25e3f30f97 Fix Rubocop Rails issue: Rails/FindEach 2024-03-20 15:34:30 +03:00
Konrad
214f7ec23c Merge pull request #12229 from cyrillefr/Decreasing-the-quantity-of-an-item-does-not-update-enterprise-fees-per-item
[BO Orders] Update Entreprise fees when decreasing quantity
2024-03-20 11:53:06 +01:00
Maikel
b679a20f23 Merge pull request #12285 from dacook/comments
Add placeholder file with comments
2024-03-20 14:26:48 +11:00
Gaetan Craig-Riou
eb2213bd10 Merge pull request #12287 from openfoodfoundation/dependabot/bundler/aws-sdk-s3-1.146.0
chore(deps): bump aws-sdk-s3 from 1.145.0 to 1.146.0
2024-03-20 09:57:55 +11:00
Gaetan Craig-Riou
b07bf9989a Merge pull request #12288 from openfoodfoundation/dependabot/bundler/rspec-rails-6.1.2
chore(deps-dev): bump rspec-rails from 6.1.1 to 6.1.2
2024-03-20 09:57:12 +11:00
Pauloparakleto
13f387e0a4 chore(README.md): change the order the instalation guide appears. Make clear ruby and node versions must be checked bebore running the script 2024-03-19 16:17:27 -03:00
dependabot[bot]
fc24a830a5 chore(deps-dev): bump rspec-rails from 6.1.1 to 6.1.2
Bumps [rspec-rails](https://github.com/rspec/rspec-rails) from 6.1.1 to 6.1.2.
- [Changelog](https://github.com/rspec/rspec-rails/blob/main/Changelog.md)
- [Commits](https://github.com/rspec/rspec-rails/compare/v6.1.1...v6.1.2)

---
updated-dependencies:
- dependency-name: rspec-rails
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-19 09:35:00 +00:00
dependabot[bot]
dd86222391 chore(deps): bump aws-sdk-s3 from 1.145.0 to 1.146.0
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.145.0 to 1.146.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-19 09:34:23 +00:00
David Cook
794f92d9f5 Add placeholder file with comments
I was surprised to find there's a channel defined by our app code, because it's not defined in the standard place.
2024-03-19 15:06:23 +11:00
David Cook
e061dbb86b Merge pull request #12284 from mkllnk/logo-size-for-dfc
Limit enterprise image sizes on DFC API
2024-03-19 14:51:24 +11:00
Maikel Linke
526069dbb3 Limit enterprise image sizes on DFC API
Uploaded images can be several MB in size. While offering the big size
would enable other apps to resize it and store the image size they need,
we have only one app using it in practice and it's using the image
directly. It's much simpler and if a default size will work for others
in the future then why not just serve that.

We can revise this in the future. There is a DFC discussion about
publishing several sizes which I started:
https://github.com/datafoodconsortium/ontology/discussions/77#discussioncomment-8228094
2024-03-19 12:26:23 +11:00
David Cook
acb53a6ddc Merge pull request #12273 from mkllnk/rubocop
Rubocop
2024-03-19 10:14:01 +11:00
Gaetan Craig-Riou
b623ecab26 Merge pull request #12275 from openfoodfoundation/dependabot/bundler/stripe-10.12.0
chore(deps): bump stripe from 10.11.0 to 10.12.0
2024-03-19 10:05:43 +11:00
Gaetan Craig-Riou
476825251d Merge pull request #12279 from openfoodfoundation/dependabot/bundler/aws-sdk-s3-1.145.0
chore(deps): bump aws-sdk-s3 from 1.144.0 to 1.145.0
2024-03-19 09:41:43 +11:00
Gaetan Craig-Riou
0d17230dd2 Merge pull request #12278 from openfoodfoundation/dependabot/bundler/shoulda-matchers-6.2.0
chore(deps-dev): bump shoulda-matchers from 6.1.0 to 6.2.0
2024-03-19 09:35:00 +11:00
Gaetan Craig-Riou
73eeaaabc2 Update Stripe API recordings for new version 2024-03-19 09:33:07 +11:00
dependabot[bot]
cad1140b18 chore(deps): bump stripe from 10.11.0 to 10.12.0
Bumps [stripe](https://github.com/stripe/stripe-ruby) from 10.11.0 to 10.12.0.
- [Release notes](https://github.com/stripe/stripe-ruby/releases)
- [Changelog](https://github.com/stripe/stripe-ruby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-ruby/compare/v10.11.0...v10.12.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-19 09:33:07 +11:00
Konrad
67a5aa6877 Merge pull request #12207 from abdellani/11673-update-invoice-generate-route
fix route to Admin#order#invoice#generate
2024-03-18 19:18:00 +01:00
dependabot[bot]
c4fa936f15 chore(deps): bump aws-sdk-s3 from 1.144.0 to 1.145.0
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.144.0 to 1.145.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 09:37:59 +00:00
dependabot[bot]
ad7d19a0be chore(deps-dev): bump shoulda-matchers from 6.1.0 to 6.2.0
Bumps [shoulda-matchers](https://github.com/thoughtbot/shoulda-matchers) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/thoughtbot/shoulda-matchers/releases)
- [Changelog](https://github.com/thoughtbot/shoulda-matchers/blob/main/CHANGELOG.md)
- [Commits](https://github.com/thoughtbot/shoulda-matchers/compare/v6.1.0...v6.2.0)

---
updated-dependencies:
- dependency-name: shoulda-matchers
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 09:36:35 +00:00
David Cook
69df56ae76 Merge pull request #12247 from dacook/buu/broadcast-queue-12020
[BUU] Enqueue actions to perform at end of reflex
2024-03-18 11:38:21 +11:00
David Cook
63549b3dca Add comment
I still don't know why the morph method doesn't work in this context..
2024-03-18 11:17:15 +11:00
David Cook
8a84e0084f Enqueue cable_ready actions to perform at end of reflex
I think this resolves [this discussion](https://github.com/openfoodfoundation/openfoodnetwork/pull/11163#discussion_r1260531844)

I guess we just didn't know [how it works](https://docs.stimulusreflex.com/guide/cableready.html#order-of-operations) before..
2024-03-18 11:16:38 +11:00
Gaetan Craig-Riou
6dc0988933 Merge pull request #12276 from openfoodfoundation/dependabot/bundler/activerecord-import-1.6.0
chore(deps): bump activerecord-import from 1.5.1 to 1.6.0
2024-03-18 11:11:35 +11:00
Gaetan Craig-Riou
e2a53b57d4 Merge pull request #12272 from openfoodfoundation/dependabot/npm_and_yarn/follow-redirects-1.15.6
chore(deps): bump follow-redirects from 1.15.4 to 1.15.6
2024-03-18 11:09:14 +11:00
Gaetan Craig-Riou
3c3f65c271 Merge pull request #12256 from feruzoripov/services/group
Group `Order && OrderCycle` related services
2024-03-18 11:07:22 +11:00
Feruz Oripov
bdcb0856af Fix failed specs 2024-03-16 19:23:17 +05:00
Feruz Oripov
778ed46d50 cops 2024-03-16 19:14:18 +05:00
Feruz Oripov
9d919938f3 Group Order && OrderCycle related services and specs 2024-03-16 19:07:08 +05:00
dependabot[bot]
8858ed86ac chore(deps): bump activerecord-import from 1.5.1 to 1.6.0
Bumps [activerecord-import](https://github.com/zdennis/activerecord-import) from 1.5.1 to 1.6.0.
- [Changelog](https://github.com/zdennis/activerecord-import/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zdennis/activerecord-import/compare/v1.5.1...v1.6.0)

---
updated-dependencies:
- dependency-name: activerecord-import
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-15 10:05:17 +00:00
Maikel Linke
8efc215a14 Include product submenu on product import confirmation 2024-03-15 16:46:41 +11:00
Maikel Linke
d2d2db8489 Assign random product category on import if missing
Failing in this case may be desired in some circumstances but most of
the time we want compatibility and easy interoperability even when not
all data matches.
2024-03-15 16:46:41 +11:00
Maikel Linke
3af7fa7521 Offer nice select box for enterprise id 2024-03-15 16:46:41 +11:00
Maikel Linke
b5c47b099e Store semantic link when importing DFC products 2024-03-15 16:46:41 +11:00
Maikel Linke
d47d3eba8f Add SemanticLink model for variants
We want to link variants/products to external DFC SuppliedProducts to
trigger supplier orders when local stock is exhausted. This is the first
step to enable the link.
2024-03-15 16:46:41 +11:00
Maikel Linke
2e101c5fe6 Refresh OIDC token and try again
Access tokens are only valid for half an hour. So if requesting a DFC
API fails, it's likely due to an expired token and we refresh it.
2024-03-15 16:46:41 +11:00
Maikel Linke
1c09b5d16c Move DFC API request logic to service object
I'm planning to add more to it.
2024-03-15 16:46:41 +11:00
Maikel Linke
d6da52929f Allow local DFC import in development 2024-03-15 16:46:41 +11:00
Maikel Linke
30e8f9eb28 Importing products from DFC catalog
Technical demonstration of a complete product export-import roundtrip
which we could now do between OFN instances.
2024-03-15 16:46:41 +11:00
Maikel Linke
7abe455899 Exclude false Style/RedundantLineContinuation file
The current Rubocop version flags good code as bad. Regenerating the
todo file added it there and when the issue is fixed it will disappear
from our generated todo list as well.
2024-03-15 12:52:09 +11:00
Maikel Linke
931ee2f9d2 Style Style/RedundantLineContinuation 2024-03-15 12:40:00 +11:00
Maikel Linke
477336c660 Style RSpec/NotToNot 2024-03-15 12:17:48 +11:00
Maikel Linke
96ccea3691 Add controller to handle import of DFC products
It's not doing anything yet, but this is the basic setup.
2024-03-15 11:40:10 +11:00
dependabot[bot]
c6a83588fe chore(deps): bump follow-redirects from 1.15.4 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-14 23:38:41 +00:00
Maikel
fcef8e8d7d Merge pull request #12269 from openfoodfoundation/dependabot/bundler/aws-sdk-s3-1.144.0
chore(deps): bump aws-sdk-s3 from 1.143.1 to 1.144.0
2024-03-15 10:38:11 +11:00
Maikel
696559200f Merge pull request #12267 from cyrillefr/BUU-Improve-Product-Search-Keywords
Add tests to the search product feature
2024-03-15 10:36:07 +11:00
Mohamed ABDELLANI
c497b37452 set variant_processor to mini_magick
Seem like the default variant processor become vips.

https://github.com/openfoodfoundation/openfoodnetwork/actions/runs/8280341472/job/22656656944?pr=12209
https://github.com/rails/rails/issues/44211

The comment on the source code doesn't seem to be updated
4bb7323341/activestorage/app/models/active_storage/variant.rb (L11)
2024-03-15 00:00:05 +01:00
Konrad
acc52cc45f Merge pull request #11918 from abdellani/11896-prevent_bulk_printing_sending_when_ABN_not_set
Bulk printing/sending should show warning if ABN is required but not set.
2024-03-14 13:33:41 +01:00
Mohamed ABDELLANI
38b832cec2 fix route to admin#order#invoice#generate 2024-03-14 13:00:10 +01:00
cyrillefr
261cb2d81b Requested changes: trimming number of examples
- to improve speed of testing
2024-03-14 10:55:09 +01:00
dependabot[bot]
f59c43b8e9 chore(deps): bump aws-sdk-s3 from 1.143.1 to 1.144.0
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.143.1 to 1.144.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-14 09:19:04 +00:00
Gaetan Craig-Riou
29dad44f9f Update all locales with the latest Transifex translations 2024-03-14 10:03:59 +11:00
David Cook
c4a8bf2490 Merge pull request #12254 from mkllnk/dfc-create-variant
Add spree_product_uri to SuppliedProduct
2024-03-14 10:02:01 +11:00
Maikel
82c444b8d8 Merge pull request #12266 from openfoodfoundation/dependabot/bundler/aws-sdk-s3-1.143.1
chore(deps): bump aws-sdk-s3 from 1.143.0 to 1.143.1
2024-03-14 08:19:28 +11:00
Maikel
d7fca66433 Merge pull request #12265 from openfoodfoundation/dependabot/bundler/rails-i18n-7.0.9
chore(deps): bump rails-i18n from 7.0.8 to 7.0.9
2024-03-14 08:18:52 +11:00
cyrillefr
43d13253e7 Add tests to the search product feature 2024-03-13 10:54:06 +01:00
dependabot[bot]
84551068ee chore(deps): bump aws-sdk-s3 from 1.143.0 to 1.143.1
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.143.0 to 1.143.1.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-13 09:19:46 +00:00
dependabot[bot]
163c45d301 chore(deps): bump rails-i18n from 7.0.8 to 7.0.9
Bumps [rails-i18n](https://github.com/svenfuchs/rails-i18n) from 7.0.8 to 7.0.9.
- [Changelog](https://github.com/svenfuchs/rails-i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenfuchs/rails-i18n/compare/v7.0.8...v7.0.9)

---
updated-dependencies:
- dependency-name: rails-i18n
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-13 09:18:26 +00:00
Maikel
63a1931ce8 Merge pull request #12249 from mkllnk/test-users
Set known default password for sample users
2024-03-13 15:41:30 +11:00
David Cook
98457b2fff Merge pull request #12248 from mkllnk/rubocop-precommit
Add dev script to run rubocop on changed files
2024-03-13 14:59:29 +11:00
Gaetan Craig-Riou
e072823071 Merge pull request #12259 from openfoodfoundation/dependabot/bundler/rubocop-1.62.1
chore(deps-dev): bump rubocop from 1.60.2 to 1.62.1
2024-03-13 09:09:48 +11:00
Maikel
d253effc29 Fix typo in spec description
Co-authored-by: Gaetan Craig-Riou <40413322+rioug@users.noreply.github.com>
2024-03-12 16:32:01 +11:00
Maikel
331894017a Merge pull request #12252 from filipefurtad0/bump_stripe_10.11.0
Bumps Stripe from 10.10.0 to 10.11.0
2024-03-12 13:55:13 +11:00
Maikel Linke
1674d8ab5c Simplify DFC product controller 2024-03-12 13:11:31 +11:00
Maikel Linke
85a47e61fd Create variants only for own products 2024-03-12 13:11:31 +11:00
Gaetan Craig-Riou
a4b7a8f95d Spec creating variant via DFC API 2024-03-12 12:43:10 +11:00
Gaetan Craig-Riou
462c763cd1 Add spree_product_uri to SuppliedProduct
Also update SuppliedProductBuilder and specs
2024-03-12 12:43:10 +11:00
Gaetan Craig-Riou
4f77ad40a3 Update recording with new filtering 2024-03-12 12:17:21 +11:00
Gaetan Craig-Riou
a33eb80f56 Fix filtering of sensible data
* Hide Stripe Client User Agent header, it contains the hostname of
the machine generating the cassettes
* Hide client_secret
2024-03-12 12:17:15 +11:00
Maikel
fb8c86a9a7 Merge pull request #12251 from openfoodfoundation/dependabot/bundler/i18n-1.14.4
chore(deps): bump i18n from 1.14.3 to 1.14.4
2024-03-12 11:49:15 +11:00
dependabot[bot]
b53be15fda chore(deps-dev): bump rubocop from 1.60.2 to 1.62.1
Bumps [rubocop](https://github.com/rubocop/rubocop) from 1.60.2 to 1.62.1.
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.60.2...v1.62.1)

---
updated-dependencies:
- dependency-name: rubocop
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 23:47:51 +00:00
Gaetan Craig-Riou
f755aff23c Merge pull request #12225 from openfoodfoundation/dependabot/bundler/rubocop-rails-2.24.0
chore(deps-dev): bump rubocop-rails from 2.23.1 to 2.24.0
2024-03-12 10:45:02 +11:00
Gaetan Craig-Riou
ff52a66e75 Merge pull request #12250 from mkllnk/not-to-not
Enforce RSpec expect(..).not_to over to_not
2024-03-12 10:35:16 +11:00
dependabot[bot]
2cac8471fc chore(deps-dev): bump rubocop-rails from 2.23.1 to 2.24.0
Bumps [rubocop-rails](https://github.com/rubocop/rubocop-rails) from 2.23.1 to 2.24.0.
- [Release notes](https://github.com/rubocop/rubocop-rails/releases)
- [Changelog](https://github.com/rubocop/rubocop-rails/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop-rails/compare/v2.23.1...v2.24.0)

---
updated-dependencies:
- dependency-name: rubocop-rails
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-08 00:41:31 +00:00
filipefurtad0
5653d542f6 Hides sensitive data 2024-03-07 15:15:18 +00:00
filipefurtad0
7f3953882d Update Stripe API recordings for new version 2024-03-07 14:49:11 +00:00
filipefurtad0
f126b8b316 Update Stripe API recordings for new version 2024-03-07 14:41:40 +00:00
filipefurtad0
7849f30f46 Update Stripe API recordings for new version 2024-03-07 14:20:16 +00:00
dependabot[bot]
b82496b8a1 chore(deps): bump i18n from 1.14.3 to 1.14.4
Bumps [i18n](https://github.com/ruby-i18n/i18n) from 1.14.3 to 1.14.4.
- [Release notes](https://github.com/ruby-i18n/i18n/releases)
- [Changelog](https://github.com/ruby-i18n/i18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ruby-i18n/i18n/compare/v1.14.3...v1.14.4)

---
updated-dependencies:
- dependency-name: i18n
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-07 09:31:25 +00:00
Maikel Linke
bd6b0ddbf3 Enforce RSpec expect(..).not_to over to_not 2024-03-07 16:57:54 +11:00
Maikel Linke
4a423e3275 Set known default password for sample users
This enables us to easily log in as one of the sample users to test
functionality as enterprise user or customer instead of admin.
2024-03-07 16:25:35 +11:00
Maikel Linke
1472749da8 Add dev script to run rubocop on changed files 2024-03-07 13:43:52 +11:00
David Cook
27d1a9ee09 Downgrade cable_ready JS to 5.0.1
In order to match the gem version

I don't know if I'm using yarn wrong, but it wanted to install a newer version alongside this version, in order to resolve 'cable_ready@^5.0.0'. I manually edited the lockfile and yarn install now works as expected.
2024-03-07 10:32:28 +11:00
cyrillefr
45f4a06263 [BO Orders] Update Ent. fees on item qty decreasing 2024-03-04 21:08:51 +01:00
Mohamed ABDELLANI
8370a5fed0 fix existing tests 2024-02-19 11:00:55 +01:00
Mohamed ABDELLANI
64b42b1284 improve all_distributors_can_invoice? 2024-02-19 11:00:41 +01:00
Mohamed ABDELLANI
f582bffbc5 remove assertions before tests 2024-02-19 09:13:58 +01:00
Mohamed ABDELLANI
b669b804c4 update tests 2024-02-19 09:13:58 +01:00
Mohamed ABDELLANI
6e9089ad47 check ABN before bulk printing 2024-02-19 09:13:58 +01:00
446 changed files with 7381 additions and 6392 deletions

View File

@@ -14,3 +14,4 @@ SITE_URL="test.host"
OPENID_APP_ID="test-provider"
OPENID_APP_SECRET="12345"
OPENID_REFRESH_TOKEN="dummy-refresh-token"

View File

@@ -19,3 +19,6 @@ Capybara/NegationMatcher:
RSpec/ExpectChange:
Enabled: true
EnforcedStyle: block
RSpec/NotToNot:
Enabled: true

View File

@@ -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.

View File

@@ -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
```

View File

@@ -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)

View 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

View File

@@ -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|

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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!

View File

@@ -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)

View File

@@ -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|
{

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -175,7 +175,7 @@ class OrderCycle < ApplicationRecord
end
def clone!
OrderCycleClone.new(self).create
OrderCycles::CloneService.new(self).create
end
def variants

View File

@@ -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

View 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

View File

@@ -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,

View File

@@ -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?

View File

@@ -48,7 +48,7 @@ module Spree
end
def capture_and_complete_order!
OrderWorkflow.new(order).complete!
Orders::WorkflowService.new(order).complete!
capture!
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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?

View File

@@ -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

View File

@@ -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

View 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

View 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")

View File

@@ -13,3 +13,5 @@
%br
= render 'upload_form'
= render 'dfc_import_form' if spree_current_user.oidc_account.present?

View 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