Compare commits

...

39 Commits

Author SHA1 Message Date
David Cook
788457618f Check ship address required based on all available methods
This check was implemented based on 'allowed' shipping methods, but we need to revert that logic. So for now, we can check all 'available' shipping methods.

This could potentially result in the same query being run twice, because load_shipping_methods also loads it. I opted to keep things simple and not try to optimise here.
2023-03-17 13:06:45 +11:00
David Cook
ef607da2c1 Revert "Fix: Customers can checkout with non-matching shipping and product categories" 2023-03-17 10:40:53 +11:00
Filipe
9ea6fa5c44 Merge pull request #9687 from dacook/9616-order-cycle-open-webhook
Add webhook triggered on Order Cycle Open
2023-03-16 17:24:22 +00:00
Konrad
a945f8f72f Merge pull request #10522 from mkllnk/flipper-upgrade
Bump flipper* from 0.20.4 to 0.26.0
2023-03-16 17:24:33 +01:00
jibees
0c3ee2e8fc Merge pull request #10574 from openfoodfoundation/dependabot/npm_and_yarn/jasmine-core-4.6.0
Bump jasmine-core from 4.5.0 to 4.6.0
2023-03-16 14:59:33 +01:00
dependabot[bot]
f6458e91c2 Bump jasmine-core from 4.5.0 to 4.6.0
Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/jasmine/jasmine/releases)
- [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md)
- [Commits](https://github.com/jasmine/jasmine/compare/v4.5.0...v4.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-16 09:59:04 +00:00
David Cook
4757b82a80 Update all locales with the latest Transifex translations 2023-03-16 14:15:04 +11:00
David Cook
ad9e82e973 Merge pull request #10569 from openfoodfoundation/dependabot/bundler/sidekiq-7.0.7
Bump sidekiq from 7.0.6 to 7.0.7
2023-03-16 11:47:55 +11:00
Filipe
e8430eae6d Merge pull request #10460 from jibees/10400-pagination-in-bom-not-aggregating-results-from-quick-search-field
BOM: Add a search input that search for items with pagination
2023-03-15 16:31:32 +00:00
dependabot[bot]
584b013a49 Bump sidekiq from 7.0.6 to 7.0.7
Bumps [sidekiq](https://github.com/sidekiq/sidekiq) from 7.0.6 to 7.0.7.
- [Release notes](https://github.com/sidekiq/sidekiq/releases)
- [Changelog](https://github.com/sidekiq/sidekiq/blob/main/Changes.md)
- [Commits](https://github.com/sidekiq/sidekiq/compare/v7.0.6...v7.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-15 10:05:08 +00:00
Maikel
f15cb01f2a Merge pull request #10520 from mkllnk/return-auth-controller-spec
Correctly include order id in return auth spec for Rails 7 compatibility
2023-03-15 14:08:16 +11:00
Maikel Linke
f1a5c46685 Remove unnecessary Flipper memoizer config
Auto-config, yay.
2023-03-15 12:54:03 +11:00
Maikel Linke
00f2f92ce0 Remove unnecessary Flipper active_record config
This is now done automatically when including flipper-active_record in
the Gemfile.
2023-03-15 12:54:03 +11:00
Maikel Linke
fdd71cff51 Remove now unnecessary flipper_id method
Flipper does it for us.
2023-03-15 12:54:02 +11:00
Maikel Linke
c9ca020f05 Bump flipper* from 0.20.4 to 0.26.0 2023-03-15 12:54:02 +11:00
David Cook
d59074dabd Tidy up spec
The best way to check if something changed or not, is with 'change' of course.
2023-03-15 12:18:17 +11:00
jibees
6a874b9527 Merge pull request #10562 from openfoodfoundation/dependabot/bundler/rails-6.1.7.3
Bump rails from 6.1.7.2 to 6.1.7.3
2023-03-14 15:02:49 +01:00
dependabot[bot]
1f08da207f Bump rails from 6.1.7.2 to 6.1.7.3
Bumps [rails](https://github.com/rails/rails) from 6.1.7.2 to 6.1.7.3.
- [Release notes](https://github.com/rails/rails/releases)
- [Commits](https://github.com/rails/rails/compare/v6.1.7.2...v6.1.7.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-14 09:59:50 +00:00
jibees
687d4593fb Merge pull request #10550 from openfoodfoundation/dependabot/bundler/database_cleaner-2.0.2
Bump database_cleaner from 2.0.1 to 2.0.2
2023-03-13 21:12:42 +01:00
dependabot[bot]
b62f88512f Bump database_cleaner from 2.0.1 to 2.0.2
Bumps [database_cleaner](https://github.com/DatabaseCleaner/database_cleaner) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/DatabaseCleaner/database_cleaner/releases)
- [Changelog](https://github.com/DatabaseCleaner/database_cleaner/blob/main/History.rdoc)
- [Commits](https://github.com/DatabaseCleaner/database_cleaner/compare/v2.0.1...v2.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-13 15:24:41 +00:00
David Cook
9d5ca2255b Apply suggestions from code review
Co-authored-by: Maikel <maikel@email.org.au>
2023-03-07 15:38:50 +11:00
David Cook
00a823b2fc 6. Add webhook endpoints to user developer settings screen
Allowing creation and deleting via the user association.
It probably won't be much effort to allow editing and multiple records, but I cut it down to the minimum needed to avoid any further delays.

I couldn't find a way to test a failure in the destroy method, but decided to keep the condition because I thought it was worth having.
2023-03-07 15:38:50 +11:00
David Cook
3d81a6e280 Prevent creating duplicate webhook notifications [migration]
Using the clever concurrency testing borrowed from SubscriptionPlacementJob, but I thought a shorter pause time (just 100ms) would be sufficient.

I considered doing this with a new 'state' field (upcoming/open/close), but decided to keep it simple.
2023-03-07 15:38:50 +11:00
David Cook
739df4be01 4. OrderCycleOpenedJob triggers webhook 2023-03-07 15:38:50 +11:00
David Cook
b91cabc510 Also send webhook payloads for distributor owners
But not supplier owners.
2023-03-07 15:38:50 +11:00
David Cook
ba152f12ee 3. Add OrderCycleWebhookService to create webhook payloads for an order cycle event 2023-03-07 15:38:50 +11:00
David Cook
778baba118 User may have many WebhookEndpoints [migration]
Although we won't be allowing multiple in the this PR, we certainly plan to in the future.

The migration helper add_reference couldn't handle the custom column name, so I had to put it together manually.
2023-03-07 15:38:50 +11:00
David Cook
85c98c6d3e 2. Add model WebhookEndpoint [migration]
This will store the URL for each user that wants a notification.

We probably don't need URL validation (it's not done on Enterprise for example). It could be validated by browser input, and anyway will be validated if the webhook actually works or not.

Inspired by Keygen: https://keygen.sh/blog/how-to-build-a-webhook-system-in-rails-using-sidekiq/
2023-03-07 15:38:50 +11:00
David Cook
de9546587a Prevent webhooks to private addresses (SSRF) [add gem]
Best reviewed with whitespace hidden.

Unfortunately the spec isn't allowed in CI. But it worked on my environment, I promise.
I chose `xit` so that it doesn't run unnecessarily. Perhaps we could use `pending` instead, which would execute, and notify us if it suddenly started working one day. But I doubt it.
2023-03-07 15:38:50 +11:00
David Cook
9741935955 Raise error on server error
And thus retry later.
I tried to test that it actually retries, or ensuring the job remained in the queue to be retried, but couldn't get it to work.
2023-03-07 15:38:50 +11:00
David Cook
9d19f37fec 1. Add WebhookDeliveryJob
This job is responsible for delivering a payload for one webhook event only. It allows the action to run asynchronously (and not slow down the calling process).
2023-03-07 15:38:50 +11:00
David Cook
718ac0ab80 Add Faraday for making HTTP requests [add gem]
It's the most popular and flexible option, so should be able to cater for our future needs best.
2023-03-07 15:38:47 +11:00
Maikel Linke
797b98d686 Remove Rails 5.0 controller spec workaround
We added a method to work around a bug. But that's not needed any more.
2023-03-07 08:46:36 +11:00
Maikel Linke
3dc3ebe584 Correctly include order id in return auth spec
The route to update a return authorization must include the order number
as id:

    /admin/orders/:order_id/return_authorizations/:id(.:format)

The spec only worked because the controller's ivars were not reset
between requests and the order was already set. But Rails 7 will reset
the ivars and it failed finding the order without a given id.
2023-03-07 08:46:36 +11:00
Jean-Baptiste Bellet
ac739108a2 Improve readability by generating search string for ransack 2023-03-06 11:31:36 +01:00
Jean-Baptiste Bellet
50bc48c96f Introduce some specs around searching in BOM 2023-03-01 14:54:46 +01:00
Jean-Baptiste Bellet
8ad532c41a Can search bill_address:phone, firstname, lastname and distributor:name 2023-03-01 14:54:46 +01:00
Jean-Baptiste Bellet
ebd5d706c2 Add search input
Actually this only search for `order_email` or `order_number` or `product_name` or `supplier_name`

+ Improve display by reduce each columns width
2023-03-01 14:40:12 +01:00
Jean-Baptiste Bellet
8658b1a743 Remove quick filter that search only on displayed line items
+ remove specs as well
2023-02-28 15:39:31 +01:00
44 changed files with 937 additions and 206 deletions

View File

@@ -136,6 +136,9 @@ gem 'view_component_reflex', '3.1.14.pre9'
gem 'mini_portile2', '~> 2.8'
gem "faraday"
gem "private_address_check"
group :production, :staging do
gem 'ddtrace'
gem 'rack-timeout'

View File

@@ -44,42 +44,42 @@ GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.1.0)
actioncable (6.1.7.2)
actionpack (= 6.1.7.2)
activesupport (= 6.1.7.2)
actioncable (6.1.7.3)
actionpack (= 6.1.7.3)
activesupport (= 6.1.7.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.2)
actionpack (= 6.1.7.2)
activejob (= 6.1.7.2)
activerecord (= 6.1.7.2)
activestorage (= 6.1.7.2)
activesupport (= 6.1.7.2)
actionmailbox (6.1.7.3)
actionpack (= 6.1.7.3)
activejob (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
mail (>= 2.7.1)
actionmailer (6.1.7.2)
actionpack (= 6.1.7.2)
actionview (= 6.1.7.2)
activejob (= 6.1.7.2)
activesupport (= 6.1.7.2)
actionmailer (6.1.7.3)
actionpack (= 6.1.7.3)
actionview (= 6.1.7.3)
activejob (= 6.1.7.3)
activesupport (= 6.1.7.3)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.1.7.2)
actionview (= 6.1.7.2)
activesupport (= 6.1.7.2)
actionpack (6.1.7.3)
actionview (= 6.1.7.3)
activesupport (= 6.1.7.3)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionpack-action_caching (1.2.2)
actionpack (>= 4.0.0)
actiontext (6.1.7.2)
actionpack (= 6.1.7.2)
activerecord (= 6.1.7.2)
activestorage (= 6.1.7.2)
activesupport (= 6.1.7.2)
actiontext (6.1.7.3)
actionpack (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
nokogiri (>= 1.8.5)
actionview (6.1.7.2)
activesupport (= 6.1.7.2)
actionview (6.1.7.3)
activesupport (= 6.1.7.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@@ -91,19 +91,19 @@ GEM
activemodel (>= 5.2.0)
activestorage (>= 5.2.0)
activesupport (>= 5.2.0)
activejob (6.1.7.2)
activesupport (= 6.1.7.2)
activejob (6.1.7.3)
activesupport (= 6.1.7.3)
globalid (>= 0.3.6)
activemerchant (1.123.0)
activesupport (>= 4.2)
builder (>= 2.1.2, < 4.0.0)
i18n (>= 0.6.9)
nokogiri (~> 1.4)
activemodel (6.1.7.2)
activesupport (= 6.1.7.2)
activerecord (6.1.7.2)
activemodel (= 6.1.7.2)
activesupport (= 6.1.7.2)
activemodel (6.1.7.3)
activesupport (= 6.1.7.3)
activerecord (6.1.7.3)
activemodel (= 6.1.7.3)
activesupport (= 6.1.7.3)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activerecord-postgresql-adapter (0.0.1)
@@ -114,14 +114,14 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 2.0.8, < 3)
railties (>= 5.2.4.1)
activestorage (6.1.7.2)
actionpack (= 6.1.7.2)
activejob (= 6.1.7.2)
activerecord (= 6.1.7.2)
activesupport (= 6.1.7.2)
activestorage (6.1.7.3)
actionpack (= 6.1.7.3)
activejob (= 6.1.7.3)
activerecord (= 6.1.7.3)
activesupport (= 6.1.7.3)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.7.2)
activesupport (6.1.7.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -224,9 +224,9 @@ GEM
cuprite (0.14.3)
capybara (~> 3.0)
ferrum (~> 0.13.0)
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.0.0)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.1.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
@@ -283,15 +283,17 @@ GEM
websocket-driver (>= 0.6, < 0.8)
ffaker (2.21.0)
ffi (1.15.5)
flipper (0.20.4)
flipper-active_record (0.20.4)
activerecord (>= 5.0, < 7)
flipper (~> 0.20.4)
flipper-ui (0.20.4)
flipper (0.26.0)
concurrent-ruby (< 2)
flipper-active_record (0.26.0)
activerecord (>= 4.2, < 8)
flipper (~> 0.26.0)
flipper-ui (0.26.0)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 0.20.4)
flipper (~> 0.26.0)
rack (>= 1.4, < 3)
rack-protection (>= 1.5.3, < 2.2.0)
rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7)
fog-aws (2.0.1)
fog-core (~> 1.38)
fog-json (~> 1.0)
@@ -402,7 +404,7 @@ GEM
mini_portile2 (2.8.1)
mini_racer (0.6.3)
libv8-node (~> 16.10.0.0)
minitest (5.17.0)
minitest (5.18.0)
monetize (1.12.0)
money (~> 6.12)
money (6.16.0)
@@ -474,6 +476,7 @@ GEM
ttfunk
pg (1.2.3)
power_assert (2.0.2)
private_address_check (0.5.0)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
@@ -482,7 +485,7 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.6.2)
rack (2.2.6.3)
rack (2.2.6.4)
rack-mini-profiler (2.3.4)
rack (>= 1.2.0)
rack-oauth2 (1.21.3)
@@ -491,7 +494,7 @@ GEM
httpclient
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (2.1.0)
rack-protection (3.0.5)
rack
rack-proxy (0.7.6)
rack
@@ -499,20 +502,20 @@ GEM
rack-test (2.0.2)
rack (>= 1.3)
rack-timeout (0.6.3)
rails (6.1.7.2)
actioncable (= 6.1.7.2)
actionmailbox (= 6.1.7.2)
actionmailer (= 6.1.7.2)
actionpack (= 6.1.7.2)
actiontext (= 6.1.7.2)
actionview (= 6.1.7.2)
activejob (= 6.1.7.2)
activemodel (= 6.1.7.2)
activerecord (= 6.1.7.2)
activestorage (= 6.1.7.2)
activesupport (= 6.1.7.2)
rails (6.1.7.3)
actioncable (= 6.1.7.3)
actionmailbox (= 6.1.7.3)
actionmailer (= 6.1.7.3)
actionpack (= 6.1.7.3)
actiontext (= 6.1.7.3)
actionview (= 6.1.7.3)
activejob (= 6.1.7.3)
activemodel (= 6.1.7.3)
activerecord (= 6.1.7.3)
activestorage (= 6.1.7.3)
activesupport (= 6.1.7.3)
bundler (>= 1.15.0)
railties (= 6.1.7.2)
railties (= 6.1.7.3)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@@ -532,9 +535,9 @@ GEM
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
rails_safe_tasks (1.0.0)
railties (6.1.7.2)
actionpack (= 6.1.7.2)
activesupport (= 6.1.7.2)
railties (6.1.7.3)
actionpack (= 6.1.7.3)
activesupport (= 6.1.7.3)
method_source
rake (>= 12.2)
thor (~> 1.0)
@@ -549,7 +552,7 @@ GEM
ffi (~> 1.0)
redcarpet (3.6.0)
redis (4.8.1)
redis-client (0.13.0)
redis-client (0.14.0)
connection_pool
regexp_parser (2.7.0)
reline (0.3.2)
@@ -631,6 +634,9 @@ GEM
rubyzip (2.3.2)
rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6)
sanitize (6.0.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
sass (3.4.25)
sass-rails (5.0.8)
railties (>= 5.2.0)
@@ -642,7 +648,7 @@ GEM
semantic_range (3.0.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.0.6)
sidekiq (7.0.7)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
@@ -801,6 +807,7 @@ DEPENDENCIES
digest
dotenv-rails
factory_bot_rails (= 6.2.0)
faraday
ffaker
flipper
flipper-active_record
@@ -843,6 +850,7 @@ DEPENDENCIES
paypal-sdk-merchant (= 1.117.2)
pdf-reader
pg (~> 1.2.3)
private_address_check
pry (~> 0.13.0)
puma
rack-mini-profiler (< 3.0.0)

View File

@@ -17,7 +17,14 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
]
$scope.page = 1
$scope.per_page = $scope.per_page_options[0].id
searchThrough = ["order_distributor_name",
"order_bill_address_phone",
"order_bill_address_firstname",
"order_bill_address_lastname",
"variant_product_supplier_name",
"order_email",
"order_number",
"product_name"].join("_or_") + "_cont"
$scope.confirmRefresh = ->
LineItems.allSaved() || confirm(t("unsaved_changes_warning"))
@@ -26,7 +33,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
$scope.distributorFilter = ''
$scope.supplierFilter = ''
$scope.orderCycleFilter = ''
$scope.quickSearch = ''
$scope.query = ''
$scope.startDate = undefined
$scope.endDate = undefined
event = new CustomEvent('flatpickr:clear')
@@ -60,6 +67,7 @@ angular.module("admin.lineItems").controller 'LineItemsCtrl', ($scope, $timeout,
[formattedStartDate, formattedEndDate] = $scope.formatDates($scope.startDate, $scope.endDate)
RequestMonitor.load LineItems.index(
"q[#{searchThrough}]": $scope.query,
"q[order_state_not_eq]": "canceled",
"q[order_shipment_state_not_eq]": "shipped",
"q[order_completed_at_not_null]": "true",

View File

@@ -15,9 +15,7 @@ module CheckoutCallbacks
prepend_before_action :require_distributor_chosen
before_action :load_order, :associate_user, :load_saved_addresses, :load_saved_credit_cards
before_action :allowed_shipping_methods, if: -> {
params[:step] == "details"
}
before_action :load_shipping_methods, if: -> { params[:step] == "details" }
before_action :ensure_order_not_completed
before_action :ensure_checkout_allowed
@@ -48,22 +46,8 @@ module CheckoutCallbacks
@selected_card = nil
end
def allowed_shipping_methods
@allowed_shipping_methods ||= sorted_available_shipping_methods.filter(
&method(:supports_all_products_shipping_categories?)
)
end
def sorted_available_shipping_methods
available_shipping_methods.sort { |a, b| a.name.casecmp(b.name) }
end
def supports_all_products_shipping_categories?(shipping_method)
(products_shipping_categories - shipping_method.shipping_categories.pluck(:id)).empty?
end
def products_shipping_categories
@products_shipping_categories ||= @order.products.pluck(:shipping_category_id).uniq
def load_shipping_methods
@shipping_methods = available_shipping_methods.sort { |a, b| a.name.casecmp(b.name) }
end
def redirect_to_shop?

View File

@@ -24,7 +24,7 @@ class SplitCheckoutController < ::BaseController
check_step if params[:step]
recalculate_tax if params[:step] == "summary"
flash_error_when_no_shipping_method_available if allowed_shipping_methods.none?
flash_error_when_no_shipping_method_available if available_shipping_methods.none?
end
def update
@@ -168,7 +168,7 @@ class SplitCheckoutController < ::BaseController
end
def shipping_method_ship_address_not_required?
selected_shipping_method = allowed_shipping_methods&.select do |sm|
selected_shipping_method = available_shipping_methods&.select do |sm|
sm.id.to_s == params[:shipping_method_id]
end

View File

@@ -0,0 +1,47 @@
# frozen_string_literal: true
class WebhookEndpointsController < ::BaseController
before_action :load_resource, only: :destroy
def create
webhook_endpoint = spree_current_user.webhook_endpoints.new(webhook_endpoint_params)
if webhook_endpoint.save
flash[:success] = t('.success')
else
flash[:error] = t('.error')
end
redirect_to redirect_path
end
def destroy
if @webhook_endpoint.destroy
flash[:success] = t('.success')
else
flash[:error] = t('.error')
end
redirect_to redirect_path
end
def load_resource
@webhook_endpoint = spree_current_user.webhook_endpoints.find(params[:id])
end
def webhook_endpoint_params
params.require(:webhook_endpoint).permit(:url)
end
def redirect_path
if request.referer.blank? || request.referer.include?(spree.account_path)
developer_settings_path
else
request.referer
end
end
def developer_settings_path
"#{spree.account_path}#/developer_settings"
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
# Trigger jobs for any order cycles that recently opened
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')
end
mark_as_opened(recently_opened_order_cycles)
end
end
private
def recently_opened_order_cycles
@recently_opened_order_cycles ||= OrderCycle
.where(opened_at: nil)
.where(orders_open_at: 1.hour.ago..Time.zone.now)
.lock.order(:id)
end
def mark_as_opened(order_cycles)
now = Time.zone.now
order_cycles.update_all(opened_at: now, updated_at: now)
end
end

View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true
require "faraday"
require "private_address_check"
require "private_address_check/tcpsocket_ext"
# Deliver a webhook payload
# As a delayed job, it can run asynchronously and handle retries.
class WebhookDeliveryJob < ApplicationJob
# General failed request error that we're going to use to signal
# the job runner to retry our webhook worker.
class FailedWebhookRequestError < StandardError; end
queue_as :default
def perform(url, event, payload)
body = {
id: job_id,
at: Time.zone.now.to_s,
event: event,
data: payload,
}
# Request user-submitted url, preventing any private connections being made
# (SSRF).
# This method may allow the socket to open, but is necessary in order to
# protect from TOC/TOU.
# Note that private_address_check provides some methods for pre-validating,
# but they're not as comprehensive and so unnecessary here. Simply
# momentarily opening sockets probably can't cause DoS or other damage.
PrivateAddressCheck.only_public_connections do
notify_endpoint(url, body)
end
end
def notify_endpoint(url, body)
connection = Faraday.new(
request: { timeout: 30 },
headers: {
'User-Agent' => 'openfoodnetwork_webhook/1.0',
'Content-Type' => 'application/json',
}
)
response = connection.post(url, body.to_json)
# Raise a failed request error and let job runner handle retrying.
# In theory, only 5xx errors should be retried, but who knows.
raise FailedWebhookRequestError, response.status.to_s unless response.success?
end
end

View File

@@ -17,7 +17,7 @@ class Enterprise < ApplicationRecord
}.freeze
VALID_INSTAGRAM_REGEX = %r{\A[a-zA-Z0-9._]{1,30}([^/-]*)\z}
searchable_attributes :sells, :is_primary_producer
searchable_attributes :sells, :is_primary_producer, :name
searchable_associations :properties
searchable_scopes :is_primary_producer, :is_distributor, :is_hub, :activated, :visible,
:ready_for_checkout, :not_ready_for_checkout

View File

@@ -34,6 +34,7 @@ class OrderCycle < ApplicationRecord
attr_accessor :incoming_exchanges, :outgoing_exchanges
before_update :reset_opened_at, if: :will_save_change_to_orders_open_at?
before_update :reset_processed_at, if: :will_save_change_to_orders_close_at?
after_save :sync_subscriptions, if: :opening?
@@ -333,6 +334,14 @@ class OrderCycle < ApplicationRecord
errors.add(:orders_close_at, :after_orders_open_at)
end
def reset_opened_at
# Reset only if order cycle is opening again at a later date
return unless orders_open_at.present? && orders_open_at_was.present?
return unless orders_open_at > orders_open_at_was
self.opened_at = nil
end
def reset_processed_at
return unless orders_close_at.present? && orders_close_at_was.present?
return unless orders_close_at > orders_close_at_was

View File

@@ -4,7 +4,7 @@ module Spree
class Address < ApplicationRecord
include AddressDisplay
searchable_attributes :firstname, :lastname
searchable_attributes :firstname, :lastname, :phone
searchable_associations :country, :state
belongs_to :country, class_name: "Spree::Country"

View File

@@ -14,7 +14,7 @@ module Spree
searchable_attributes :number, :state, :shipment_state, :payment_state, :distributor_id,
:order_cycle_id, :email, :total, :customer_id
searchable_associations :shipping_method, :bill_address
searchable_associations :shipping_method, :bill_address, :distributor
searchable_scopes :complete, :incomplete
checkout_flow do

View File

@@ -38,7 +38,10 @@ module Spree
has_many :customers
has_many :credit_cards
has_many :report_rendering_options, class_name: "::ReportRenderingOptions", dependent: :destroy
has_many :webhook_endpoints, dependent: :destroy
accepts_nested_attributes_for :enterprise_roles, allow_destroy: true
accepts_nested_attributes_for :webhook_endpoints
accepts_nested_attributes_for :bill_address
accepts_nested_attributes_for :ship_address
@@ -148,10 +151,6 @@ module Spree
spree_orders.incomplete.where(created_by_id: id).order('created_at DESC').first
end
def flipper_id
"#{self.class.name};#{id}"
end
def disabled
disabled_at.present?
end

View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
# Records a webhook url to send notifications to
class WebhookEndpoint < ApplicationRecord
validates :url, presence: true
end

View File

@@ -0,0 +1,21 @@
# 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

@@ -15,7 +15,10 @@ module PermittedAttributes
private
def permitted_attributes
[:email, :password, :password_confirmation, :disabled]
[
:email, :password, :password_confirmation, :disabled,
{ webhook_endpoints_attributes: [:id, :url] },
]
end
end
end

View File

@@ -76,8 +76,8 @@
- display_ship_address = false
- ship_method_description = nil
- selected_shipping_method ||= @allowed_shipping_methods[0].id if @allowed_shipping_methods.length == 1
- @allowed_shipping_methods.each do |shipping_method|
- selected_shipping_method ||= @shipping_methods[0].id if @shipping_methods.length == 1
- @shipping_methods.each do |shipping_method|
- ship_method_is_selected = shipping_method.id == selected_shipping_method.to_i
%div.checkout-input.checkout-input-radio
= fields_for shipping_method do |shipping_method_form|

View File

@@ -18,22 +18,28 @@
%input.red{ type: "button", value: "Save Changes", ng: { click: "submit()", disabled: "!bulk_order_form.$dirty" } }
%legend{ align: 'center'}= t(:search)
%div{ :class => "sixteen columns alpha" }
.filter_select{ :class => "four columns" }
.quick_search.three.columns.alpha
%label{ for: 'quick_filter' }
%br
%input.quick-search.fullwidth{ ng: {model: 'query'}, name: "quick_filter", type: 'text', placeholder: t('admin.quick_search'), "ng-keypress" => "$event.keyCode === 13 && fetchResults()" }
.one.columns
&nbsp;
.filter_select{ :class => "three columns" }
%label{ :for => 'supplier_filter' }
= t("admin.producer")
%br
%input#supplier_filter.ofn-select2.fullwidth{ type: 'number', 'min-search' => 5, data: 'suppliers', placeholder: "#{t(:all)}", blank: "{ id: '', name: '#{t(:all)}' }", on: { selecting: "confirmRefresh" }, ng: { model: 'supplierFilter' } }
.filter_select{ :class => "four columns" }
.filter_select{ :class => "three columns" }
%label{ :for => 'distributor_filter' }
= t("admin.shop")
%br
%input#distributor_filter.ofn-select2.fullwidth{ type: 'number', 'min-search' => 5, data: 'distributors', placeholder: "#{t(:all)}", blank: "{ id: '', name: '#{t(:all)}' }", on: { selecting: "confirmRefresh" }, ng: { model: 'distributorFilter' } }
.filter_select{ :class => "four columns" }
.filter_select{ :class => "three columns" }
%label{ :for => 'order_cycle_filter' }
= t("admin.order_cycle")
%br
%input#order_cycle_filter.ofn-select2.fullwidth{ type: 'number', 'min-search' => 5, data: 'orderCycles', placeholder: "#{t(:all)}", blank: "{ id: '', name: '#{t(:all)}' }", on: { selecting: "confirmRefresh" }, ng: { model: 'orderCycleFilter' } }
.date_filter{class: "four columns"}
.date_filter{class: "three columns"}
%label
= t("date_range")
%br
@@ -97,8 +103,6 @@
.clear
%div{ ng: { hide: 'RequestMonitor.loading || line_items.length == 0' }, style: "display: flex; justify-content: flex-start; column-gap: 10px; margin-bottom: 15px" }
%div{ style: "flex-grow: 1" }
%input.fullwidth{ :type => "text", :id => 'quick_search', 'ng-model' => 'quickSearch', :placeholder => 'Quick Search' }
-# This -20px is a hack to make the dropdowns align properly
%div{ style: "margin-right: -20px;" }
= render 'admin/shared/bulk_actions_dropdown'
@@ -162,7 +166,7 @@
= "#{t('admin.price')} (#{Spree::Money.currency_symbol})"
%th.actions
%tr.line_item{ ng: { repeat: "line_item in filteredLineItems = ( line_items | filter:quickSearch | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:sorting.predicate:sorting.reverse )", 'class-even' => "'even'", 'class-odd' => "'odd'", attr: { id: "li_{{line_item.id}}" } } }
%tr.line_item{ ng: { repeat: "line_item in filteredLineItems = ( line_items | variantFilter:selectedUnitsProduct:selectedUnitsVariant:sharedResource | orderBy:sorting.predicate:sorting.reverse )", 'class-even' => "'even'", 'class-odd' => "'odd'", attr: { id: "li_{{line_item.id}}" } } }
%td.bulk
%input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'line_item.checked', 'ignore-dirty' => true }
%td.order_no{ 'ng-show' => 'columns.order_no.visible' } {{ line_item.order.number }}

View File

@@ -1,3 +1,4 @@
%script{ type: "text/ng-template", id: "account/developer_settings.html" }
%h3= t('.title')
= render partial: 'api_keys'
= render partial: 'webhook_endpoints'

View File

@@ -0,0 +1,33 @@
%section{ id: "webhook_endpoints" }
%hr
%h3= t('.title')
%p= t('.description')
%table{width: "100%"}
%thead
%tr
%th= t('.event_type.header')
%th= t('.url.header')
%th.actions
%tbody
-# Existing endpoints
- @user.webhook_endpoints.each do |webhook_endpoint|
%tr
%td= t('.event_types.order_cycle_opened') # For now, we only support one type.
%td= webhook_endpoint.url
%td.actions
- if webhook_endpoint.persisted?
= button_to account_webhook_endpoint_path(webhook_endpoint), method: :delete,
class: "tiny alert no-margin",
data: { confirm: I18n.t(:are_you_sure)} do
= I18n.t(:delete)
-# Create new
- if @user.webhook_endpoints.empty? # Only one allowed for now.
%tr
%td= t('.event_types.order_cycle_opened') # For now, we only support one type.
%td
= form_for(@user.webhook_endpoints.build, url: account_webhook_endpoints_path, id: 'new_webhook_endpoint') do |f|
= f.url_field :url, placeholder: t('.url.create_placeholder'), required: true, size: 64
%td.actions
= button_tag t(:create), class: 'button primary tiny no-margin', form: 'new_webhook_endpoint'

View File

@@ -1,14 +1,5 @@
require "flipper"
require "flipper/adapters/active_record"
require "flipper/instrumentation/log_subscriber"
Flipper.configure do |config|
config.default do
adapter = Flipper::Adapters::ActiveRecord.new
instrumented = Flipper::Adapters::Instrumented.new(adapter, instrumenter: ActiveSupport::Notifications)
Flipper.new(instrumented, instrumenter: ActiveSupport::Notifications)
end
end
if Rails.env.production?
Flipper::UI.configure do |config|
@@ -17,6 +8,4 @@ if Rails.env.production?
end
end
Rails.configuration.middleware.use Flipper::Middleware::Memoizer, preload_all: true
Flipper.register(:admins) { |actor| actor.respond_to?(:admin?) && actor.admin? }

View File

@@ -3565,6 +3565,14 @@ See the %{link} to find out more about %{sitename}'s features and to start using
previous: "Previous"
last: "Last"
webhook_endpoints:
create:
success: Webhook endpoint successfully created
error: Webhook endpoint failed to create
destroy:
success: Webhook endpoint successfully deleted
error: Webhook endpoint failed to delete
spree:
order_updated: "Order Updated"
add_country: "Add country"
@@ -4360,6 +4368,16 @@ See the %{link} to find out more about %{sitename}'s features and to start using
api_keys:
regenerate_key: "Regenerate Key"
title: API key
webhook_endpoints:
title: Webhook Endpoints
description: Events in the system may trigger webhooks to external systems.
event_types:
order_cycle_opened: Order Cycle Opened
event_type:
header: Event type
url:
header: Endpoint URL
create_placeholder: Enter the URL of the remote webhook endpoint
developer_settings:
title: Developer Settings
form:

View File

@@ -6,12 +6,22 @@ en_CA:
spree/shipping_method: Shipping/Pick-up Method
attributes:
spree/order/ship_address:
address1: "Shipping address (House number + Street)"
address2: "Shipping address line 2"
city: "Shipping address city"
country: "Shipping address country"
phone: "Phone number"
firstname: "First name"
lastname: "Last name"
zipcode: "Shipping address postal code"
spree/order/bill_address:
address1: "Billing address (House number & Street)"
zipcode: "Billing address Postal Code"
city: "Billing address City"
country: "Billing address country"
firstname: "Billing address first name"
lastname: "Billing address last name"
phone: Customer phone
spree/user:
password: "Password"
password_confirmation: "Password confirmation"
@@ -1313,6 +1323,7 @@ en_CA:
total_by_customer: Total by Customer
total_by_supplier: Total By Supplier
supplier_totals: Order Cycle Supplier Totals
percentage: "%{value} %"
supplier_totals_by_distributor: Order Cycle Supplier Totals by Distributor
totals_by_supplier: Order Cycle Distributor Totals by Supplier
customer_totals: Order Cycle Customer Totals
@@ -1323,6 +1334,8 @@ en_CA:
addresses: Addresses
payment_methods: Payment Methods Report
delivery: Delivery Report
sales_tax_totals_by_producer: Sales Tax Totals by Producer
sales_tax_totals_by_order: Sales Tax Totals by Order
tax_types: Tax Types
tax_rates: Tax Rates
pack_by_customer: Pack By Customer
@@ -2671,6 +2684,7 @@ en_CA:
report_header_tax_on_delivery: "Tax on Delivery (%{currency_symbol})"
report_header_tax_on_fees: "Tax on Fees (%{currency_symbol})"
report_header_tax_category: "Tax Category"
report_header_tax_rate_name: "Tax Rate Name"
report_header_tax_rate: "Tax Rate"
report_header_total_tax: "Total Tax (%{currency_symbol})"
report_header_total_excl_tax: "Total excl. tax (%{currency_symbol})"
@@ -2694,6 +2708,7 @@ en_CA:
report_header_supplier: Supplier
report_header_producer: Producer
report_header_producer_suburb: Producer City/Town
report_header_producer_tax_status: Producer Tax Status
report_header_producer_charges_sales_tax?: Tax registered
report_header_unit: Unit
report_header_group_buy_unit_quantity: Group Buy Unit Quantity
@@ -2710,6 +2725,7 @@ en_CA:
report_header_distributor_address: Distributor address
report_header_distributor_city: Distributor city
report_header_distributor_postcode: Distributor postal code
report_header_distributor_tax_status: Distributor Tax Status
report_header_delivery_address: Delivery Address
report_header_delivery_postcode: Delivery Postal Code
report_header_bulk_unit_size: Bulk Unit Size
@@ -3083,6 +3099,9 @@ en_CA:
cancel_the_order_send_cancelation_email: "Send a cancellation email to the customer."
restock_item: "Restock Items: Return this item to stock"
restock_items: "Restock Items: Return all items to stock"
delete_line_items_html:
one: "This will delete one line item from the order. <br />Are you sure you want to proceed?"
other: "This will delete %{count} line items from the order.<br />Are you sure you want to proceed?"
resend_user_email_confirmation:
resend: "Resend"
sending: "Resend..."

View File

@@ -6,9 +6,22 @@ fr_CA:
spree/shipping_method: Option d'expédition
attributes:
spree/order/ship_address:
address1: "Adresse de livraison (Numéro et Rue)"
address2: "Adresse (Numéro et rue)"
city: "Adresse de livraison - Ville"
country: "Pays"
phone: "Numéro de téléphone"
firstname: "Prénom"
lastname: "Nom de famille"
zipcode: "Code postal"
spree/order/bill_address:
address1: "Adresse de facturation (Numéro et rue)"
zipcode: "Adresse de facturation - Code postal"
city: "Adresse de facturation - Ville"
country: "Adresse de facturation - Pays"
firstname: "Adresse de facturation - Prénom"
lastname: "Adresse de facturation - Nom"
phone: Téléphone
spree/user:
password: "Mot de passe"
password_confirmation: "Confirmation du mot de passe"
@@ -409,8 +422,11 @@ fr_CA:
filters:
categories:
title: Conditions de transport
selected_categories: "%{count} catégories sélectionnées"
producers:
title: Producteurs
selected_producers: "%{count} producteurs sélectionnés"
per_page: "%{count} éléments par page"
colums: Colonnes
columns:
name: Nom
@@ -666,6 +682,7 @@ fr_CA:
not_found: n'a pas été trouvé dans la base de donnée
category_not_found: n'est pas conforme aux catégories utilisées. Merci de modifier les catégories en utilisant celles listées sur la page d'import ou vérifier qu'il n'y ait pas de faute de frappe ou d'espace à fin du mot.
not_updatable: ne peut pas être mis à jour pour des produits existants via la fonctionnalité d'import de fichier produits
values_must_be_same: doit être identique pour les produits avec un nom identique
blank: Champ obligatoire
products_no_permission: vous n'avez pas les droits requis pour gérer les produits de cette entreprise
inventory_no_permission: Vous n'avez pas la permission de créer un catalogue boutique pour ce producteur
@@ -1309,6 +1326,7 @@ fr_CA:
total_by_customer: Total par acheteur
total_by_supplier: Total par producteur
supplier_totals: Totaux Cycle de Vente par Producteur
percentage: "%{value}%"
supplier_totals_by_distributor: Totaux Cycle de Vente par Producteur pour chaque Hub Distributeur
totals_by_supplier: Totaux Cycle de Vente par Hub Distributeur pour chaque Producteur
customer_totals: Totaux Cycle de Vente par Acheteur
@@ -1319,6 +1337,8 @@ fr_CA:
addresses: Adresses
payment_methods: Rapport Méthodes de Paiement
delivery: Rapport de Livraison
sales_tax_totals_by_producer: Détail des montants de taxes par producteur
sales_tax_totals_by_order: Détail des montants de taxes par commande
tax_types: Type de taxe
tax_rates: Taux de taxe
pack_by_customer: Préparation des commandes par Acheteur
@@ -1497,6 +1517,10 @@ fr_CA:
stripe_connect_fail: Désolé, la connexion de votre compte Stripe a échoué :-(
stripe_connect_settings:
resource: Configuration de Stripe Connect
resend_confirmation_emails_feedback:
one: "Emails envoyés pour 1 commande."
many: "Emails envoyés pour %{count} commandes"
other: "Emails envoyés pour %{count} commandes."
api:
unknown_error: "Quelque chose n'a pas fonctionné. Notre équipe a été notifiée."
invalid_api_key: " La clé API (%{key}) n'est pas valide."
@@ -1807,6 +1831,7 @@ fr_CA:
message_html: "Vous avez déjà passé une commande pour ce cycle de vente. Vérifiez votre %{cart} pour voir les produits commandés. Vous pouvez annuler ou modifier votre commande jusqu'à la fermeture du cycle de vente."
step1:
contact_information:
title: Contact
email:
label: Email
phone:
@@ -1860,10 +1885,13 @@ fr_CA:
title: Détails de livraison
edit: Modifier
address: Adresse de livraison
instructions: Instructions
payment_method:
title: Méthode de paiement
edit: Modifier
instructions: Instructions
order:
title: Total commande
edit: Modifier
terms_and_conditions:
message_html: "J'accepte les %{terms_and_conditions_link}"
@@ -1876,6 +1904,7 @@ fr_CA:
submit: Valider ma commande
cancel: Retour à la méthode de paiement
errors:
saving_failed: "La sauvegarde n'a pas fonctionné, veuillez mettre à jour les champs en rouge.%{messages}"
terms_not_accepted: Merci d'accepter les CGU & CGV.
required: Ce champ ne peut pas être vide
invalid_number: "Merci de renseigner un numéro de téléphone valide"
@@ -2576,6 +2605,7 @@ fr_CA:
report_customers_cycle: "Cycle de vente"
report_customers_type: "Type de rapport"
report_customers_csv: "Télécharger en csv"
report_customers: Acheteur
report_producers: "Producteurs"
report_type: "Type de rapport"
report_hubs: "Hubs"
@@ -2657,6 +2687,7 @@ fr_CA:
report_header_tax_on_delivery: "Taxe sur livraison (%{currency_symbol})"
report_header_tax_on_fees: "Taxe sur commission hub (%{currency_symbol})"
report_header_tax_category: "Type de taxe"
report_header_tax_rate_name: "Taxe"
report_header_tax_rate: "Taxe applicable"
report_header_total_tax: "Total Taxe (%{currency_symbol})"
report_header_total_excl_tax: "Total HT (%{currency_symbol})"
@@ -2667,6 +2698,7 @@ fr_CA:
report_header_customer_code: Code acheteur
report_header_product: Produit
report_header_product_properties: Propriétés / labels Produits
report_header_product_tax_category: Taxe applicable
report_header_quantity: Nb commandé
report_header_max_quantity: Quantité Max
report_header_variant: Variante
@@ -2679,6 +2711,8 @@ fr_CA:
report_header_supplier: Fournisseur
report_header_producer: Producteur
report_header_producer_suburb: Ville Producteur
report_header_producer_tax_status: Soumis à la taxe
report_header_producer_charges_sales_tax?: Soumis à la GST
report_header_unit: Unité
report_header_group_buy_unit_quantity: Nb d'unités achetées (vente par lots)
report_header_cost: Coût
@@ -2694,10 +2728,12 @@ fr_CA:
report_header_distributor_address: Adresse Hub Distributeur
report_header_distributor_city: Ville Distributeur
report_header_distributor_postcode: Code Postal Distributeur
report_header_distributor_tax_status: Statut de la boutique
report_header_delivery_address: Adresse Livraison
report_header_delivery_postcode: Code Postal Livraison
report_header_bulk_unit_size: Quantité totale du lot
report_header_weight: Poids
report_header_final_weight_volume: Poids ou volume livré
report_header_height: Hauteur
report_header_width: Largeur
report_header_depth: Profondeur
@@ -2876,6 +2912,7 @@ fr_CA:
deleting_item_will_cancel_order: "Cette opération va rendre une ou plusieurs commandes vides, sans aucun produit. Elles vont ainsi être annulées. Souhaitez-vous continuer ?"
modals:
got_it: "J'ai compris"
confirm: "Valider"
close: "Fermer"
continue: "Suivant"
cancel: "Annuler"
@@ -3432,6 +3469,8 @@ fr_CA:
server_error: "Erreur serveur"
shipping_method_names:
UPS Ground: "UPS Ground"
pick_up: "Retrait"
delivery: "Signé, scellé, livré"
start_date: "Date de début"
successfully_removed: "Supprimé avec succès"
taxonomy_edit: "Modifier la taxonomie"
@@ -3516,6 +3555,7 @@ fr_CA:
display_currency: "Afficher la devise"
choose_currency: "Choisir la devise"
mail_method_settings: "Paramètre méthode mail"
mail_settings_notice_html: "<b>Les modifications apportées ici seront temporaires</b>et peuvent changer à la prochaine mise à jour. <br> Si vous souhaitez réaliser des changements permanents, un administrateur système doit se charger de mettre à jour les informations et provisionner le serveur en utilisant<a href='https://github.com/openfoodfoundation/ofn-install'> ofn-install </a>."
general: "Général"
enable_mail_delivery: "Permettre distribution des mails"
send_mails_as: "Envoyer les mails en tant que"
@@ -3638,6 +3678,7 @@ fr_CA:
messages:
included_price_validation: "Ce cycle de vente a déjà été utilisé par un acheteur et ne peut être supprimé. Pour empêcher aux acheteurs d'y accéder, veuillez plutôt le fermer."
blank: "Champ obligatoire"
invalid_instagram_url: "Uniquement le nom d'utilisateur / identifiant par ex. le_prof"
layouts:
admin:
login_nav:
@@ -3748,6 +3789,7 @@ fr_CA:
print_invoices: "Imprimer les factures"
cancel_orders: "Annuler les commandes"
resend_confirmation: "Renvoyer la confirmation"
resend_confirmation_confirm_html: "Cette action va renvoyer l'email de confirmation de commande. Etes-vous sûr de vouloir continuer ?"
selected:
zero: "Aucune commande sélectionnée"
one: "1 commande sélectionnée"
@@ -3896,6 +3938,7 @@ fr_CA:
title: "Nouveau Produit"
new_product: "Nouveau Produit"
supplier: "Fournisseur"
supplier_select_placeholder: "Sélectionner un producteur"
product_name: "Nom du Produit"
units: "Unité de mesure"
value: "Nb unités"
@@ -3941,6 +3984,9 @@ fr_CA:
select_and_search: "Sélectionner les filtres et cliquez sur %{option} pour accéder aux données."
customer_names_message:
customer_names_tip: "Si les noms des acheteurs sont masqué, vous pouvez contacter le gestionnaire de la boutique. Il pourra vous donner accès à cette information."
products_and_inventory:
all_products:
message: "Attention les stocks correspondent aux stocks producteurs et non au stock du catalogue boutique."
users:
index:
listing_users: "Liste des utilisateurs"
@@ -4304,6 +4350,7 @@ fr_CA:
search_input:
placeholder: Chercher
selector_with_filter:
selected_items: "%{count} sélectionné"
search_placeholder: Chercher
pagination:
next: Suivant

View File

@@ -32,7 +32,9 @@ Spree::Core::Engine.routes.draw do
put '/password/change' => 'user_passwords#update', :as => :update_password
end
resource :account, :controller => 'users'
resource :account, :controller => 'users' do
resources :webhook_endpoints, only: [:create, :destroy], controller: '/webhook_endpoints'
end
match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management", via: :get
match '/admin/payment_methods/show_provider_preferences' => 'admin/payment_methods#show_provider_preferences', :via => :get

View File

@@ -15,5 +15,7 @@
every: "5m"
SubscriptionConfirmJob:
every: "5m"
OrderCycleOpenedJob:
every: "5m"
OrderCycleClosingJob:
every: "5m"

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddOpenedAtToOrderCycle < ActiveRecord::Migration[6.1]
def change
add_column :order_cycles, :opened_at, :timestamp
end
end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
class CreateWebhookEndpoints < ActiveRecord::Migration[6.1]
def change
create_table :webhook_endpoints do |t|
t.string :url, null: false
t.timestamps null: false
end
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddSpreeUserReferenceToWebhookEndpoint < ActiveRecord::Migration[6.1]
def change
add_column :webhook_endpoints, :user_id, :bigint, default: 0, null: false
add_index :webhook_endpoints, :user_id
add_foreign_key :webhook_endpoints, :spree_users, column: :user_id
end
end

View File

@@ -310,6 +310,7 @@ ActiveRecord::Schema.define(version: 2023_02_13_160135) do
t.datetime "processed_at"
t.boolean "automatic_notifications", default: false
t.boolean "mails_sent", default: false
t.datetime "opened_at"
end
create_table "order_cycles_distributor_payment_methods", id: false, force: :cascade do |t|
@@ -1189,6 +1190,14 @@ ActiveRecord::Schema.define(version: 2023_02_13_160135) do
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
end
create_table "webhook_endpoints", force: :cascade do |t|
t.string "url", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.bigint "user_id", default: 0, null: false
t.index ["user_id"], name: "index_webhook_endpoints_on_user_id"
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "adjustment_metadata", "enterprises", name: "adjustment_metadata_enterprise_id_fk"
@@ -1293,4 +1302,5 @@ ActiveRecord::Schema.define(version: 2023_02_13_160135) do
add_foreign_key "subscriptions", "spree_shipping_methods", column: "shipping_method_id", name: "subscriptions_shipping_method_id_fk"
add_foreign_key "variant_overrides", "enterprises", column: "hub_id", name: "variant_overrides_hub_id_fk"
add_foreign_key "variant_overrides", "spree_variants", column: "variant_id", name: "variant_overrides_variant_id_fk"
add_foreign_key "webhook_endpoints", "spree_users", column: "user_id"
end

View File

@@ -43,7 +43,7 @@
"devDependencies": {
"@webpack-cli/serve": "*",
"husky": "^8.0.0",
"jasmine-core": "~4.5.0",
"jasmine-core": "~4.6.0",
"jest": "^27.4.7",
"karma": "~6.4.1",
"karma-chrome-launcher": "~3.1.0",

View File

@@ -122,10 +122,8 @@ describe Admin::SchedulesController, type: :controller do
spree_put :update, format: :json, id: coordinated_schedule.id,
order_cycle_ids: [coordinated_order_cycle.id, coordinated_order_cycle2.id]
reset_controller_environment
spree_put :update, format: :json, id: coordinated_schedule.id,
order_cycle_ids: [coordinated_order_cycle.id]
reset_controller_environment
spree_put :update, format: :json, id: coordinated_schedule.id,
order_cycle_ids: [coordinated_order_cycle.id]
end

View File

@@ -23,11 +23,9 @@ module Spree
expect(return_authorization.amount.to_s).to eq "20.2"
expect(return_authorization.reason.to_s).to eq "broken"
# Reset the test controller between requests
reset_controller_environment
# Update return authorization
spree_put :update, id: return_authorization.id,
spree_put :update, order_id: order.number,
id: return_authorization.id,
return_authorization: { amount: "10.2", reason: "half broken" }
expect(response).to redirect_to spree.admin_order_return_authorizations_url(order.number)

View File

@@ -0,0 +1,67 @@
# frozen_string_literal: false
require 'spec_helper'
require 'open_food_network/order_cycle_permissions'
describe WebhookEndpointsController, type: :controller do
let(:user) { create(:admin_user) }
before { allow(controller).to receive(:spree_current_user) { user } }
describe "#create" do
it "creates a webhook_endpoint" do
expect {
spree_post :create, { url: "https://url" }
}.to change {
user.webhook_endpoints.count
}.by(1)
expect(flash[:success]).to be_present
expect(flash[:error]).to be_blank
expect(user.webhook_endpoints.first.url).to eq "https://url"
end
it "shows error if parameters not specified" do
expect {
spree_post :create, { url: "" }
}.to_not change {
user.webhook_endpoints.count
}
expect(flash[:success]).to be_blank
expect(flash[:error]).to be_present
end
it "redirects back to referrer" do
spree_post :create, { url: "https://url" }
expect(response).to redirect_to "/account#/developer_settings"
end
end
describe "#destroy" do
let!(:webhook_endpoint) { user.webhook_endpoints.create(url: "https://url") }
it "destroys a webhook_endpoint" do
webhook_endpoint2 = user.webhook_endpoints.create!(url: "https://url2")
expect {
spree_delete :destroy, { id: webhook_endpoint.id }
}.to change {
user.webhook_endpoints.count
}.by(-1)
expect(flash[:success]).to be_present
expect(flash[:error]).to be_blank
expect{ webhook_endpoint.reload }.to raise_error(ActiveRecord::RecordNotFound)
expect(webhook_endpoint2.reload).to be_present
end
it "redirects back to developer settings tab" do
spree_delete :destroy, id: webhook_endpoint.id
expect(response).to redirect_to "/account#/developer_settings"
end
end
end

View File

@@ -0,0 +1,62 @@
# frozen_string_literal: true
require 'spec_helper'
describe OrderCycleOpenedJob do
let(:oc_opened_before) {
create(:order_cycle, orders_open_at: Time.zone.now - 1.hour)
}
let(:oc_opened_now) {
create(:order_cycle, orders_open_at: Time.zone.now)
}
let(:oc_opening_soon) {
create(:order_cycle, orders_open_at: Time.zone.now + 1.minute)
}
it "enqueues jobs for recently opened order cycles only" do
expect(OrderCycleWebhookService)
.to receive(:create_webhook_job).with(oc_opened_now, 'order_cycle.opened')
expect(OrderCycleWebhookService)
.to_not receive(:create_webhook_job).with(oc_opened_before, 'order_cycle.opened')
expect(OrderCycleWebhookService)
.to_not receive(:create_webhook_job).with(oc_opening_soon, 'order_cycle.opened')
OrderCycleOpenedJob.perform_now
end
describe "concurrency", concurrency: true do
let(:breakpoint) { Mutex.new }
it "doesn't place duplicate job when run concurrently" do
oc_opened_now
# Pause jobs when placing new job:
breakpoint.lock
allow(OrderCycleOpenedJob).to(
receive(:new).and_wrap_original do |method, *args|
breakpoint.synchronize {}
method.call(*args)
end
)
expect(OrderCycleWebhookService)
.to receive(:create_webhook_job).with(oc_opened_now, 'order_cycle.opened').once
# Start two jobs in parallel:
threads = [
Thread.new { OrderCycleOpenedJob.perform_now },
Thread.new { OrderCycleOpenedJob.perform_now },
]
# Wait for both to jobs to pause.
# This can reveal a race condition.
sleep 0.1
# Resume and complete both jobs:
breakpoint.unlock
threads.each(&:join)
end
end
end

View File

@@ -0,0 +1,87 @@
# frozen_string_literal: true
require 'spec_helper'
describe WebhookDeliveryJob do
subject { WebhookDeliveryJob.new(url, event, data) }
let(:url) { 'https://test/endpoint' }
let(:event) { 'order_cycle.opened' }
let(:data) {
{
order_cycle_id: 123, name: "Order cycle 1", open_at: 1.minute.ago.to_s, tags: ["tag1", "tag2"]
}
}
before do
stub_request(:post, url)
end
it "sends a request to specified url" do
subject.perform_now
expect(a_request(:post, url)).to have_been_made.once
end
it "delivers a payload" do
Timecop.freeze do
expected_body = {
id: /.+/,
at: Time.zone.now.to_s,
event: event,
data: data,
}
subject.perform_now
expect(a_request(:post, url).with(body: expected_body)).
to have_been_made.once
end
end
# Ensure responses from a local network aren't allowed, to prevent a user
# seeing a private response or initiating an unauthorised action (SSRF).
# Currently, we're not doing anything with responses. When we do, we should
# update this to confirm the response isn't exposed.
describe "server side request forgery" do
describe "private addresses" do
private_addresses = [
"http://127.0.0.1/all_the_secrets",
"http://localhost/all_the_secrets",
]
private_addresses.each do |url|
it "rejects private address #{url}" do
# Github Actions doesn't allow local connections.
pending if ENV["CI"]
expect {
WebhookDeliveryJob.perform_now(url, event, data)
}.to raise_error(PrivateAddressCheck::PrivateConnectionAttemptedError)
end
end
end
describe "redirects" do
it "doesn't follow a redirect" do
other_url = 'http://localhost/all_the_secrets'
stub_request(:post, url).
to_return(status: 302, headers: { 'Location' => other_url })
stub_request(:any, other_url)
expect {
subject.perform_now
}.to raise_error(StandardError, "302")
expect(a_request(:any, other_url)).not_to have_been_made
end
end
end
# Exceptions are considered a job failure, which the job runner
# (Sidekiq) and/or ActiveJob will handle and retry later.
describe "failure" do
it "raises error on server error" do
stub_request(:post, url).to_return(status: [500, "Internal Server Error"])
expect{ subject.perform_now }.to raise_error(StandardError, "500")
end
end
end

View File

@@ -551,6 +551,27 @@ describe OrderCycle do
end
end
describe "opened_at " do
let!(:oc) {
create(:simple_order_cycle, orders_open_at: 2.days.ago, orders_close_at: 1.day.ago, opened_at: 1.week.ago)
}
it "reset opened_at if open date change in future" do
expect{ oc.update!(orders_open_at: 1.week.from_now, orders_close_at: 2.weeks.from_now) }
.to change { oc.opened_at }.to be_nil
end
it "it does not reset opened_at if open date is changed to be earlier" do
expect{ oc.update!(orders_open_at: 3.days.ago) }
.to_not change { oc.opened_at }
end
it "it does not reset opened_at if open date does not change" do
expect{ oc.update!(orders_close_at: 1.day.from_now) }
.to_not change { oc.opened_at }
end
end
describe "processed_at " do
let!(:oc) {
create(:simple_order_cycle, orders_open_at: 1.week.ago, orders_close_at: 1.day.ago, processed_at: 1.hour.ago)
@@ -562,13 +583,13 @@ describe OrderCycle do
expect(oc.processed_at).to be_nil
end
it "it does not reset processed_at if close date change in the past" do
it "it does not reset processed_at if close date is changed to be earlier" do
expect(oc.processed_at).to_not be_nil
oc.update!(orders_close_at: 2.days.ago)
expect(oc.processed_at).to_not be_nil
end
it "it does not reset processed_at if close date do not change" do
it "it does not reset processed_at if close date does not change" do
expect(oc.processed_at).to_not be_nil
oc.update!(orders_open_at: 2.weeks.ago)
expect(oc.processed_at).to_not be_nil

View File

@@ -7,6 +7,7 @@ describe Spree::User do
describe "associations" do
it { is_expected.to have_many(:owned_enterprises) }
it { is_expected.to have_many(:webhook_endpoints).dependent(:destroy) }
describe "addresses" do
let(:user) { create(:user, bill_address: create(:address)) }

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
describe WebhookEndpoint, type: :model do
describe "validations" do
it { is_expected.to validate_presence_of(:url) }
end
end

View File

@@ -0,0 +1,143 @@
# frozen_string_literal: true
require 'spec_helper'
describe OrderCycleWebhookService do
let(:order_cycle) {
create(
:simple_order_cycle,
name: "Order cycle 1",
orders_open_at: "2022-09-19 09:00:00".to_time,
orders_close_at: "2022-09-19 17:00:00".to_time,
coordinator: coordinator,
)
}
let(:coordinator) { create :distributor_enterprise, name: "Starship Enterprise" }
describe "creating payloads" do
it "doesn't create webhook payload for enterprise users" do
# The co-ordinating enterprise has a non-owner user with an endpoint.
# They shouldn't receive a notification.
coordinator_user = create(:user, enterprises: [coordinator])
coordinator_user.webhook_endpoints.create!(url: "http://coordinator_user_url")
expect{ OrderCycleWebhookService.create_webhook_job(order_cycle, "order_cycle.opened") }
.to_not enqueue_job(WebhookDeliveryJob).with("http://coordinator_user_url", any_args)
end
context "coordinator owner has endpoint configured" do
before do
coordinator.owner.webhook_endpoints.create! url: "http://coordinator_owner_url"
end
it "creates webhook payload for order cycle coordinator" do
expect{ OrderCycleWebhookService.create_webhook_job(order_cycle, "order_cycle.opened") }
.to enqueue_job(WebhookDeliveryJob).with("http://coordinator_owner_url", any_args)
end
it "creates webhook payload with details for the specified order cycle only" do
# The coordinating enterprise has another OC. It should be ignored.
order_cycle.dup.save
data = {
id: order_cycle.id,
name: "Order cycle 1",
orders_open_at: "2022-09-19 09:00:00".to_time,
orders_close_at: "2022-09-19 17:00:00".to_time,
coordinator_id: coordinator.id,
coordinator_name: "Starship Enterprise",
}
expect{ OrderCycleWebhookService.create_webhook_job(order_cycle, "order_cycle.opened") }
.to enqueue_job(WebhookDeliveryJob).exactly(1).times
.with("http://coordinator_owner_url", "order_cycle.opened", hash_including(data))
end
end
context "coordinator owner doesn't have endpoint configured" do
it "doesn't create webhook payload" do
expect{ OrderCycleWebhookService.create_webhook_job(order_cycle, "order_cycle.opened") }
.not_to enqueue_job(WebhookDeliveryJob)
end
end
describe "distributors" do
context "multiple distributors have owners with endpoint configured" do
let(:order_cycle) {
create(
:simple_order_cycle,
coordinator: coordinator,
distributors: two_distributors,
)
}
let(:two_distributors) {
(1..2).map do |i|
user = create(:user)
user.webhook_endpoints.create!(url: "http://distributor#{i}_owner_url")
create(:distributor_enterprise, owner: user)
end
}
it "creates webhook payload for each order cycle distributor" do
data = {
coordinator_id: order_cycle.coordinator_id,
coordinator_name: "Starship Enterprise",
}
expect{ OrderCycleWebhookService.create_webhook_job(order_cycle, "order_cycle.opened") }
.to enqueue_job(WebhookDeliveryJob).with("http://distributor1_owner_url",
"order_cycle.opened", hash_including(data))
.and enqueue_job(WebhookDeliveryJob).with("http://distributor2_owner_url",
"order_cycle.opened", hash_including(data))
end
end
context "distributor owner is same user as coordinator owner" do
let(:user) { coordinator.owner }
let(:order_cycle) {
create(
:simple_order_cycle,
coordinator: coordinator,
distributors: [create(:distributor_enterprise, owner: user)],
)
}
it "creates only one webhook payload for the user's endpoint" do
user.webhook_endpoints.create! url: "http://coordinator_owner_url"
expect{ OrderCycleWebhookService.create_webhook_job(order_cycle, "order_cycle.opened") }
.to enqueue_job(WebhookDeliveryJob).with("http://coordinator_owner_url", any_args)
end
end
end
describe "suppliers" do
context "supplier has owner with endpoint configured" do
let(:order_cycle) {
create(
:simple_order_cycle,
coordinator: coordinator,
suppliers: [supplier],
)
}
let(:supplier) {
user = create(:user)
user.webhook_endpoints.create!(url: "http://supplier_owner_url")
create(:supplier_enterprise, owner: user)
}
it "doesn't create a webhook payload for supplier owner" do
expect{ OrderCycleWebhookService.create_webhook_job(order_cycle, "order_cycle.opened") }
.to_not enqueue_job(WebhookDeliveryJob).with("http://supplier_owner_url", any_args)
end
end
end
end
context "without webhook subscribed to enterprise" do
it "doesn't create webhook payload" do
expect{ OrderCycleWebhookService.create_webhook_job(order_cycle, "order_cycle.opened") }
.not_to enqueue_job(WebhookDeliveryJob)
end
end
end

View File

@@ -24,13 +24,5 @@ module OpenFoodNetwork
allow(controller).to receive_messages(spree_current_user: @enterprise_user)
end
def reset_controller_environment
# Rails 5.0 introduced a bug in controller tests (fixed in 5.2) where the controller's
# environment is essentially cached if multiple requests are made in the same `it` block,
# meaning subsequent requests will not be handled well. This resets the environment.
# This edge case is quite rare though; normally we only do one request per test.
@request.env.delete("RAW_POST_DATA")
end
end
end

View File

@@ -127,6 +127,81 @@ describe '
end
end
context "searching" do
let!(:a1) { create(:address, phone: "1234567890", firstname: "Willy", lastname: "Wonka") }
let!(:o1) {
create(:order_with_distributor, state: 'complete', shipment_state: 'ready',
completed_at: Time.zone.now, bill_address: a1)
}
let!(:o2) {
create(:order_with_distributor, state: 'complete', shipment_state: 'ready',
completed_at: Time.zone.now )
}
let!(:s1) { create(:supplier_enterprise) }
let!(:s2) { create(:supplier_enterprise) }
let!(:li1) { create(:line_item_with_shipment, order: o1, product: create(:product, supplier: s1)) }
let!(:li2) { create(:line_item_with_shipment, order: o2, product: create(:product, supplier: s2)) }
let!(:li3) { create(:line_item_with_shipment, order: o2, product: create(:product, supplier: s2)) }
before :each do
visit_bulk_order_management
end
it "by product name" do
fill_in "quick_filter", with: li1.product.name
page.find('.filter-actions .button.icon-search').click
expect_line_items_results [li1], [li2, li3]
end
it "by supplier name" do
fill_in "quick_filter", with: li1.product.supplier.name
page.find('.filter-actions .button.icon-search').click
expect_line_items_results [li1], [li2, li3]
end
it "by email" do
fill_in "quick_filter", with: o1.email
page.find('.filter-actions .button.icon-search').click
expect_line_items_results [li1], [li2, li3]
end
it "by order number" do
fill_in "quick_filter", with: o1.number
page.find('.filter-actions .button.icon-search').click
expect_line_items_results [li1], [li2, li3]
end
it "by phone number" do
fill_in "quick_filter", with: o1.bill_address.phone
page.find('.filter-actions .button.icon-search').click
expect_line_items_results [li1], [li2, li3]
end
it "by distributor name" do
fill_in "quick_filter", with: o1.distributor.name
page.find('.filter-actions .button.icon-search').click
expect_line_items_results [li1], [li2, li3]
end
it "by customer name" do
fill_in "quick_filter", with: o1.bill_address.firstname
page.find('.filter-actions .button.icon-search').click
expect_line_items_results [li1], [li2, li3]
fill_in "quick_filter", with: o1.bill_address.lastname
page.find('.filter-actions .button.icon-search').click
expect_line_items_results [li1], [li2, li3]
end
end
context "displaying individual columns" do
let!(:o1) {
create(:order_with_distributor, state: 'complete', shipment_state: 'ready', completed_at: Time.zone.now,
@@ -602,38 +677,6 @@ describe '
end
end
context "using quick search" do
let!(:o1) {
create(:order_with_distributor, state: 'complete', shipment_state: 'ready',
completed_at: Time.zone.now )
}
let!(:o2) {
create(:order_with_distributor, state: 'complete', shipment_state: 'ready',
completed_at: Time.zone.now )
}
let!(:o3) {
create(:order_with_distributor, state: 'complete', shipment_state: 'ready',
completed_at: Time.zone.now )
}
let!(:li1) { create(:line_item_with_shipment, order: o1 ) }
let!(:li2) { create(:line_item_with_shipment, order: o2 ) }
let!(:li3) { create(:line_item_with_shipment, order: o3 ) }
before :each do
visit_bulk_order_management
end
it "filters line items based on their attributes and the contents of the quick search input" do
expect(page).to have_selector "tr#li_#{li1.id}"
expect(page).to have_selector "tr#li_#{li2.id}"
expect(page).to have_selector "tr#li_#{li3.id}"
fill_in "quick_search", with: o1.email
expect(page).to have_selector "tr#li_#{li1.id}"
expect(page).to have_no_selector "tr#li_#{li2.id}"
expect(page).to have_no_selector "tr#li_#{li3.id}"
end
end
context "using date restriction controls" do
let!(:o1) {
create(:order_with_distributor, state: 'complete', shipment_state: 'ready',
@@ -825,39 +868,6 @@ describe '
end
end
end
context "when a filter has been applied" do
it "only toggles checkboxes which are in filteredLineItems" do
fill_in "quick_search", with: o1.number
expect(page).to have_no_selector "tr#li_#{li2.id}"
check "toggle_bulk"
fill_in "quick_search", with: ''
wait_until { request_monitor_finished 'LineItemsCtrl' }
expect(find("tr#li_#{li1.id} input[type='checkbox'][name='bulk']").checked?).to be true
expect(find("tr#li_#{li2.id} input[type='checkbox'][name='bulk']").checked?).to be false
expect(find("input[type='checkbox'][name='toggle_bulk']").checked?).to be false
end
it "only applies the delete action to filteredLineItems" do
check "toggle_bulk"
fill_in "quick_search", with: o1.number
expect(page).to have_no_selector "tr#li_#{li2.id}"
find("div#bulk-actions-dropdown").click
find("div#bulk-actions-dropdown div.menu_item", text: "Delete Selected" ).click
within ".modal" do
click_on("OK")
end
expect(page).to have_no_selector "tr#li_#{li1.id}"
expect(page).to have_selector "#quick_search"
fill_in "quick_search", with: ''
wait_until { request_monitor_finished 'LineItemsCtrl' }
expect(page).to have_selector "tr#li_#{li2.id}"
expect(page).to have_no_selector "tr#li_#{li1.id}"
end
end
end
context "using action buttons" do
@@ -1101,4 +1111,14 @@ describe '
expect(page).to have_selector "tr#li_#{li1.id}"
expect(page).to have_selector "tr#li_#{li2.id}"
end
def expect_line_items_results(line_items, excluded_line_items)
expect(page).to have_text "Loading orders"
line_items.each do |li|
expect(page).to have_selector "tr#li_#{li.id}"
end
excluded_line_items.each do |li|
expect(page).to have_no_selector "tr#li_#{li.id}"
end
end
end

View File

@@ -35,6 +35,22 @@ describe "Developer Settings" do
expect(page).to have_content "Key generated"
expect(page).to have_input "api_key", with: user.reload.spree_api_key
end
describe "Webhook Endpoints" do
it "creates a new webhook endpoint and deletes it" do
within "#webhook_endpoints" do
fill_in "webhook_endpoint_url", with: "https://url"
click_button I18n.t(:create)
expect(page.document).to have_content I18n.t('webhook_endpoints.create.success')
expect(page).to have_content "https://url"
click_button I18n.t(:delete)
expect(page.document).to have_content I18n.t('webhook_endpoints.destroy.success')
expect(page).to_not have_content "https://url"
end
end
end
end
end

View File

@@ -5379,10 +5379,10 @@ istanbul-reports@^3.1.3:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
jasmine-core@~4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-4.5.0.tgz#1a6bd0bde3f60996164311c88a0995d67ceda7c3"
integrity sha512-9PMzyvhtocxb3aXJVOPqBDswdgyAeSB81QnLop4npOpbqnheaTEwPc9ZloQeVswugPManznQBjD8kWDTjlnHuw==
jasmine-core@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-4.6.0.tgz#6884fc3d5b66bf293e422751eed6d6da217c38f5"
integrity sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==
jest-changed-files@^27.5.1:
version "27.5.1"