mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-15 19:06:50 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
788457618f | ||
|
|
ef607da2c1 | ||
|
|
9ea6fa5c44 | ||
|
|
a945f8f72f | ||
|
|
0c3ee2e8fc | ||
|
|
f6458e91c2 | ||
|
|
4757b82a80 | ||
|
|
ad9e82e973 | ||
|
|
e8430eae6d | ||
|
|
584b013a49 | ||
|
|
f15cb01f2a | ||
|
|
f1a5c46685 | ||
|
|
00f2f92ce0 | ||
|
|
fdd71cff51 | ||
|
|
c9ca020f05 | ||
|
|
d59074dabd | ||
|
|
6a874b9527 | ||
|
|
1f08da207f | ||
|
|
687d4593fb | ||
|
|
b62f88512f | ||
|
|
9d5ca2255b | ||
|
|
00a823b2fc | ||
|
|
3d81a6e280 | ||
|
|
739df4be01 | ||
|
|
b91cabc510 | ||
|
|
ba152f12ee | ||
|
|
778baba118 | ||
|
|
85c98c6d3e | ||
|
|
de9546587a | ||
|
|
9741935955 | ||
|
|
9d19f37fec | ||
|
|
718ac0ab80 | ||
|
|
797b98d686 | ||
|
|
3dc3ebe584 | ||
|
|
ac739108a2 | ||
|
|
50bc48c96f | ||
|
|
8ad532c41a | ||
|
|
ebd5d706c2 | ||
|
|
8658b1a743 |
3
Gemfile
3
Gemfile
@@ -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'
|
||||
|
||||
144
Gemfile.lock
144
Gemfile.lock
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
47
app/controllers/webhook_endpoints_controller.rb
Normal file
47
app/controllers/webhook_endpoints_controller.rb
Normal 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
|
||||
27
app/jobs/order_cycle_opened_job.rb
Normal file
27
app/jobs/order_cycle_opened_job.rb
Normal 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
|
||||
50
app/jobs/webhook_delivery_job.rb
Normal file
50
app/jobs/webhook_delivery_job.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
6
app/models/webhook_endpoint.rb
Normal file
6
app/models/webhook_endpoint.rb
Normal 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
|
||||
21
app/services/order_cycle_webhook_service.rb
Normal file
21
app/services/order_cycle_webhook_service.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
.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 }}
|
||||
|
||||
@@ -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'
|
||||
|
||||
33
app/views/spree/users/_webhook_endpoints.html.haml
Normal file
33
app/views/spree/users/_webhook_endpoints.html.haml
Normal 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'
|
||||
@@ -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? }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,5 +15,7 @@
|
||||
every: "5m"
|
||||
SubscriptionConfirmJob:
|
||||
every: "5m"
|
||||
OrderCycleOpenedJob:
|
||||
every: "5m"
|
||||
OrderCycleClosingJob:
|
||||
every: "5m"
|
||||
|
||||
@@ -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
|
||||
11
db/migrate/20221028051650_create_webhook_endpoints.rb
Normal file
11
db/migrate/20221028051650_create_webhook_endpoints.rb
Normal 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
|
||||
@@ -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
|
||||
10
db/schema.rb
10
db/schema.rb
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
67
spec/controllers/webhook_endpoints_controller_spec.rb
Normal file
67
spec/controllers/webhook_endpoints_controller_spec.rb
Normal 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
|
||||
62
spec/jobs/order_cycle_opened_job_spec.rb
Normal file
62
spec/jobs/order_cycle_opened_job_spec.rb
Normal 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
|
||||
87
spec/jobs/webhook_delivery_job_spec.rb
Normal file
87
spec/jobs/webhook_delivery_job_spec.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)) }
|
||||
|
||||
9
spec/models/webhook_endpoint_spec.rb
Normal file
9
spec/models/webhook_endpoint_spec.rb
Normal 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
|
||||
143
spec/services/order_cycle_webhook_service_spec.rb
Normal file
143
spec/services/order_cycle_webhook_service_spec.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user