mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-04-06 07:29:16 +00:00
Compare commits
126 Commits
v5.4.7.1
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e0f5225b4 | ||
|
|
783ac990bc | ||
|
|
a4cc2f17dc | ||
|
|
c19241ddd9 | ||
|
|
eee9f61c38 | ||
|
|
7f711d746f | ||
|
|
732234f1c0 | ||
|
|
450fe4ada1 | ||
|
|
bcecbf9a0f | ||
|
|
2e6e4b665f | ||
|
|
e255bcc082 | ||
|
|
51b4dc64cc | ||
|
|
77b6bc15e7 | ||
|
|
9b145da898 | ||
|
|
00d600911d | ||
|
|
10d6dd73f2 | ||
|
|
c74624cd57 | ||
|
|
60edcada2c | ||
|
|
b61f6ab444 | ||
|
|
ccc38367f3 | ||
|
|
80a12db191 | ||
|
|
5beed6f028 | ||
|
|
0a65322594 | ||
|
|
b7f154d289 | ||
|
|
edb8a03436 | ||
|
|
3ee338fa8d | ||
|
|
e3da27ca12 | ||
|
|
4c8e6d8260 | ||
|
|
de28083007 | ||
|
|
01bfd72387 | ||
|
|
69d9c52a53 | ||
|
|
5371361a74 | ||
|
|
b7c628dc2a | ||
|
|
5bef61aa2e | ||
|
|
79c346acb1 | ||
|
|
e87159426e | ||
|
|
ca10ae2f5c | ||
|
|
423e8a2cff | ||
|
|
191df4ecf7 | ||
|
|
c274c19e96 | ||
|
|
0e7e09bcfe | ||
|
|
96c2dff744 | ||
|
|
01cff7a618 | ||
|
|
ecca47f96d | ||
|
|
4a66984ec4 | ||
|
|
ac716150eb | ||
|
|
2fe28d1707 | ||
|
|
dcf3ab74b8 | ||
|
|
ef56df09a1 | ||
|
|
3d116d0027 | ||
|
|
fe0c6a4deb | ||
|
|
1e6de5e251 | ||
|
|
af2299c666 | ||
|
|
b37111f007 | ||
|
|
043a8a84f3 | ||
|
|
8ba0ab6b5a | ||
|
|
044f6131da | ||
|
|
062fcd317c | ||
|
|
a2fad2cab3 | ||
|
|
5ab1ce751b | ||
|
|
1a2b5ffc3a | ||
|
|
080c4f7cb5 | ||
|
|
9d389e22d3 | ||
|
|
fc123b38b4 | ||
|
|
6c4ae1d2c1 | ||
|
|
eff1ed4a5e | ||
|
|
7ea2b126f2 | ||
|
|
bfca6248ae | ||
|
|
1ff665a33a | ||
|
|
8250029eb7 | ||
|
|
5e92fa9a17 | ||
|
|
d23ad9c8ad | ||
|
|
d80249da2d | ||
|
|
d6c69fdc2c | ||
|
|
715a8f421a | ||
|
|
06d6db5a07 | ||
|
|
3f81883bc7 | ||
|
|
27be0f6fd1 | ||
|
|
8880f83d09 | ||
|
|
23a4ca5933 | ||
|
|
4dc44c6156 | ||
|
|
8defb2f4c8 | ||
|
|
067349f742 | ||
|
|
aa3fa59a32 | ||
|
|
032953e7d6 | ||
|
|
1878a39188 | ||
|
|
544f62dbc5 | ||
|
|
8e6f1c4e99 | ||
|
|
2004934399 | ||
|
|
827ba1990d | ||
|
|
9961578fc1 | ||
|
|
53c2ef53d5 | ||
|
|
7619062ad2 | ||
|
|
18fb1cfa74 | ||
|
|
e9ce2df5a9 | ||
|
|
c165ade4ba | ||
|
|
7e8b3694be | ||
|
|
6ee715419a | ||
|
|
7da6adfe4f | ||
|
|
05c31db46a | ||
|
|
666e872ac8 | ||
|
|
de6eb9e281 | ||
|
|
299ada1220 | ||
|
|
5757f086ec | ||
|
|
8955ffe126 | ||
|
|
b26152cf0e | ||
|
|
78db179ff3 | ||
|
|
5fc6d25a69 | ||
|
|
b877540f5f | ||
|
|
04c0adf960 | ||
|
|
1c89e9979e | ||
|
|
eba2fbcc30 | ||
|
|
940aa57daf | ||
|
|
766bedb773 | ||
|
|
6fe2357ca0 | ||
|
|
bd01b5f113 | ||
|
|
e565243ce4 | ||
|
|
0f3b299544 | ||
|
|
1332051a6e | ||
|
|
fb2dfed6bf | ||
|
|
cf53ac1990 | ||
|
|
fdd22bc097 | ||
|
|
956c4a27c2 | ||
|
|
2b32f6b909 | ||
|
|
303b91af5e | ||
|
|
ce96b58800 |
6
.env
6
.env
@@ -21,12 +21,6 @@ CHECKOUT_ZONE="Australia"
|
||||
# Find currency codes at http://en.wikipedia.org/wiki/ISO_4217.
|
||||
CURRENCY="AUD"
|
||||
|
||||
# The whenever gem can set the `MAILTO` variable for our cron jobs.
|
||||
# You can define an email address to notify if any job outputs something.
|
||||
# But you need a working mail server setup so that the message is delivered.
|
||||
# See: config/schedule.rb
|
||||
# SCHEDULE_NOTIFICATIONS="admin@example.com"
|
||||
|
||||
# Mail settings
|
||||
MAIL_HOST="example.com"
|
||||
MAIL_DOMAIN="example.com"
|
||||
|
||||
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
@@ -9,32 +9,30 @@ multi-ecosystem-groups:
|
||||
turbo_power:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
updates:
|
||||
# turbo_power: ensure gem and package are updated together
|
||||
- package-ecosystem: "bundler"
|
||||
directory: "/"
|
||||
patterns: ["turbo_power"]
|
||||
multi-ecosystem-group: "turbo_power"
|
||||
|
||||
# Only specific requirements are specified in Gemfile, so don't touch it.
|
||||
versioning-strategy: lockfile-only
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
patterns: ["turbo_power"]
|
||||
multi-ecosystem-group: "turbo_power"
|
||||
|
||||
# Only specific requirements are specified in package.json, so don't touch it.
|
||||
versioning-strategy: lockfile-only
|
||||
|
||||
# All others
|
||||
- package-ecosystem: "bundler"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Only specific requirements are specified in Gemfile, so don't touch it.
|
||||
# Only specific requirements are specified in Gemfile, so don't let Dependabot touch it.
|
||||
versioning-strategy: lockfile-only
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
@@ -43,6 +41,5 @@ updates:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Only specific requirements are specified in package.json, so don't touch it.
|
||||
# Only specific requirements are specified in package.json, so don't let Dependabot touch it.
|
||||
versioning-strategy: lockfile-only
|
||||
|
||||
2
Gemfile
2
Gemfile
@@ -118,8 +118,6 @@ gem 'immigrant'
|
||||
gem 'roo' # read spreadsheets
|
||||
gem 'spreadsheet_architect' # write spreadsheets
|
||||
|
||||
gem 'whenever', require: false
|
||||
|
||||
gem 'coffee-rails', '~> 5.0.0'
|
||||
|
||||
gem 'angular_rails_csrf'
|
||||
|
||||
66
Gemfile.lock
66
Gemfile.lock
@@ -112,7 +112,7 @@ GEM
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
active_model_serializers (0.8.4)
|
||||
activemodel (>= 3.0)
|
||||
active_storage_validations (3.0.3)
|
||||
active_storage_validations (3.0.4)
|
||||
activejob (>= 6.1.4)
|
||||
activemodel (>= 6.1.4)
|
||||
activestorage (>= 6.1.4)
|
||||
@@ -167,7 +167,7 @@ GEM
|
||||
zeitwerk (>= 2.4, < 3.0)
|
||||
acts_as_list (1.0.4)
|
||||
activerecord (>= 4.2)
|
||||
addressable (2.8.8)
|
||||
addressable (2.8.9)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
afm (1.0.0)
|
||||
@@ -185,8 +185,8 @@ GEM
|
||||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1227.0)
|
||||
aws-sdk-core (3.243.0)
|
||||
aws-partitions (1.1233.0)
|
||||
aws-sdk-core (3.244.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -194,11 +194,11 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.122.0)
|
||||
aws-sdk-core (~> 3, >= 3.241.4)
|
||||
aws-sdk-kms (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.215.0)
|
||||
aws-sdk-core (~> 3, >= 3.243.0)
|
||||
aws-sdk-s3 (1.217.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
@@ -210,9 +210,9 @@ GEM
|
||||
bigdecimal (3.3.1)
|
||||
bindata (2.5.1)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.22.0)
|
||||
bootsnap (1.23.0)
|
||||
msgpack (~> 1.2)
|
||||
bugsnag (6.28.0)
|
||||
bugsnag (6.29.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
builder (3.3.0)
|
||||
bullet (8.1.0)
|
||||
@@ -245,7 +245,6 @@ GEM
|
||||
cgi (0.5.1)
|
||||
childprocess (5.0.0)
|
||||
choice (0.2.0)
|
||||
chronic (0.10.2)
|
||||
coderay (1.1.3)
|
||||
coffee-rails (5.0.0)
|
||||
coffee-script (>= 2.2.0)
|
||||
@@ -290,8 +289,8 @@ GEM
|
||||
warden (~> 1.2.3)
|
||||
devise-encryptable (0.2.0)
|
||||
devise (>= 2.1.0)
|
||||
devise-i18n (1.15.0)
|
||||
devise (>= 4.9.0)
|
||||
devise-i18n (1.16.0)
|
||||
devise (>= 5.0.0)
|
||||
rails-i18n
|
||||
diff-lcs (1.6.2)
|
||||
digest (3.2.1)
|
||||
@@ -385,11 +384,11 @@ GEM
|
||||
good_migrations (0.3.1)
|
||||
activerecord (>= 3.1)
|
||||
railties (>= 3.1)
|
||||
haml (6.3.0)
|
||||
haml (7.2.0)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
haml_lint (0.68.0)
|
||||
haml_lint (0.72.0)
|
||||
haml (>= 5.0)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
@@ -402,18 +401,19 @@ GEM
|
||||
highline (3.1.2)
|
||||
reline
|
||||
htmlentities (4.4.2)
|
||||
http_parser.rb (0.8.0)
|
||||
http_parser.rb (0.8.1)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.9.2)
|
||||
i18n (>= 0.6.6)
|
||||
i18n-tasks (1.0.15)
|
||||
i18n-tasks (1.1.2)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
erubi
|
||||
highline (>= 2.0.0)
|
||||
highline (>= 3.0.0)
|
||||
i18n
|
||||
parser (>= 3.2.2.1)
|
||||
prism
|
||||
rails-i18n
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.8, >= 1.8.1)
|
||||
@@ -514,7 +514,7 @@ GEM
|
||||
money (6.16.0)
|
||||
i18n (>= 0.6.4, <= 2)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.17.0)
|
||||
multi_json (1.19.1)
|
||||
multi_xml (0.6.0)
|
||||
mutex_m (0.3.0)
|
||||
net-http (0.9.1)
|
||||
@@ -590,12 +590,12 @@ GEM
|
||||
hashery (~> 2.0)
|
||||
ruby-rc4
|
||||
ttfunk
|
||||
pg (1.6.2)
|
||||
pg (1.6.3)
|
||||
pp (0.6.3)
|
||||
prettyprint
|
||||
prettyprint (0.2.0)
|
||||
prism (1.9.0)
|
||||
private_address_check (0.5.0)
|
||||
private_address_check (0.6.0)
|
||||
pry (0.16.0)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
@@ -603,9 +603,10 @@ GEM
|
||||
psych (5.3.1)
|
||||
date
|
||||
stringio
|
||||
public_suffix (7.0.2)
|
||||
puffing-billy (4.0.2)
|
||||
public_suffix (7.0.5)
|
||||
puffing-billy (4.0.4)
|
||||
addressable (~> 2.5)
|
||||
cgi
|
||||
em-http-request (~> 1.1, >= 1.1.0)
|
||||
em-synchrony
|
||||
eventmachine (~> 1.2)
|
||||
@@ -619,7 +620,7 @@ GEM
|
||||
railties (>= 4.2)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.22)
|
||||
rack (2.2.23)
|
||||
rack-mini-profiler (2.3.4)
|
||||
rack (>= 1.2.0)
|
||||
rack-oauth2 (2.3.0)
|
||||
@@ -896,14 +897,14 @@ GEM
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
sysexits (1.2.0)
|
||||
taler (0.2.0)
|
||||
taler (0.3.0)
|
||||
temple (0.10.4)
|
||||
terminal-table (4.0.0)
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
thor (1.5.0)
|
||||
thread-local (1.1.0)
|
||||
tilt (2.7.0)
|
||||
timeout (0.6.0)
|
||||
timeout (0.6.1)
|
||||
tsort (0.2.0)
|
||||
ttfunk (1.8.0)
|
||||
bigdecimal (~> 3.1)
|
||||
@@ -914,7 +915,7 @@ GEM
|
||||
turbo-rails (>= 1.3.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
undercover (0.8.3)
|
||||
undercover (0.8.4)
|
||||
base64
|
||||
bigdecimal
|
||||
imagen (>= 0.2.0)
|
||||
@@ -936,9 +937,9 @@ GEM
|
||||
validates_lengths_from_database (0.8.0)
|
||||
activerecord (>= 4)
|
||||
vcr (6.4.0)
|
||||
view_component (4.1.1)
|
||||
actionview (>= 7.1.0, < 8.2)
|
||||
activesupport (>= 7.1.0, < 8.2)
|
||||
view_component (4.5.0)
|
||||
actionview (>= 7.1.0)
|
||||
activesupport (>= 7.1.0)
|
||||
concurrent-ruby (~> 1)
|
||||
view_component_reflex (3.1.14.pre9)
|
||||
rails (>= 5.2, < 8.0)
|
||||
@@ -957,7 +958,7 @@ GEM
|
||||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.26.1)
|
||||
webmock (3.26.2)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
@@ -966,8 +967,6 @@ GEM
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
whenever (1.1.0)
|
||||
chronic (>= 0.6.3)
|
||||
xml-simple (1.1.8)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
@@ -1125,7 +1124,6 @@ DEPENDENCIES
|
||||
web!
|
||||
web-console
|
||||
webmock
|
||||
whenever
|
||||
wicked_pdf!
|
||||
wkhtmltopdf-binary!
|
||||
|
||||
|
||||
@@ -3,7 +3,15 @@ angular.module('admin.orderCycles').controller 'AdminOrderCycleIncomingCtrl', ($
|
||||
|
||||
$scope.view = 'incoming'
|
||||
# NB: weirdly at this next line $scope.order_cycle.id comes out undefined so we use $scope.order_cycle_id instead
|
||||
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true)
|
||||
$scope.enterprise_fees = null
|
||||
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true) unless EnterpriseFee.loading
|
||||
|
||||
# We want to make sure to load the filtered EnterpriseFee when any previous request is finished
|
||||
# otherwise the enterprise_fees migh get overriden by non filtered ones.
|
||||
$scope.$watch(( -> EnterpriseFee.loading), (isLoading) =>
|
||||
$scope.enterprise_fees ||= EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true) unless isLoading
|
||||
)
|
||||
|
||||
$scope.exchangeTotalVariants = (exchange) ->
|
||||
return unless $scope.enterprises? && $scope.enterprises[exchange.enterprise_id]?
|
||||
|
||||
|
||||
@@ -14,10 +14,14 @@ angular.module('admin.orderCycles').factory('EnterpriseFee', ($resource) ->
|
||||
EnterpriseFee: EnterpriseFee
|
||||
enterprise_fees: {}
|
||||
loaded: false
|
||||
loading: false
|
||||
|
||||
index: (params={}) ->
|
||||
return if @loading == true
|
||||
@loading = true
|
||||
EnterpriseFee.index params, (data) =>
|
||||
@enterprise_fees = data
|
||||
@loading = false
|
||||
@loaded = true
|
||||
|
||||
forEnterprise: (enterprise_id) ->
|
||||
|
||||
@@ -6,6 +6,7 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris
|
||||
'manage_products'
|
||||
'edit_profile'
|
||||
'create_variant_overrides'
|
||||
'create_linked_variants'
|
||||
]
|
||||
|
||||
constructor: ->
|
||||
@@ -30,3 +31,4 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris
|
||||
when "manage_products" then t('js.services.manage_products')
|
||||
when "edit_profile" then t('js.services.edit_profile')
|
||||
when "create_variant_overrides" then t('js.services.add_products_to_inventory')
|
||||
when "create_linked_variants" then t('js.services.create_linked_variants')
|
||||
|
||||
56
app/controllers/admin/ajax_search_controller.rb
Normal file
56
app/controllers/admin/ajax_search_controller.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class AjaxSearchController < Spree::Admin::BaseController
|
||||
def producers
|
||||
query = OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
.managed_product_enterprises.is_primary_producer.by_name
|
||||
|
||||
render json: build_search_response(query)
|
||||
end
|
||||
|
||||
def categories
|
||||
query = Spree::Taxon.all
|
||||
|
||||
render json: build_search_response(query)
|
||||
end
|
||||
|
||||
def tax_categories
|
||||
query = Spree::TaxCategory.all
|
||||
|
||||
render json: build_search_response(query)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_search_response(query)
|
||||
page = (params[:page] || 1).to_i
|
||||
per_page = 30
|
||||
|
||||
filtered_query = apply_search_filter(query)
|
||||
total_count = filtered_query.size
|
||||
items = paginated_items(filtered_query, page, per_page)
|
||||
results = format_results(items)
|
||||
|
||||
{ results: results, pagination: { more: (page * per_page) < total_count } }
|
||||
end
|
||||
|
||||
def apply_search_filter(query)
|
||||
search_term = params[:q]
|
||||
return query if search_term.blank?
|
||||
|
||||
escaped_search_term = ActiveRecord::Base.sanitize_sql_like(search_term)
|
||||
pattern = "%#{escaped_search_term}%"
|
||||
|
||||
query.where('name ILIKE ?', pattern)
|
||||
end
|
||||
|
||||
def paginated_items(query, page, per_page)
|
||||
query.order(:name).offset((page - 1) * per_page).limit(per_page).pluck(:name, :id)
|
||||
end
|
||||
|
||||
def format_results(items)
|
||||
items.map { |label, value| { value:, label: } }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -107,6 +107,33 @@ module Admin
|
||||
end
|
||||
end
|
||||
|
||||
# Clone a variant, retaining a link to the "source"
|
||||
def create_linked_variant
|
||||
linked_variant = Spree::Variant.find(params[:variant_id])
|
||||
product_index = params[:product_index]
|
||||
authorize! :create_linked_variant, linked_variant
|
||||
status = :ok
|
||||
|
||||
begin
|
||||
variant = linked_variant.create_linked_variant(spree_current_user)
|
||||
|
||||
flash.now[:success] = t('.success')
|
||||
variant_index = "-#{variant.id}"
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
flash.now[:error] = variant.errors.full_messages.to_sentence
|
||||
status = :unprocessable_entity
|
||||
variant_index = "-1" # Create a unique-enough index
|
||||
end
|
||||
|
||||
respond_with do |format|
|
||||
format.turbo_stream {
|
||||
locals = { linked_variant:, variant:, product_index:, variant_index:,
|
||||
producer_options:, category_options: categories, tax_category_options: }
|
||||
render :create_linked_variant, status:, locals:
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def index_url(params)
|
||||
"/admin/products?#{params.to_query}" # todo: fix routing so this can be automaticly generated
|
||||
end
|
||||
|
||||
@@ -52,7 +52,7 @@ module Spree
|
||||
# (we can't use respond_override because Spree no longer uses respond_with)
|
||||
def fire
|
||||
event = params[:e]
|
||||
return unless event && @payment.payment_source
|
||||
return unless event
|
||||
|
||||
# capture_and_complete_order will complete the order, so we want to try to redeem VINE
|
||||
# voucher first and exit if it fails
|
||||
|
||||
@@ -47,5 +47,20 @@ module Admin
|
||||
def variant_tag_enabled?(user)
|
||||
feature?(:variant_tag, user) || feature?(:variant_tag, *user.enterprises)
|
||||
end
|
||||
|
||||
def allowed_source_producers
|
||||
@allowed_source_producers ||= OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
.enterprises_granting_linked_variants
|
||||
end
|
||||
|
||||
# Query only name of the model to avoid loading the whole record
|
||||
def selected_option(id, model)
|
||||
return [] unless id
|
||||
|
||||
name = model.where(id: id).pick(:name)
|
||||
return [] unless name
|
||||
|
||||
[[name, id]]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
15
app/jobs/rake_job.rb
Normal file
15
app/jobs/rake_job.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rake"
|
||||
|
||||
# Executes a rake task
|
||||
class RakeJob < ApplicationJob
|
||||
def perform(task_string)
|
||||
Rails.application.load_tasks if Rake::Task.tasks.empty?
|
||||
|
||||
Rake.application.invoke_task(task_string)
|
||||
ensure
|
||||
name, _args = Rake.application.parse_task_string(task_string)
|
||||
Rake::Task[name].reenable
|
||||
end
|
||||
end
|
||||
@@ -22,10 +22,10 @@ class PaymentMailer < ApplicationMailer
|
||||
end
|
||||
end
|
||||
|
||||
def refund_available(payment, taler_order_status_url)
|
||||
def refund_available(amount, payment, taler_order_status_url)
|
||||
@order = payment.order
|
||||
@shop = @order.distributor.name
|
||||
@amount = payment.display_amount
|
||||
@amount = amount
|
||||
@taler_order_status_url = taler_order_status_url
|
||||
|
||||
I18n.with_locale valid_locale(@order.user) do
|
||||
|
||||
@@ -197,12 +197,16 @@ module Spree
|
||||
can [:admin, :index, :destroy], :oidc_setting
|
||||
|
||||
can [:admin, :create], Voucher
|
||||
|
||||
can [:admin, :destroy], EnterpriseRole do |enterprise_role|
|
||||
enterprise_role.enterprise.owner_id == user.id
|
||||
end
|
||||
end
|
||||
|
||||
def add_product_management_abilities(user)
|
||||
# Enterprise User can only access products that they are a supplier for
|
||||
can [:create], Spree::Product
|
||||
# An enterperprise user can change a product if they are supplier of at least
|
||||
# An enterprise user can change a product if they are supplier of at least
|
||||
# one of the product's associated variants
|
||||
can [:admin, :read, :index, :update,
|
||||
:seo, :group_buy_options,
|
||||
@@ -214,7 +218,24 @@ module Spree
|
||||
)
|
||||
end
|
||||
|
||||
can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone], :products_v3
|
||||
# An enterprise user can clone if they have been granted permission to the source variant.
|
||||
# Technically I'd call this permission clone_linked_variant, but it would be less confusing to
|
||||
# use the same name as everywhere else.
|
||||
can [:create_linked_variant], Spree::Variant do |variant|
|
||||
OpenFoodNetwork::Permissions.new(user).
|
||||
enterprises_granting_linked_variants.include? variant.supplier
|
||||
end
|
||||
can [
|
||||
:admin,
|
||||
:index,
|
||||
:bulk_update,
|
||||
:destroy,
|
||||
:destroy_variant,
|
||||
:clone,
|
||||
:create_linked_variant
|
||||
], :products_v3
|
||||
|
||||
can [:admin, :producers, :categories, :tax_categories], :ajax_search
|
||||
|
||||
can [:create], Spree::Variant
|
||||
can [:admin, :index, :read, :edit,
|
||||
|
||||
@@ -63,35 +63,6 @@ module Spree
|
||||
"XXXX-XXXX-XXXX-#{last_digits}"
|
||||
end
|
||||
|
||||
def actions
|
||||
%w{capture_and_complete_order void credit resend_authorization_email}
|
||||
end
|
||||
|
||||
def can_resend_authorization_email?(payment)
|
||||
payment.requires_authorization?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to capture the payment
|
||||
def can_capture_and_complete_order?(payment)
|
||||
return false if payment.requires_authorization?
|
||||
|
||||
payment.pending? || payment.checkout?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to void the payment.
|
||||
def can_void?(payment)
|
||||
!payment.void?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to credit the payment. Note that most gateways require that the
|
||||
# payment be settled first which generally happens within 12-24 hours of the transaction.
|
||||
def can_credit?(payment)
|
||||
return false unless payment.completed?
|
||||
return false unless payment.order.payment_state == 'credit_owed'
|
||||
|
||||
payment.credit_allowed.positive?
|
||||
end
|
||||
|
||||
# Allows us to use a gateway_payment_profile_id to store Stripe Tokens
|
||||
def has_payment_profile?
|
||||
gateway_customer_profile_id.present? || gateway_payment_profile_id.present?
|
||||
|
||||
@@ -13,6 +13,35 @@ module Spree
|
||||
preference :server, :string, default: 'live'
|
||||
preference :test_mode, :boolean, default: false
|
||||
|
||||
def actions
|
||||
%w{capture_and_complete_order void credit resend_authorization_email}
|
||||
end
|
||||
|
||||
# Indicates whether its possible to capture the payment
|
||||
def can_capture_and_complete_order?(payment)
|
||||
return false if payment.requires_authorization?
|
||||
|
||||
payment.pending? || payment.checkout?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to void the payment.
|
||||
def can_void?(payment)
|
||||
!payment.void?
|
||||
end
|
||||
|
||||
# Indicates whether its possible to credit the payment. Note that most gateways require that the
|
||||
# payment be settled first which generally happens within 12-24 hours of the transaction.
|
||||
def can_credit?(payment)
|
||||
return false unless payment.completed?
|
||||
return false unless payment.order.payment_state == 'credit_owed'
|
||||
|
||||
payment.credit_allowed.positive?
|
||||
end
|
||||
|
||||
def can_resend_authorization_email?(payment)
|
||||
payment.requires_authorization?
|
||||
end
|
||||
|
||||
def payment_source_class
|
||||
CreditCard
|
||||
end
|
||||
|
||||
@@ -152,11 +152,10 @@ module Spree
|
||||
end
|
||||
|
||||
def actions
|
||||
return [] unless payment_source.respond_to?(:actions)
|
||||
return [] unless payment_method.respond_to?(:actions)
|
||||
|
||||
payment_source.actions.select do |action|
|
||||
!payment_source.respond_to?("can_#{action}?") ||
|
||||
payment_source.__send__("can_#{action}?", self)
|
||||
payment_method.actions.select do |action|
|
||||
payment_method.__send__("can_#{action}?", self)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -166,11 +165,6 @@ module Spree
|
||||
PaymentMailer.authorize_payment(self).deliver_later
|
||||
end
|
||||
|
||||
def payment_source
|
||||
res = source.is_a?(Payment) ? source.source : source
|
||||
res || payment_method
|
||||
end
|
||||
|
||||
def ensure_correct_adjustment
|
||||
revoke_adjustment_eligibility if ['failed', 'invalid', 'void'].include?(state)
|
||||
return if adjustment.try(:finalized?)
|
||||
|
||||
@@ -18,15 +18,27 @@ module Spree
|
||||
# - backend_url: https://backend.demo.taler.net/instances/sandbox
|
||||
# - api_key: sandbox
|
||||
class Taler < PaymentMethod
|
||||
# Demo backend instances will use the KUDOS currency.
|
||||
DEMO_PREFIX = "https://backend.demo.taler.net/instances"
|
||||
|
||||
preference :backend_url, :string
|
||||
preference :api_key, :password
|
||||
|
||||
def actions
|
||||
%w{void}
|
||||
%w[credit void]
|
||||
end
|
||||
|
||||
def can_void?(payment)
|
||||
payment.state == "completed"
|
||||
# The source can be another payment. Then this is an offset payment
|
||||
# like a credit record. We can't void a refund.
|
||||
payment.source == self && payment.state == "completed"
|
||||
end
|
||||
|
||||
def can_credit?(payment)
|
||||
return false unless payment.completed?
|
||||
return false unless payment.order.payment_state == 'credit_owed'
|
||||
|
||||
payment.credit_allowed.positive?
|
||||
end
|
||||
|
||||
# Name of the view to display during checkout
|
||||
@@ -68,6 +80,23 @@ module Spree
|
||||
ActiveMerchant::Billing::Response.new(success, message)
|
||||
end
|
||||
|
||||
def credit(money, response_code, gateway_options)
|
||||
amount = money / 100 # called with cents
|
||||
payment = gateway_options[:payment]
|
||||
taler_order = taler_order(id: response_code)
|
||||
status = taler_order.fetch("order_status")
|
||||
|
||||
raise "Unsupported action" if status != "paid"
|
||||
|
||||
taler_amount = "KUDOS:#{amount}"
|
||||
taler_order.refund(refund: taler_amount, reason: "credit")
|
||||
|
||||
spree_money = Spree::Money.new(amount, currency: payment.currency).to_s
|
||||
PaymentMailer.refund_available(spree_money, payment, taler_order.status_url).deliver_later
|
||||
|
||||
ActiveMerchant::Billing::Response.new(true, "Refund initiated")
|
||||
end
|
||||
|
||||
def void(response_code, gateway_options)
|
||||
payment = gateway_options[:payment]
|
||||
taler_order = taler_order(id: response_code)
|
||||
@@ -82,7 +111,8 @@ module Spree
|
||||
amount = taler_order.fetch("contract_terms")["amount"]
|
||||
taler_order.refund(refund: amount, reason: "void")
|
||||
|
||||
PaymentMailer.refund_available(payment, taler_order.status_url).deliver_later
|
||||
spree_money = payment.money.to_s
|
||||
PaymentMailer.refund_available(spree_money, payment, taler_order.status_url).deliver_later
|
||||
|
||||
ActiveMerchant::Billing::Response.new(true, "Refund initiated")
|
||||
end
|
||||
@@ -96,7 +126,7 @@ module Spree
|
||||
def create_taler_order(payment)
|
||||
# We are ignoring currency for now so that we can test with the
|
||||
# current demo backend only working with the KUDOS currency.
|
||||
taler_amount = "KUDOS:#{payment.amount}"
|
||||
taler_amount = "#{currency(payment)}:#{payment.amount}"
|
||||
urls = Rails.application.routes.url_helpers
|
||||
fulfillment_url = urls.payment_gateways_confirm_taler_url(payment_id: payment.id)
|
||||
taler_order.create(
|
||||
@@ -113,6 +143,12 @@ module Spree
|
||||
id:,
|
||||
)
|
||||
end
|
||||
|
||||
def currency(payment)
|
||||
return "KUDOS" if preferred_backend_url.starts_with?(DEMO_PREFIX)
|
||||
|
||||
payment.order.currency
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,6 +40,7 @@ module Spree
|
||||
belongs_to :shipping_category, class_name: 'Spree::ShippingCategory', optional: false
|
||||
belongs_to :primary_taxon, class_name: 'Spree::Taxon', touch: true, optional: false
|
||||
belongs_to :supplier, class_name: 'Enterprise', optional: false, touch: true
|
||||
belongs_to :hub, class_name: 'Enterprise', optional: true
|
||||
|
||||
delegate :name, :name=, :description, :description=, :meta_keywords, to: :product
|
||||
|
||||
@@ -72,6 +73,15 @@ module Spree
|
||||
has_many :semantic_links, as: :subject, dependent: :delete_all
|
||||
has_many :supplier_properties, through: :supplier, source: :properties
|
||||
|
||||
# Linked variants: I may have one or many sources.
|
||||
has_many :variant_links_as_target, class_name: 'VariantLink', foreign_key: :target_variant_id,
|
||||
dependent: :delete_all, inverse_of: :target_variant
|
||||
has_many :source_variants, through: :variant_links_as_target, source: :source_variant
|
||||
# I may also have one more many targets.
|
||||
has_many :variant_links_as_source, class_name: 'VariantLink', foreign_key: :source_variant_id,
|
||||
dependent: :delete_all, inverse_of: :source_variant
|
||||
has_many :target_variants, through: :variant_links_as_source, source: :target_variant
|
||||
|
||||
localize_number :price, :weight
|
||||
|
||||
validates_lengths_from_database
|
||||
@@ -263,6 +273,24 @@ module Spree
|
||||
@on_hand_desired = ActiveModel::Type::Integer.new.cast(val)
|
||||
end
|
||||
|
||||
# Clone this variant, retaining a 'source' link to it
|
||||
def create_linked_variant(user)
|
||||
# Hub owner is my enterprise which has permission to create variant sourced from that supplier
|
||||
hub_id = EnterpriseRelationship.permitted_by(supplier).permitting(user.enterprises)
|
||||
.with_permission(:create_linked_variants)
|
||||
.pick(:child_id)
|
||||
|
||||
dup.tap do |variant|
|
||||
variant.price = price
|
||||
variant.source_variants = [self]
|
||||
variant.stock_items << Spree::StockItem.new(variant:)
|
||||
variant.hub_id = hub_id
|
||||
variant.on_demand = on_demand
|
||||
variant.on_hand = on_hand
|
||||
variant.save!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_currency
|
||||
|
||||
6
app/models/variant_link.rb
Normal file
6
app/models/variant_link.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VariantLink < ApplicationRecord
|
||||
belongs_to :source_variant, class_name: 'Spree::Variant'
|
||||
belongs_to :target_variant, class_name: 'Spree::Variant'
|
||||
end
|
||||
@@ -14,7 +14,6 @@ module Checkout
|
||||
apply_strong_parameters
|
||||
set_pickup_address
|
||||
set_address_details
|
||||
set_payment_amount
|
||||
set_existing_card
|
||||
|
||||
@order_params
|
||||
@@ -58,12 +57,6 @@ module Checkout
|
||||
end
|
||||
end
|
||||
|
||||
def set_payment_amount
|
||||
return unless @order_params[:payments_attributes]
|
||||
|
||||
@order_params[:payments_attributes].first[:amount] = order.outstanding_balance.amount
|
||||
end
|
||||
|
||||
def set_existing_card
|
||||
return unless existing_card_selected?
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ module Payments
|
||||
payment: @payment.slice(:updated_at, :amount, :state),
|
||||
enterprise: @enterprise.slice(:abn, :acn, :name)
|
||||
.merge(address: @enterprise.address.slice(:address1, :address2, :city, :zipcode)),
|
||||
order: @order.slice(:total, :currency).merge(line_items: line_items)
|
||||
order: @order.slice(:number, :total, :currency).merge(line_items: line_items)
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
@@ -31,6 +31,7 @@ module Payments
|
||||
|
||||
def self.test_order
|
||||
order = Spree::Order.new(
|
||||
number: "R555555555",
|
||||
total: 0.00,
|
||||
currency: "AUD",
|
||||
)
|
||||
|
||||
@@ -9,14 +9,22 @@
|
||||
- if producer_options.many?
|
||||
.producers
|
||||
= label_tag :producer_id, t('.producers.label')
|
||||
= select_tag :producer_id, options_for_select(producer_options, producer_id),
|
||||
include_blank: t('.all_producers'), class: "fullwidth",
|
||||
data: { "controller": "tom-select", 'tom-select-placeholder-value': t('.search_for_producers')}
|
||||
= render(SearchableDropdownComponent.new(name: :producer_id,
|
||||
aria_label: t('.producers.label'),
|
||||
options: selected_option(producer_id, Enterprise),
|
||||
selected_option: producer_id,
|
||||
remote_url: admin_ajax_search_producers_url,
|
||||
include_blank: t('.all_producers'),
|
||||
placeholder_value: t('.search_for_producers')))
|
||||
.categories
|
||||
= label_tag :category_id, t('.categories.label')
|
||||
= select_tag :category_id, options_for_select(category_options, category_id),
|
||||
include_blank: t('.all_categories'), class: "fullwidth",
|
||||
data: { "controller": "tom-select", 'tom-select-placeholder-value': t('.search_for_categories')}
|
||||
= render(SearchableDropdownComponent.new(name: :category_id,
|
||||
aria_label: t('.categories.label'),
|
||||
options: selected_option(category_id, Spree::Taxon),
|
||||
selected_option: category_id,
|
||||
remote_url: admin_ajax_search_categories_url,
|
||||
include_blank: t('.all_categories'),
|
||||
placeholder_value: t('.search_for_categories')))
|
||||
-if variant_tag_enabled?(spree_current_user)
|
||||
.tags
|
||||
= label_tag :tags_name_in, t('.tags.label')
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
|
||||
-# Filter out variant a user has not permission to update, but keep variant with no supplier
|
||||
- next if variant.supplier.present? && !allowed_producers.include?(variant.supplier)
|
||||
|
||||
= form.fields_for("products][#{product_index}][variants_attributes", variant, index: variant_index) do |variant_form|
|
||||
%tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper", 'data-new-record': variant.new_record? ? "true" : false }
|
||||
= render partial: 'variant_row', locals: { variant:, f: variant_form, category_options:, tax_category_options:, producer_options: }
|
||||
= render partial: 'variant_row', locals: { variant:, f: variant_form, product_index:, category_options:, tax_category_options:, producer_options: }
|
||||
|
||||
= form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", prepare_new_variant(product, producer_options)) do |new_variant_form|
|
||||
%template{ 'data-nested-form-target': "template" }
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
-# locals: (variant:, f:, category_options:, tax_category_options:, producer_options:)
|
||||
-# haml-lint:disable ViewLength (This file is big, but doesn't make sense to split up at this point)
|
||||
-# locals: (variant:, f:, product_index: nil, category_options:, tax_category_options:, producer_options:)
|
||||
- method_on_demand, method_on_hand = variant.new_record? ? [:on_demand_desired, :on_hand_desired ]: [:on_demand, :on_hand]
|
||||
%td.col-image
|
||||
-# empty
|
||||
- variant.source_variants.each do |source_variant|
|
||||
= content_tag(:span, "🔗", title: t('admin.products_page.variant_row.sourced_from', source_name: source_variant.display_name, source_id: source_variant.id, hub_name: variant.hub&.name))
|
||||
%td.col-name.field.naked_inputs
|
||||
= f.hidden_field :id
|
||||
= f.text_field :display_name, 'aria-label': t('admin.products_page.columns.name'), placeholder: variant.product.name
|
||||
@@ -56,27 +59,28 @@
|
||||
= render(SearchableDropdownComponent.new(form: f,
|
||||
name: :supplier_id,
|
||||
aria_label: t('.producer_field_name'),
|
||||
options: producer_options,
|
||||
options: variant.supplier_id ? [[variant.supplier.name, variant.supplier_id]] : [],
|
||||
selected_option: variant.supplier_id,
|
||||
include_blank: t('admin.products_v3.filters.select_producer'),
|
||||
remote_url: admin_ajax_search_producers_url,
|
||||
placeholder_value: t('admin.products_v3.filters.select_producer')))
|
||||
= error_message_on variant, :supplier
|
||||
%td.col-category.field.naked_inputs
|
||||
= render(SearchableDropdownComponent.new(form: f,
|
||||
name: :primary_taxon_id,
|
||||
options: category_options,
|
||||
options: variant.primary_taxon_id ? [[variant.primary_taxon.name, variant.primary_taxon_id]] : [],
|
||||
selected_option: variant.primary_taxon_id,
|
||||
aria_label: t('.category_field_name'),
|
||||
include_blank: t('admin.products_v3.filters.select_category'),
|
||||
remote_url: admin_ajax_search_categories_url,
|
||||
placeholder_value: t('admin.products_v3.filters.select_category')))
|
||||
= error_message_on variant, :primary_taxon
|
||||
%td.col-tax_category.field.naked_inputs
|
||||
= render(SearchableDropdownComponent.new(form: f,
|
||||
name: :tax_category_id,
|
||||
options: tax_category_options,
|
||||
options: variant.tax_category_id ? [[variant.tax_category.name, variant.tax_category_id]] : [],
|
||||
selected_option: variant.tax_category_id,
|
||||
include_blank: t('.none_tax_category'),
|
||||
aria_label: t('.tax_category_field_name'),
|
||||
include_blank: t('.none_tax_category'),
|
||||
remote_url: admin_ajax_search_tax_categories_url,
|
||||
placeholder_value: t('.search_for_tax_categories')))
|
||||
= error_message_on variant, :tax_category
|
||||
- if variant_tag_enabled?(spree_current_user)
|
||||
@@ -88,6 +92,10 @@
|
||||
= render(VerticalEllipsisMenuComponent.new) do
|
||||
- if variant.persisted?
|
||||
= link_to t('admin.products_page.actions.edit'), edit_admin_product_variant_path(variant.product, variant)
|
||||
|
||||
- if variant.source_variants.empty? && allowed_source_producers.include?(variant.supplier)
|
||||
= link_to t('admin.products_page.actions.create_linked_variant'), admin_create_linked_variant_path(variant_id: variant.id, product_index:), 'data-turbo-method': :post
|
||||
|
||||
- if variant.product.variants.size > 1
|
||||
%a{ "data-controller": "modal-link", "data-action": "click->modal-link#setModalDataSetOnConfirm click->modal-link#open",
|
||||
"data-modal-link-target-value": "variant-delete-modal", "class": "delete",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-# locals: (variant:, linked_variant:, product_index:, variant_index:, producer_options:, category_options:, tax_category_options:)
|
||||
-# Pre-render the form, because you can't do it inside turbo stream block
|
||||
- variant_row = nil
|
||||
- fields_for("products][#{product_index}][variants_attributes", variant, index: variant_index) do |f|
|
||||
- variant_row = render(partial: 'variant_row', formats: :html,
|
||||
locals: { f:,
|
||||
variant:,
|
||||
producer_options:,
|
||||
category_options:,
|
||||
tax_category_options:})
|
||||
= turbo_stream.after dom_id(linked_variant) do
|
||||
%tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper slide-in",'data-variant-bulk-form-outlet': "#products-form"}
|
||||
= variant_row
|
||||
|
||||
= turbo_stream.append "flashes" do
|
||||
= render(partial: 'admin/shared/flashes', locals: { flashes: flash })
|
||||
@@ -6,7 +6,7 @@
|
||||
%p= t ".description"
|
||||
|
||||
%fieldset.no-border-top.no-border-bottom
|
||||
.row
|
||||
.row.field
|
||||
= f.label :email, t(:email)
|
||||
= f.email_field :email, placeholder: t('.eg_email_address'), data: { controller: "select-user" }, inputmode: "email", autocomplete: "off"
|
||||
= f.error_message_on :email
|
||||
|
||||
@@ -44,12 +44,17 @@ export default class BulkFormController extends Controller {
|
||||
}
|
||||
|
||||
// Register any new elements (may be called by another controller after dynamically adding fields)
|
||||
registerElements() {
|
||||
const registeredElements = Object.values(this.recordElements).flat();
|
||||
// Select only elements that haven't been registered yet
|
||||
const newElements = Array.from(this.form.elements).filter(
|
||||
(n) => !registeredElements.includes(n),
|
||||
);
|
||||
// May be called with array of elements to register, otherwise finds all un-registered elements.
|
||||
registerElements(eventOrElements = null) {
|
||||
let newElements;
|
||||
|
||||
if (Array.isArray(eventOrElements)) {
|
||||
newElements = eventOrElements;
|
||||
} else {
|
||||
const registeredElements = Object.values(this.recordElements).flat();
|
||||
// Select only elements that haven't been registered yet
|
||||
newElements = Array.from(this.form.elements).filter((n) => !registeredElements.includes(n));
|
||||
}
|
||||
|
||||
this.#registerElements(newElements);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
#addRemoteOptions(options) {
|
||||
// by default, for dropdown_input plugin, it's true. Otherwise for multi-select it's false
|
||||
// it should always be true so to invoke the onDropdownOpen to fetch options
|
||||
options.shouldOpen = true;
|
||||
this.openedByClick = false;
|
||||
|
||||
options.firstUrl = (query) => {
|
||||
@@ -91,12 +94,9 @@ export default class extends Controller {
|
||||
|
||||
options.load = this.#fetchOptions.bind(this);
|
||||
|
||||
options.onFocus = function () {
|
||||
this.control.load("", () => {});
|
||||
}.bind(this);
|
||||
|
||||
options.onDropdownOpen = function () {
|
||||
this.openedByClick = true;
|
||||
this.control.load("", () => {});
|
||||
}.bind(this);
|
||||
|
||||
options.onType = function () {
|
||||
|
||||
@@ -4,6 +4,8 @@ import OptionValueNamer from "js/services/option_value_namer";
|
||||
// Dynamically update related variant fields
|
||||
//
|
||||
export default class VariantController extends Controller {
|
||||
static outlets = ["bulk-form"];
|
||||
|
||||
connect() {
|
||||
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
|
||||
// It could automatically find (and cache a ref to) each dom element and get/set the values.
|
||||
@@ -40,6 +42,12 @@ export default class VariantController extends Controller {
|
||||
// on display_as changed; update unit_to_display
|
||||
// TODO: optimise to avoid unnecessary OptionValueNamer calc
|
||||
this.displayAs.addEventListener("input", this.#updateUnitDisplay.bind(this), { passive: true });
|
||||
|
||||
// Register with bulk products form to listen for changes. Used when dynamically appending variants.
|
||||
if (this.hasBulkFormOutlet) {
|
||||
const formElements = this.element.querySelectorAll("input, select, textarea, button");
|
||||
this.bulkFormOutlet.registerElements(formElements);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
.button, button {
|
||||
@include border-radius(0.5em);
|
||||
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
|
||||
&.x-small {
|
||||
@@ -65,7 +66,6 @@
|
||||
}
|
||||
|
||||
.button.primary, button.primary {
|
||||
font-family: $body-font;
|
||||
background: $orange-450;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,15 @@ redis_connection_settings = {
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = redis_connection_settings
|
||||
config.on(:startup) do
|
||||
# Load schedule file similar to sidekiq/cli.rb loading the main config.
|
||||
path = File.expand_path("../sidekiq_scheduler.yml", __dir__)
|
||||
erb = ERB.new(File.read(path), trim_mode: "-")
|
||||
|
||||
Sidekiq.schedule =
|
||||
YAML.safe_load(erb.result, permitted_classes: [Symbol], aliases: true)
|
||||
SidekiqScheduler::Scheduler.instance.reload_schedule!
|
||||
end
|
||||
end
|
||||
|
||||
Sidekiq.configure_client do |config|
|
||||
|
||||
@@ -717,8 +717,11 @@ en:
|
||||
delete: Delete
|
||||
remove: Remove
|
||||
preview: Preview
|
||||
create_linked_variant: Create linked variant
|
||||
image:
|
||||
edit: Edit
|
||||
variant_row:
|
||||
sourced_from: "Sourced from: %{source_name} (%{source_id}); Hub: %{hub_name}"
|
||||
product_preview:
|
||||
product_preview: Product preview
|
||||
shop_tab: Shop
|
||||
@@ -1098,6 +1101,8 @@ en:
|
||||
clone:
|
||||
success: Successfully cloned the product
|
||||
error: Unable to clone the product
|
||||
create_linked_variant:
|
||||
success: "Successfully created linked variant"
|
||||
tag_rules:
|
||||
rules_per_tag:
|
||||
one: "%{tag} has 1 rule"
|
||||
@@ -3901,6 +3906,7 @@ en:
|
||||
manage_products: "manage products"
|
||||
edit_profile: "edit profile"
|
||||
add_products_to_inventory: "add products to inventory"
|
||||
create_linked_variants: "create linked variants [BETA]"
|
||||
resources:
|
||||
could_not_delete_customer: 'Could not delete customer'
|
||||
product_import:
|
||||
|
||||
@@ -287,7 +287,7 @@ en_CA:
|
||||
customer_instructions: "Customer instructions"
|
||||
additional_information: "Additional information"
|
||||
connect_app:
|
||||
url: "https://n8n.openfoodnetwork.org/webhook/foodjustice/connect-enterprise"
|
||||
url: "https://n8n.openfoodnetwork.org/webhook-test/foodjustice/connect-enterprise"
|
||||
devise:
|
||||
passwords:
|
||||
spree_user:
|
||||
@@ -668,8 +668,11 @@ en_CA:
|
||||
delete: Delete
|
||||
remove: Remove
|
||||
preview: Preview
|
||||
create_linked_variant: Create linked variant
|
||||
image:
|
||||
edit: Edit
|
||||
variant_row:
|
||||
sourced_from: "Sourced from: %{source_name} (%{source_id}); Hub: %{hub_name}"
|
||||
product_preview:
|
||||
product_preview: Product preview
|
||||
shop_tab: Shop
|
||||
@@ -811,6 +814,7 @@ en_CA:
|
||||
bill_address: "Billing Address"
|
||||
ship_address: "Shipping Address"
|
||||
balance: "Balance"
|
||||
credit: "Available Credit"
|
||||
update_address_success: "Address updated successfully."
|
||||
update_address_error: "Sorry! Please input all of the required fields!"
|
||||
edit_bill_address: "Edit Billing Address"
|
||||
@@ -825,12 +829,16 @@ en_CA:
|
||||
guest_label: "Guest checkout"
|
||||
credit_owed: "Credit Owed"
|
||||
balance_due: "Balance Due"
|
||||
id: Id
|
||||
destroy:
|
||||
has_associated_subscriptions: "Delete failed: This customer has active subscriptions. Cancel them first."
|
||||
customer_account_transaction:
|
||||
index:
|
||||
available_credit: "Available credit: %{available_credit}"
|
||||
transaction_date: Transaction Date
|
||||
description: Description
|
||||
amount: Amount
|
||||
created_by: Created by
|
||||
running_balance: Running balance
|
||||
column_preferences:
|
||||
bulk_update:
|
||||
@@ -1020,6 +1028,8 @@ en_CA:
|
||||
clone:
|
||||
success: Successfully cloned the product
|
||||
error: Unable to clone the product
|
||||
create_linked_variant:
|
||||
success: "Successfully created linked variant"
|
||||
tag_rules:
|
||||
rules_per_tag:
|
||||
one: "%{tag} has 1 rule"
|
||||
@@ -1789,6 +1799,11 @@ en_CA:
|
||||
images: "Images"
|
||||
contact: "Contact"
|
||||
web: "Web Resources"
|
||||
stimulus_pagination:
|
||||
navigation: Pagination
|
||||
page: "Page %{number}"
|
||||
previous: Previous page
|
||||
next: Next page
|
||||
enterprise_issues:
|
||||
create_new: Create New
|
||||
resend_email: Resend Email
|
||||
@@ -2455,6 +2470,7 @@ en_CA:
|
||||
order_total: Total order
|
||||
order_payment: "Paying via:"
|
||||
no_payment_required: "No payment required"
|
||||
credit_used: "Credit used: %{amount}"
|
||||
customer_credit: Credit
|
||||
order_billing_address: Billing address
|
||||
order_delivery_on: Delivery on
|
||||
@@ -3439,6 +3455,7 @@ en_CA:
|
||||
no_orders_found: "No Orders Found"
|
||||
order_information: "Order Information"
|
||||
new_payment: "New Payment"
|
||||
credit_customer: Credit customer
|
||||
create_or_update_invoice: "Create or Update Invoice"
|
||||
date_completed: "Date Completed"
|
||||
amount: "Amount"
|
||||
@@ -3749,6 +3766,7 @@ en_CA:
|
||||
manage_products: "manage products"
|
||||
edit_profile: "edit profile"
|
||||
add_products_to_inventory: "add products to inventory"
|
||||
create_linked_variants: "create linked variants [BETA]"
|
||||
resources:
|
||||
could_not_delete_customer: 'Could not delete customer'
|
||||
product_import:
|
||||
@@ -4029,6 +4047,7 @@ en_CA:
|
||||
items_cannot_be_shipped: "Items cannot be shipped"
|
||||
gateway_config_unavailable: "Gateway config unavailable"
|
||||
gateway_error: "Payment failed"
|
||||
internal_payment_not_voidable: Payment not voidable
|
||||
more: "More"
|
||||
new_adjustment: "New adjustment"
|
||||
new_tax_category: "New Tax Category"
|
||||
@@ -4523,6 +4542,7 @@ en_CA:
|
||||
paypalexpress: "PayPal Express"
|
||||
stripesca: "Stripe SCA"
|
||||
taler: "Taler"
|
||||
customercredit: "Customer Credit"
|
||||
payments:
|
||||
source_forms:
|
||||
stripe:
|
||||
@@ -4530,6 +4550,7 @@ en_CA:
|
||||
submitting_payment: Submitting payment...
|
||||
paypal:
|
||||
no_payment_via_admin_backend: Paypal payments cannot be captured in the backoffice.
|
||||
customer_credit_successful: Customer has been successfully credited!
|
||||
products:
|
||||
image_upload_error: "Please upload the image in JPG, PNG, GIF, SVG or WEBP format."
|
||||
image_not_processable: "Image attachment is not a valid image."
|
||||
@@ -4845,6 +4866,7 @@ en_CA:
|
||||
orders: Orders
|
||||
cards: Credit Cards
|
||||
transactions: Transactions
|
||||
customer_account_transactions: Customer Transactions
|
||||
settings: Account Settings
|
||||
unconfirmed_email: "Pending email confirmation for: %{unconfirmed_email}. Your email address will be updated once the new email is confirmed."
|
||||
orders:
|
||||
@@ -4855,6 +4877,9 @@ en_CA:
|
||||
authorisation_required: Authorisation Required
|
||||
authorise: Authorize
|
||||
customer_account_transactions:
|
||||
title: Customer Transactions
|
||||
credit_available: "Credit available: %{credit}"
|
||||
transaction_date: Transaction Date
|
||||
description: Description
|
||||
amount: Amount
|
||||
running_balance: Running balance
|
||||
@@ -4997,3 +5022,22 @@ en_CA:
|
||||
invisible_captcha:
|
||||
sentence_for_humans: "Please leave empty"
|
||||
timestamp_error_message: "Please try again after 5 seconds."
|
||||
api_customer_credit: "API credit: %{description}"
|
||||
credit_payment_method:
|
||||
name: Customer credit
|
||||
description: Allow customer to pay with credit
|
||||
success: Payment with credit was sucessful
|
||||
void_success: Credit void was sucessful
|
||||
order_payment_description: "Customer credit: Payment for order: %{order_number}"
|
||||
order_void_description: "Customer credit: Refund for order: %{order_number}"
|
||||
errors:
|
||||
customer_not_found: Customer not found
|
||||
missing_payment: Missing payment
|
||||
credit_payment_method_missing: Credit payment method is missing
|
||||
no_credit_available: No credit available
|
||||
not_enough_credit_available: Not enough credit available
|
||||
orders:
|
||||
customer_credit_service:
|
||||
no_credit_owed: No credit owed
|
||||
credit_payment_method_missing: Customer credit payment method is missing, please check configuration
|
||||
refund_sucessful: Refund successful!
|
||||
|
||||
@@ -1432,7 +1432,7 @@ en_GB:
|
||||
legend: "Users"
|
||||
email_confirmation_notice_html: "Email confirmation is pending. We've sent a confirmation email to %{email}."
|
||||
resend: Resend
|
||||
contact: "Contact"
|
||||
contact: "Notifications"
|
||||
manager: "Manager"
|
||||
owner: 'Owner'
|
||||
contact_tip: "The manager who will receive enterprise emails for orders and notifications. Must have a confirmed email adress."
|
||||
|
||||
@@ -669,8 +669,11 @@ fr:
|
||||
delete: Supprimer
|
||||
remove: Supprimer
|
||||
preview: Prévisualisation
|
||||
create_linked_variant: Créer une variante liée
|
||||
image:
|
||||
edit: Modifier
|
||||
variant_row:
|
||||
sourced_from: "Source : %{source_name} ( %{source_id} ) ; Hub : %{hub_name}"
|
||||
product_preview:
|
||||
product_preview: Prévisualisation du produit
|
||||
shop_tab: Boutique
|
||||
@@ -1028,6 +1031,8 @@ fr:
|
||||
clone:
|
||||
success: Le produit a bien été dupliqué
|
||||
error: Impossible de dupliquer le produit
|
||||
create_linked_variant:
|
||||
success: "Variante liée créée avec succès"
|
||||
tag_rules:
|
||||
rules_per_tag:
|
||||
one: "%{tag} comporte une règle"
|
||||
@@ -3799,6 +3804,7 @@ fr:
|
||||
manage_products: "modifier les produits"
|
||||
edit_profile: "modifier le profil"
|
||||
add_products_to_inventory: "ajouter les produits au catalogue boutique"
|
||||
create_linked_variants: "créer des variantes liées [BÊTA]"
|
||||
resources:
|
||||
could_not_delete_customer: 'L''acheteur n''a pas pu être supprimé'
|
||||
product_import:
|
||||
|
||||
@@ -662,8 +662,11 @@ fr_BE:
|
||||
delete: Supprimer
|
||||
remove: Supprimer
|
||||
preview: Aperçu
|
||||
create_linked_variant: Créer une variante liée
|
||||
image:
|
||||
edit: Modifier
|
||||
variant_row:
|
||||
sourced_from: "Source : %{source_name} (%{source_id}); Hub : %{hub_name}"
|
||||
product_preview:
|
||||
product_preview: Aperçu du produit
|
||||
shop_tab: Comptoir
|
||||
@@ -1021,6 +1024,8 @@ fr_BE:
|
||||
clone:
|
||||
success: Le produit a bien été dupliqué
|
||||
error: Impossible de dupliquer le produit
|
||||
create_linked_variant:
|
||||
success: "Variante liée créée avec succès"
|
||||
tag_rules:
|
||||
rules_per_tag:
|
||||
one: "%{tag} comporte 1 règle"
|
||||
@@ -3774,6 +3779,7 @@ fr_BE:
|
||||
manage_products: "modifier les produits"
|
||||
edit_profile: "modifier le profil"
|
||||
add_products_to_inventory: "ajouter les produits au catalogue comptoir"
|
||||
create_linked_variants: "créer des variantes liées [BÊTA]"
|
||||
resources:
|
||||
could_not_delete_customer: 'L''acheteur·euse n''a pas pu être supprimé'
|
||||
product_import:
|
||||
|
||||
@@ -274,6 +274,10 @@ fr_CA:
|
||||
no_default_card: "Pas de carte de paiement par défaut pour cet acheteur"
|
||||
shipping_method:
|
||||
not_available_to_shop: "n'est pas disponible pour %{shop}"
|
||||
user_invitation:
|
||||
attributes:
|
||||
email:
|
||||
is_already_manager: est déjà gestionnaire!
|
||||
card_details: "Détalis de la carte"
|
||||
card_type: "Type de carte"
|
||||
card_type_is: "Type de carte"
|
||||
@@ -666,8 +670,11 @@ fr_CA:
|
||||
delete: Supprimer
|
||||
remove: Supprimer
|
||||
preview: Prévisualisation
|
||||
create_linked_variant: Créer une variante liée
|
||||
image:
|
||||
edit: Modifier
|
||||
variant_row:
|
||||
sourced_from: "Source : %{source_name} (%{source_id}); Hub : %{hub_name}"
|
||||
product_preview:
|
||||
product_preview: Prévisualisation du produit
|
||||
shop_tab: Boutique
|
||||
@@ -809,6 +816,7 @@ fr_CA:
|
||||
bill_address: "Adresse de facturation"
|
||||
ship_address: "Adresse de livraison"
|
||||
balance: "Solde"
|
||||
credit: "Crédit disponible"
|
||||
update_address_success: "Adresse mise à jour avec succès."
|
||||
update_address_error: "Oups! Veuillez remplir tous les champs obligatoires!"
|
||||
edit_bill_address: "Modifier l'adresse de facturation"
|
||||
@@ -823,12 +831,16 @@ fr_CA:
|
||||
guest_label: "Commande en mode invite"
|
||||
credit_owed: "Crédit dû"
|
||||
balance_due: "Solde dû"
|
||||
id: Id
|
||||
destroy:
|
||||
has_associated_subscriptions: "La suppression a planté : cet acheteur a des abonnements actifs. Veuillez d'abord les annuler."
|
||||
customer_account_transaction:
|
||||
index:
|
||||
available_credit: "Crédit disponible : %{available_credit}"
|
||||
transaction_date: Date de la transaction
|
||||
description: Description
|
||||
amount: Montant
|
||||
created_by: Créé par
|
||||
running_balance: Solde courant
|
||||
column_preferences:
|
||||
bulk_update:
|
||||
@@ -1020,6 +1032,8 @@ fr_CA:
|
||||
clone:
|
||||
success: Le produit a bien été dupliqué
|
||||
error: Impossible de dupliquer le produit
|
||||
create_linked_variant:
|
||||
success: "Variante liée créée avec succès"
|
||||
tag_rules:
|
||||
rules_per_tag:
|
||||
one: "%{tag} a 1 règle"
|
||||
@@ -1426,10 +1440,12 @@ fr_CA:
|
||||
show_hide_payment: 'Afficher ou Montrer les méthodes de paiement lors de la finalisation de commande'
|
||||
show_hide_order_cycles: 'Afficher ou Masquer les cycles de vente de ma boutique'
|
||||
users:
|
||||
description: Les utilisateurs autorisés à gérer cette entreprise.
|
||||
legend: "Utilisateurs"
|
||||
email_confirmation_notice_html: "L'email de confirmation n'a pas encore été validé. Il a été envoyé à %{email}."
|
||||
resend: Renvoyer
|
||||
contact: "Contact"
|
||||
manager: "Gestionnaire"
|
||||
owner: 'Manager principal'
|
||||
contact_tip: "Le manager qui recevra les emails de confirmation de commande et autres notifications de l'entreprise. Il doit avoir confirmé son adresse email pour pouvoir être sélectionné."
|
||||
owner_tip: Manager principal de cette entreprise.
|
||||
@@ -1440,6 +1456,8 @@ fr_CA:
|
||||
invite_manager: "Inviter un manager"
|
||||
email_confirmed: "Email confirmé"
|
||||
email_not_confirmed: "Email non confirmé"
|
||||
set_as_contact: "Configurer %{email} comme contact"
|
||||
set_as_owner: "Définir %{email} comme propriétaire"
|
||||
vouchers:
|
||||
legend: Bon de réduction
|
||||
voucher_code: Code promo
|
||||
@@ -1788,6 +1806,11 @@ fr_CA:
|
||||
images: "Images"
|
||||
contact: "Contact"
|
||||
web: "Liens web"
|
||||
stimulus_pagination:
|
||||
navigation: Pagination
|
||||
page: "Page %{number}"
|
||||
previous: Page précédente
|
||||
next: Page suivante
|
||||
enterprise_issues:
|
||||
create_new: Créer Nouveau
|
||||
resend_email: Renvoyer l'email
|
||||
@@ -2018,7 +2041,10 @@ fr_CA:
|
||||
user_invitations:
|
||||
new:
|
||||
back: Retour
|
||||
description: "Invitez un utilisateur à s'inscrire et à devenir gestionnaire de cette entreprise."
|
||||
eg_email_address: 'ex : l''adresse e-mail d''un utilisateur nouveau ou existant'
|
||||
email: Email
|
||||
invite_new_user: Inviter un nouvel utilisateur
|
||||
invite: Inviter
|
||||
vouchers:
|
||||
new:
|
||||
@@ -2454,6 +2480,7 @@ fr_CA:
|
||||
order_total: Total commande
|
||||
order_payment: "Payer via:"
|
||||
no_payment_required: "Pas de paiement requis"
|
||||
credit_used: "Crédit utilisé : %{amount}"
|
||||
customer_credit: Crédit
|
||||
order_billing_address: Adresse de facturation
|
||||
order_delivery_on: Livraison prévue
|
||||
@@ -3441,6 +3468,7 @@ fr_CA:
|
||||
no_orders_found: "Aucune commande trouvée"
|
||||
order_information: "Info commande"
|
||||
new_payment: "Nouveau paiement"
|
||||
credit_customer: Client à crédit
|
||||
create_or_update_invoice: "Créer ou mettre à jour la facture"
|
||||
date_completed: "Date d'opération"
|
||||
amount: "Montant"
|
||||
@@ -3762,6 +3790,7 @@ fr_CA:
|
||||
manage_products: "Gérer les produits"
|
||||
edit_profile: "modifier le profil"
|
||||
add_products_to_inventory: "ajouter les produits au catalogue boutique"
|
||||
create_linked_variants: "créer des variantes liées [BÊTA]"
|
||||
resources:
|
||||
could_not_delete_customer: 'L''acheteur n''a pas pu être supprimé'
|
||||
product_import:
|
||||
@@ -4076,6 +4105,7 @@ fr_CA:
|
||||
items_cannot_be_shipped: "Les produits ne peuvent pas être envoyés"
|
||||
gateway_config_unavailable: "Configuration de la passerelle indisponible"
|
||||
gateway_error: "Le paiement a échoué"
|
||||
internal_payment_not_voidable: Paiement non annulable
|
||||
more: "Plus"
|
||||
new_adjustment: "Nouvel ajustement"
|
||||
new_tax_category: "Nouvelle catégorie de taxe"
|
||||
@@ -4570,6 +4600,7 @@ fr_CA:
|
||||
paypalexpress: "PayPal Express"
|
||||
stripesca: "Stripe SCA"
|
||||
taler: "Taler"
|
||||
customercredit: "Crédit client"
|
||||
payments:
|
||||
source_forms:
|
||||
stripe:
|
||||
@@ -4577,6 +4608,7 @@ fr_CA:
|
||||
submitting_payment: Envoi du paiement...
|
||||
paypal:
|
||||
no_payment_via_admin_backend: 'Il n''est pas encore possible de payer avec Paypal via l''administration. '
|
||||
customer_credit_successful: Le client a été crédité avec succès!
|
||||
products:
|
||||
image_upload_error: "Veuillez utiliser une image au format JPG, PNG, GIF, SVG ou WEBP format."
|
||||
image_not_processable: "L'image n'est pas valide"
|
||||
@@ -4893,6 +4925,7 @@ fr_CA:
|
||||
orders: Commandes
|
||||
cards: Cartes bancaires
|
||||
transactions: Achats
|
||||
customer_account_transactions: Transactions des clients
|
||||
settings: Paramètres du Compte
|
||||
unconfirmed_email: "Attente de validation pour l'email: %{unconfirmed_email}. Votre adresse email sera mise à jour quand le nouvel email aura été confirmé."
|
||||
orders:
|
||||
@@ -4903,6 +4936,9 @@ fr_CA:
|
||||
authorisation_required: Autorisation nécessaire
|
||||
authorise: Autorise
|
||||
customer_account_transactions:
|
||||
title: Transactions des clients
|
||||
credit_available: "Crédit disponible : %{credit}"
|
||||
transaction_date: Date de la transaction
|
||||
description: Description
|
||||
amount: Montant
|
||||
running_balance: Solde courant
|
||||
@@ -5058,3 +5094,22 @@ fr_CA:
|
||||
invisible_captcha:
|
||||
sentence_for_humans: "Merci de laisser ce champ libre"
|
||||
timestamp_error_message: "S'il vous plaît réessayez après 5 secondes."
|
||||
api_customer_credit: "Crédit API : %{description}"
|
||||
credit_payment_method:
|
||||
name: Crédit client
|
||||
description: Autoriser le client à payer par crédit
|
||||
success: Le paiement par crédit a été effectué avec succès.
|
||||
void_success: L'annulation du crédit a réussi.
|
||||
order_payment_description: "Crédit client : Paiement de la commande :%{order_number}"
|
||||
order_void_description: "Crédit client : Remboursement pour la commande :%{order_number}"
|
||||
errors:
|
||||
customer_not_found: Client introuvable
|
||||
missing_payment: Paiement manquant
|
||||
credit_payment_method_missing: Le mode de paiement par crédit est manquant.
|
||||
no_credit_available: Aucune carte de paiement autorisée disponible
|
||||
not_enough_credit_available: Crédit disponible insuffisant
|
||||
orders:
|
||||
customer_credit_service:
|
||||
no_credit_owed: Aucun crédit dû
|
||||
credit_payment_method_missing: Le mode de paiement par crédit client est manquant. Veuillez vérifier la configuration.
|
||||
refund_sucessful: Remboursement effectué avec succès !
|
||||
|
||||
@@ -1298,10 +1298,10 @@ hu:
|
||||
add_new_button: '+ Új alapértelmezett szabály hozzáadása'
|
||||
no_tags_yet: Ehhez a vállalkozáshoz még nem tartozik címke
|
||||
add_new_tag: '+ Új címke hozzáadása'
|
||||
show_hide_variants: 'Változatok megjelenítése vagy elrejtése a kirakatomban'
|
||||
show_hide_variants: 'Termékváltozatok megjelenítése vagy elrejtése a kínálatomban'
|
||||
show_hide_shipping: 'Áruátadási módok megjelenítése vagy elrejtése fizetéskor'
|
||||
show_hide_payment: 'Fizetési módok megjelenítése vagy elrejtése a pénztárnál'
|
||||
show_hide_order_cycles: 'Rendelési ciklusok megjelenítése vagy elrejtése az online kirakatban'
|
||||
show_hide_payment: 'Fizetési módok megjelenítése vagy elrejtése rendelés leadásakor'
|
||||
show_hide_order_cycles: 'Rendelési ciklusok megjelenítése vagy elrejtése a kínálatomban'
|
||||
users:
|
||||
legend: "Felhasználók"
|
||||
email_confirmation_notice_html: "Az email megerősítés függőben van. Megerősítő emailt küldtünk a következő címre: %{email}."
|
||||
@@ -1878,6 +1878,7 @@ hu:
|
||||
new:
|
||||
back: Vissza
|
||||
description: "Hívj meg egy felhasználót, hogy regisztráljon és a vállalkozás menedzsere legyen."
|
||||
eg_email_address: írd be egy új vagy meglévő felhasználó e-mail címét
|
||||
email: Email
|
||||
invite_new_user: Új felhasználó meghívása
|
||||
invite: Meghívás
|
||||
@@ -4776,16 +4777,16 @@ hu:
|
||||
default_placeholder: Adj hozzá egy címkét
|
||||
tag_rule_form:
|
||||
tag_rules:
|
||||
shipping_method_tagged_top: "Áruátadási módok felcímkézve"
|
||||
shipping_method_tagged_bottom: "vannak:"
|
||||
payment_method_tagged_top: "Fizetési módok felcímkézve"
|
||||
payment_method_tagged_bottom: "vannak:"
|
||||
order_cycle_tagged_top: "Rendelési ciklusok felcímkézve"
|
||||
order_cycle_tagged_bottom: "vannak:"
|
||||
inventory_tagged_top: "Leltár változatok felcímkézve"
|
||||
inventory_tagged_bottom: "vannak:"
|
||||
variant_tagged_top: "Változat felcímkézve"
|
||||
variant_tagged_bottom: "vannak:"
|
||||
shipping_method_tagged_top: "Ezen címkéjű áruátadási módok:"
|
||||
shipping_method_tagged_bottom: "hatása:"
|
||||
payment_method_tagged_top: "Ezen címkéjű fizetési módok:"
|
||||
payment_method_tagged_bottom: "hatása:"
|
||||
order_cycle_tagged_top: "Ezen címkéjű rendelési ciklusok:"
|
||||
order_cycle_tagged_bottom: "hatása:"
|
||||
inventory_tagged_top: "Ezen címkéjű termékváltozatok:"
|
||||
inventory_tagged_bottom: "hatása:"
|
||||
variant_tagged_top: "Ezen címkéjű termékváltozatok:"
|
||||
variant_tagged_bottom: "hatása:"
|
||||
visible: LÁTHATÓ
|
||||
not_visible: NEM LÁTHATÓ
|
||||
tag_rule_group_form:
|
||||
|
||||
@@ -82,6 +82,14 @@ Openfoodnetwork::Application.routes.draw do
|
||||
delete 'products_v3/:id', to: 'products_v3#destroy', as: 'product_destroy'
|
||||
delete 'products_v3/destroy_variant/:id', to: 'products_v3#destroy_variant', as: 'destroy_variant'
|
||||
post 'clone/:id', to: 'products_v3#clone', as: 'clone_product'
|
||||
post 'products/create_linked_variant', to: 'products_v3#create_linked_variant', as: 'create_linked_variant'
|
||||
|
||||
scope :ajax_search, as: :ajax_search, controller: :ajax_search do
|
||||
get :producers
|
||||
get :categories
|
||||
get :tax_categories
|
||||
end
|
||||
|
||||
resources :product_preview, only: [:show]
|
||||
|
||||
resources :variant_overrides do
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Force manual loading of rails application to get all env variables from dotenv-rails when running whenever cmd
|
||||
require File.expand_path('../environment', __FILE__)
|
||||
|
||||
require 'whenever'
|
||||
require 'yaml'
|
||||
|
||||
# Learn more: http://github.com/javan/whenever
|
||||
|
||||
env "MAILTO", ENV["SCHEDULE_NOTIFICATIONS"] if ENV["SCHEDULE_NOTIFICATIONS"]
|
||||
|
||||
# If we use -e with a file containing specs, rspec interprets it and filters out our examples
|
||||
job_type :run_file, "cd :path; :environment_variable=:environment bundle exec script/rails runner :task :output"
|
||||
|
||||
every 1.month, at: '4:30am' do
|
||||
rake 'ofn:data:remove_transient_data'
|
||||
end
|
||||
|
||||
every 1.day, at: '2:45am' do
|
||||
rake 'db2fog:clean' if ENV['S3_BACKUPS_BUCKET']
|
||||
end
|
||||
|
||||
every 4.hours do
|
||||
rake 'db2fog:backup' if ENV['S3_BACKUPS_BUCKET']
|
||||
end
|
||||
@@ -7,15 +7,8 @@
|
||||
- default
|
||||
- mailers
|
||||
|
||||
:scheduler:
|
||||
:schedule:
|
||||
HeartbeatJob:
|
||||
every: ["5m", first_in: "0s"]
|
||||
SubscriptionPlacementJob:
|
||||
every: "5m"
|
||||
SubscriptionConfirmJob:
|
||||
every: "5m"
|
||||
TriggerOrderCyclesToOpenJob:
|
||||
every: "5m"
|
||||
OrderCycleClosingJob:
|
||||
every: "5m"
|
||||
# This config is loaded by sidekiq before dotenv is loading our server config.
|
||||
# Therefore we load the schedule later. See:
|
||||
#
|
||||
# - config/initializers/sidekiq.rb
|
||||
# - config/sidekiq_scheduler.yml
|
||||
|
||||
36
config/sidekiq_scheduler.yml
Normal file
36
config/sidekiq_scheduler.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
# Configure sidekiq-scheduler to run jobs.
|
||||
#
|
||||
# - https://github.com/sidekiq-scheduler/sidekiq-scheduler
|
||||
#
|
||||
# > Note that every and interval count from when the Sidekiq process (re)starts.
|
||||
# > So every: '48h' will never run if the Sidekiq process is restarted daily,
|
||||
# > for example. You can do every: ['48h', first_in: '0s'] to make the job run
|
||||
# > immediately after a restart, and then have the worker check when it was
|
||||
# > last run.
|
||||
#
|
||||
# Therefore, we use `cron` for jobs that should run at certain times like backups.
|
||||
HeartbeatJob:
|
||||
every: ["5m", first_in: "0s"]
|
||||
SubscriptionPlacementJob:
|
||||
every: "5m"
|
||||
SubscriptionConfirmJob:
|
||||
every: "5m"
|
||||
TriggerOrderCyclesToOpenJob:
|
||||
every: "5m"
|
||||
OrderCycleClosingJob:
|
||||
every: "5m"
|
||||
|
||||
backup:
|
||||
class: "RakeJob"
|
||||
args: ["db2fog:backup"]
|
||||
cron: "0 */4 * * *" # every 4 hours
|
||||
enabled: <%= ENV.fetch("S3_BACKUPS_BUCKET", false) && true %>
|
||||
backup_clean:
|
||||
class: "RakeJob"
|
||||
args: ["db2fog:clean"]
|
||||
cron: "45 2 * * *" # every day at 2:45am
|
||||
enabled: <%= ENV.fetch("S3_BACKUPS_BUCKET", false) && true %>
|
||||
ofn_clean:
|
||||
class: "RakeJob"
|
||||
args: ["ofn:data:remove_transient_data"]
|
||||
cron: "30 4 1 * *" # every month on the first at 4:30am
|
||||
19
db/migrate/20260211055758_create_variant_links.rb
Normal file
19
db/migrate/20260211055758_create_variant_links.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateVariantLinks < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
# Create a join table to join two variants. One is the source of the other.
|
||||
# Primary key index ensures uniqueness and assists querying. target_variant_id is the most
|
||||
# likely subject and so is first in the index.
|
||||
# An additional index for source_variant is also included because it may be helpful
|
||||
# (https://stackoverflow.com/questions/10790518/best-sql-indexes-for-join-table).
|
||||
create_table :variant_links, primary_key: [:target_variant_id, :source_variant_id] do |t|
|
||||
t.integer :source_variant_id, null: false, index: true
|
||||
t.integer :target_variant_id, null: false
|
||||
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
add_foreign_key :variant_links, :spree_variants, column: :source_variant_id
|
||||
add_foreign_key :variant_links, :spree_variants, column: :target_variant_id
|
||||
end
|
||||
end
|
||||
7
db/migrate/20260225022934_add_hub_to_spree_variants.rb
Normal file
7
db/migrate/20260225022934_add_hub_to_spree_variants.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddHubToSpreeVariants < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_reference :spree_variants, :hub, foreign_key: { to_table: :enterprises }
|
||||
end
|
||||
end
|
||||
12
db/schema.rb
12
db/schema.rb
@@ -1009,6 +1009,8 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
|
||||
t.bigint "supplier_id"
|
||||
t.float "variant_unit_scale"
|
||||
t.string "variant_unit_name", limit: 255
|
||||
t.bigint "hub_id"
|
||||
t.index ["hub_id"], name: "index_spree_variants_on_hub_id"
|
||||
t.index ["primary_taxon_id"], name: "index_spree_variants_on_primary_taxon_id"
|
||||
t.index ["product_id"], name: "index_variants_on_product_id"
|
||||
t.index ["shipping_category_id"], name: "index_spree_variants_on_shipping_category_id"
|
||||
@@ -1113,6 +1115,13 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
|
||||
t.datetime "updated_at", precision: nil, null: false
|
||||
end
|
||||
|
||||
create_table "variant_links", primary_key: ["target_variant_id", "source_variant_id"], force: :cascade do |t|
|
||||
t.integer "source_variant_id", null: false
|
||||
t.integer "target_variant_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["source_variant_id"], name: "index_variant_links_on_source_variant_id"
|
||||
end
|
||||
|
||||
create_table "variant_overrides", id: :serial, force: :cascade do |t|
|
||||
t.integer "variant_id", null: false
|
||||
t.integer "hub_id", null: false
|
||||
@@ -1262,6 +1271,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
|
||||
add_foreign_key "spree_tax_rates", "spree_zones", column: "zone_id", name: "spree_tax_rates_zone_id_fk"
|
||||
add_foreign_key "spree_users", "spree_addresses", column: "bill_address_id", name: "spree_users_bill_address_id_fk"
|
||||
add_foreign_key "spree_users", "spree_addresses", column: "ship_address_id", name: "spree_users_ship_address_id_fk"
|
||||
add_foreign_key "spree_variants", "enterprises", column: "hub_id"
|
||||
add_foreign_key "spree_variants", "enterprises", column: "supplier_id"
|
||||
add_foreign_key "spree_variants", "spree_products", column: "product_id", name: "spree_variants_product_id_fk"
|
||||
add_foreign_key "spree_variants", "spree_shipping_categories", column: "shipping_category_id"
|
||||
@@ -1278,6 +1288,8 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
|
||||
add_foreign_key "subscriptions", "spree_payment_methods", column: "payment_method_id", name: "subscriptions_payment_method_id_fk"
|
||||
add_foreign_key "subscriptions", "spree_shipping_methods", column: "shipping_method_id", name: "subscriptions_shipping_method_id_fk"
|
||||
add_foreign_key "tag_rules", "enterprises"
|
||||
add_foreign_key "variant_links", "spree_variants", column: "source_variant_id"
|
||||
add_foreign_key "variant_links", "spree_variants", column: "target_variant_id"
|
||||
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 "vouchers", "enterprises"
|
||||
|
||||
@@ -30,17 +30,19 @@ module OrderManagement
|
||||
other_permitted_producer_ids = EnterpriseRelationship.joins(:parent)
|
||||
.permitting(distributor.id).with_permission(:add_to_order_cycle)
|
||||
.merge(Enterprise.is_primary_producer)
|
||||
.pluck(:parent_id)
|
||||
.select(:parent_id)
|
||||
|
||||
# Append to the potentially gigantic array instead of using union, which creates a new array
|
||||
# The db IN statement won't care if there's a duplicate.
|
||||
other_permitted_producer_ids << distributor.id
|
||||
Enterprise.where(id: distributor.id)
|
||||
.select(:id)
|
||||
.or(Enterprise.where(id: other_permitted_producer_ids))
|
||||
end
|
||||
|
||||
def self.outgoing_exchange_variant_ids(distributor)
|
||||
ExchangeVariant.select("DISTINCT exchange_variants.variant_id").joins(:exchange)
|
||||
# DISTINCT is not required here since this subquery is used within an IN clause,
|
||||
# where duplicate values do not impact the result.
|
||||
ExchangeVariant.joins(:exchange)
|
||||
.where(exchanges: { incoming: false, receiver_id: distributor.id })
|
||||
.pluck(:variant_id)
|
||||
.select(:variant_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -86,6 +86,10 @@ module OpenFoodNetwork
|
||||
managed_and_related_enterprises_granting :manage_products
|
||||
end
|
||||
|
||||
def enterprises_granting_linked_variants
|
||||
related_enterprises_granting :create_linked_variants
|
||||
end
|
||||
|
||||
def manages_one_enterprise?
|
||||
@user.enterprises.length == 1
|
||||
end
|
||||
|
||||
@@ -1,54 +1,5 @@
|
||||
---
|
||||
http_interactions:
|
||||
- request:
|
||||
method: post
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/orders
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"order":{"amount":"KUDOS:10.0","summary":"Open Food Network order","fulfillment_url":"http://test.host/payment_gateways/taler/61"},"create_token":false}'
|
||||
headers:
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Taler Ruby
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Thu, 22 Jan 2026 04:43:32 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '42'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Expose-Headers:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
Via:
|
||||
- 1.1 Caddy
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |-
|
||||
{
|
||||
"order_id": "2026.022-0284X4GE8WKMJ"
|
||||
}
|
||||
recorded_at: Thu, 22 Jan 2026 04:43:33 GMT
|
||||
- request:
|
||||
method: get
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/orders/2026.022-0284X4GE8WKMJ
|
||||
@@ -103,4 +54,108 @@ http_interactions:
|
||||
}
|
||||
}
|
||||
recorded_at: Thu, 22 Jan 2026 04:43:34 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
- request:
|
||||
method: post
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"scope":"write"}'
|
||||
headers:
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Taler Ruby
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Fri, 20 Mar 2026 04:31:47 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '258'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Expose-Headers:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
Via:
|
||||
- 1.1 Caddy
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: |-
|
||||
{
|
||||
"access_token": "secret-token:J38S28NEJ6T07H1WP60F3T6PPWQYNKMR251TEZEX3CXP3SH54210",
|
||||
"token": "secret-token:J38S28NEJ6T07H1WP60F3T6PPWQYNKMR251TEZEX3CXP3SH54210",
|
||||
"scope": "write",
|
||||
"refreshable": false,
|
||||
"expiration": {
|
||||
"t_s": 1774067507
|
||||
}
|
||||
}
|
||||
recorded_at: Fri, 20 Mar 2026 04:31:48 GMT
|
||||
- request:
|
||||
method: post
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/orders
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"order":{"amount":"KUDOS:10.0","summary":"Open Food Network order","fulfillment_url":"http://test.host/payment_gateways/taler/198"},"create_token":false}'
|
||||
headers:
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Taler Ruby
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Fri, 20 Mar 2026 04:31:48 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '42'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Expose-Headers:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
Via:
|
||||
- 1.1 Caddy
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: |-
|
||||
{
|
||||
"order_id": "2026.079-0189PJNWMX6JA"
|
||||
}
|
||||
recorded_at: Fri, 20 Mar 2026 04:31:48 GMT
|
||||
recorded_with: VCR 6.4.0
|
||||
|
||||
@@ -47,6 +47,61 @@ http_interactions:
|
||||
"detail": "taler-order-id:12345"
|
||||
}
|
||||
recorded_at: Sat, 24 Jan 2026 00:51:31 GMT
|
||||
- request:
|
||||
method: post
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/token
|
||||
body:
|
||||
encoding: UTF-8
|
||||
string: '{"scope":"write"}'
|
||||
headers:
|
||||
Authorization:
|
||||
- "<HIDDEN-AUTHORIZATION-HEADER>"
|
||||
Accept:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- Taler Ruby
|
||||
Content-Type:
|
||||
- application/json
|
||||
Accept-Encoding:
|
||||
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
||||
response:
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
headers:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Fri, 20 Mar 2026 04:52:23 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
- '258'
|
||||
Connection:
|
||||
- keep-alive
|
||||
Access-Control-Allow-Origin:
|
||||
- "*"
|
||||
Access-Control-Expose-Headers:
|
||||
- "*"
|
||||
Cache-Control:
|
||||
- no-store
|
||||
Via:
|
||||
- 1.1 Caddy
|
||||
Strict-Transport-Security:
|
||||
- max-age=63072000; includeSubDomains; preload
|
||||
body:
|
||||
encoding: ASCII-8BIT
|
||||
string: |-
|
||||
{
|
||||
"access_token": "secret-token:176N5XTVVSR98FE6V4QR2Y35HKS61ZW5CK1BC7YEZYHX9M41N5GG",
|
||||
"token": "secret-token:176N5XTVVSR98FE6V4QR2Y35HKS61ZW5CK1BC7YEZYHX9M41N5GG",
|
||||
"scope": "write",
|
||||
"refreshable": false,
|
||||
"expiration": {
|
||||
"t_s": 1774068743
|
||||
}
|
||||
}
|
||||
recorded_at: Fri, 20 Mar 2026 04:52:23 GMT
|
||||
- request:
|
||||
method: get
|
||||
uri: https://backend.demo.taler.net/instances/sandbox/private/orders/2026.020-03R3ETNZZ0DVA
|
||||
@@ -70,7 +125,7 @@ http_interactions:
|
||||
Server:
|
||||
- nginx/1.26.3
|
||||
Date:
|
||||
- Sat, 24 Jan 2026 00:55:33 GMT
|
||||
- Fri, 20 Mar 2026 04:52:24 GMT
|
||||
Content-Type:
|
||||
- application/json
|
||||
Content-Length:
|
||||
@@ -205,5 +260,5 @@ http_interactions:
|
||||
"refund_details": [],
|
||||
"order_status_url": "https://backend.demo.taler.net/instances/sandbox/orders/2026.020-03R3ETNZZ0DVA"
|
||||
}
|
||||
recorded_at: Sat, 24 Jan 2026 00:55:32 GMT
|
||||
recorded_with: VCR 6.3.1
|
||||
recorded_at: Fri, 20 Mar 2026 04:52:24 GMT
|
||||
recorded_with: VCR 6.4.0
|
||||
|
||||
@@ -166,7 +166,6 @@ describe("TomSelectController", () => {
|
||||
expect(settings.searchField).toBe("label");
|
||||
expect(settings.load).toEqual(expect.any(Function));
|
||||
expect(settings.firstUrl).toEqual(expect.any(Function));
|
||||
expect(settings.onFocus).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it("fetches page 1 on focus", async () => {
|
||||
|
||||
@@ -16,3 +16,4 @@ describe "enterprise relationships", ->
|
||||
expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "manage products"
|
||||
expect(EnterpriseRelationships.permission_presentation("edit_profile")).toEqual "edit profile"
|
||||
expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "add products to inventory"
|
||||
expect(EnterpriseRelationships.permission_presentation("create_linked_variants")).toEqual "create linked variants [BETA]"
|
||||
|
||||
18
spec/jobs/rake_job_spec.rb
Normal file
18
spec/jobs/rake_job_spec.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "tasks/data/remove_transient_data"
|
||||
|
||||
RSpec.describe RakeJob do
|
||||
let(:task_string) { "ofn:data:remove_transient_data" }
|
||||
|
||||
it "calls the removal service" do
|
||||
expect(RemoveTransientData).to receive(:new).and_call_original
|
||||
RakeJob.perform_now(task_string)
|
||||
end
|
||||
|
||||
it "can be called several times" do
|
||||
expect(RemoveTransientData).to receive(:new).twice.and_call_original
|
||||
RakeJob.perform_now(task_string)
|
||||
RakeJob.perform_now(task_string)
|
||||
end
|
||||
end
|
||||
@@ -56,7 +56,7 @@ RSpec.describe PaymentMailer do
|
||||
payment = build(:payment)
|
||||
payment.order.distributor = build(:enterprise, name: "Carrot Castle")
|
||||
link = "https://taler.example.com/order/1"
|
||||
mail = PaymentMailer.refund_available(payment, link)
|
||||
mail = PaymentMailer.refund_available(payment.money.to_s, payment, link)
|
||||
|
||||
expect(mail.subject).to eq "Refund from Carrot Castle"
|
||||
expect(mail.body).to include "Your payment of $45.75 to Carrot Castle is being refunded."
|
||||
|
||||
@@ -364,6 +364,19 @@ RSpec.describe Spree::Ability do
|
||||
for: p2.variants.first)
|
||||
end
|
||||
|
||||
describe "create_linked_variant" do
|
||||
it "should not be able to create linked variant without permission" do
|
||||
is_expected.not_to have_ability([:create_linked_variant], for: p_related.variants.first)
|
||||
end
|
||||
|
||||
it "should be able to create linked variant when granted permission" do
|
||||
create(:enterprise_relationship, parent: s_related, child: s1,
|
||||
permissions_list: [:create_linked_variants])
|
||||
|
||||
is_expected.to have_ability([:create_linked_variant], for: p_related.variants.first)
|
||||
end
|
||||
end
|
||||
|
||||
it "should not be able to access admin actions on orders" do
|
||||
is_expected.not_to have_ability([:admin], for: Spree::Order)
|
||||
end
|
||||
@@ -729,6 +742,19 @@ RSpec.describe Spree::Ability do
|
||||
it "can request permitted enterprise fees for an order cycle" do
|
||||
is_expected.to have_ability([:for_order_cycle], for: EnterpriseFee)
|
||||
end
|
||||
|
||||
describe "create_linked_variant" do
|
||||
it "should not be able to create linked variant without permission" do
|
||||
is_expected.not_to have_ability([:create_linked_variant], for: p_related.variants.first)
|
||||
end
|
||||
|
||||
it "should be able to create linked variant when granted permission" do
|
||||
create(:enterprise_relationship, parent: s_related, child: d1,
|
||||
permissions_list: [:create_linked_variants])
|
||||
|
||||
is_expected.to have_ability([:create_linked_variant], for: p_related.variants.first)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'Order Cycle co-ordinator, distributor enterprise manager' do
|
||||
@@ -804,6 +830,19 @@ RSpec.describe Spree::Ability do
|
||||
it "has the ability to manage vouchers" do
|
||||
is_expected.to have_ability([:admin, :create], for: Voucher)
|
||||
end
|
||||
|
||||
describe "create_linked_variant for own enterprise" do
|
||||
it "should not be able to create own sourced variant without permission" do
|
||||
is_expected.not_to have_ability([:create_linked_variant], for: p1.variants.first)
|
||||
end
|
||||
|
||||
it "should be able to create own sourced variant when granted self permission" do
|
||||
create(:enterprise_relationship, parent: s1, child: s1,
|
||||
permissions_list: [:create_linked_variants])
|
||||
|
||||
is_expected.to have_ability([:create_linked_variant], for: p1.variants.first)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'enterprise owner' do
|
||||
|
||||
@@ -21,53 +21,6 @@ RSpec.describe Spree::CreditCard do
|
||||
|
||||
let(:credit_card) { described_class.new }
|
||||
|
||||
context "#can_capture?" do
|
||||
it "should be true if payment is pending" do
|
||||
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
||||
allow(payment).to receive(:pending?) { true }
|
||||
expect(credit_card.can_capture_and_complete_order?(payment)).to be_truthy
|
||||
end
|
||||
|
||||
it "should be true if payment is checkout" do
|
||||
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
||||
allow(payment).to receive_messages pending?: false,
|
||||
checkout?: true
|
||||
expect(credit_card.can_capture_and_complete_order?(payment)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "#can_void?" do
|
||||
it "should be true if payment is not void" do
|
||||
payment = build_stubbed(:payment)
|
||||
allow(payment).to receive(:void?) { false }
|
||||
expect(credit_card.can_void?(payment)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "#can_credit?" do
|
||||
it "should be false if payment is not completed" do
|
||||
payment = build_stubbed(:payment)
|
||||
allow(payment).to receive(:completed?) { false }
|
||||
expect(credit_card.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
|
||||
it "should be false when order payment_state is not 'credit_owed'" do
|
||||
payment = build_stubbed(:payment,
|
||||
order: create(:order, payment_state: 'paid'))
|
||||
allow(payment).to receive(:completed?) { true }
|
||||
expect(credit_card.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
|
||||
it "should be false when credit_allowed is zero" do
|
||||
payment = build_stubbed(:payment,
|
||||
order: create(:order, payment_state: 'credit_owed'))
|
||||
allow(payment).to receive_messages completed?: true,
|
||||
credit_allowed: 0
|
||||
|
||||
expect(credit_card.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context "#valid?" do
|
||||
it "should validate presence of number" do
|
||||
credit_card.attributes = valid_credit_card_attributes.except(:number)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Spree::Gateway do
|
||||
subject(:gateway) { test_gateway.new }
|
||||
let(:test_gateway) do
|
||||
Class.new(Spree::Gateway) do
|
||||
def provider_class
|
||||
@@ -15,13 +16,58 @@ RSpec.describe Spree::Gateway do
|
||||
|
||||
it "passes through all arguments on a method_missing call" do
|
||||
expect(Rails.env).to receive(:local?).and_return(false)
|
||||
gateway = test_gateway.new
|
||||
expect(gateway.provider).to receive(:imaginary_method).with('foo')
|
||||
gateway.imaginary_method('foo')
|
||||
end
|
||||
|
||||
it "raises an error in test env" do
|
||||
gateway = test_gateway.new
|
||||
expect { gateway.imaginary_method('foo') }.to raise_error StandardError
|
||||
end
|
||||
|
||||
describe "#can_capture?" do
|
||||
it "should be true if payment is pending" do
|
||||
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
||||
allow(payment).to receive(:pending?) { true }
|
||||
expect(gateway.can_capture_and_complete_order?(payment)).to be_truthy
|
||||
end
|
||||
|
||||
it "should be true if payment is checkout" do
|
||||
payment = build_stubbed(:payment, created_at: Time.zone.now)
|
||||
allow(payment).to receive_messages pending?: false,
|
||||
checkout?: true
|
||||
expect(gateway.can_capture_and_complete_order?(payment)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#can_void?" do
|
||||
it "should be true if payment is not void" do
|
||||
payment = build_stubbed(:payment)
|
||||
allow(payment).to receive(:void?) { false }
|
||||
expect(gateway.can_void?(payment)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#can_credit?" do
|
||||
it "should be false if payment is not completed" do
|
||||
payment = build_stubbed(:payment)
|
||||
allow(payment).to receive(:completed?) { false }
|
||||
expect(gateway.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
|
||||
it "should be false when order payment_state is not 'credit_owed'" do
|
||||
payment = build_stubbed(:payment,
|
||||
order: create(:order, payment_state: 'paid'))
|
||||
allow(payment).to receive(:completed?) { true }
|
||||
expect(gateway.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
|
||||
it "should be false when credit_allowed is zero" do
|
||||
payment = build_stubbed(:payment,
|
||||
order: create(:order, payment_state: 'credit_owed'))
|
||||
allow(payment).to receive_messages completed?: true,
|
||||
credit_allowed: 0
|
||||
|
||||
expect(gateway.can_credit?(payment)).to be_falsy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,16 +10,38 @@ RSpec.describe Spree::PaymentMethod::Taler do
|
||||
)
|
||||
}
|
||||
let(:backend_url) { "https://backend.demo.taler.net/instances/sandbox" }
|
||||
let(:token_url) { "#{backend_url}/private/token" }
|
||||
|
||||
describe "#external_payment_url", vcr: true do
|
||||
it "creates an order reference and retrieves a URL to pay at" do
|
||||
describe "#external_payment_url" do
|
||||
it "creates an order reference and retrieves a URL to pay at", vcr: true do
|
||||
order = create(:order_ready_for_confirmation, payment_method: taler)
|
||||
|
||||
url = subject.external_payment_url(order:)
|
||||
expect(url).to eq "#{backend_url}/orders/2026.022-0284X4GE8WKMJ"
|
||||
expect(url).to start_with "#{backend_url}/orders/"
|
||||
expect(url).to match "orders/20...[0-9A-Z-]{17}$"
|
||||
|
||||
payment = order.payments.last.reload
|
||||
expect(payment.response_code).to match "2026.022-0284X4GE8WKMJ"
|
||||
expect(payment.response_code).to match "20...[0-9A-Z-]{17}$"
|
||||
end
|
||||
|
||||
it "creates the Taler order with the right currency" do
|
||||
order = create(:order_ready_for_confirmation, payment_method: taler)
|
||||
|
||||
backend_url = "https://taler.example.com"
|
||||
token_url = "https://taler.example.com/private/token"
|
||||
order_url = "https://taler.example.com/private/orders"
|
||||
taler = Spree::PaymentMethod::Taler.new(
|
||||
preferred_backend_url: "https://taler.example.com",
|
||||
preferred_api_key: "sandbox",
|
||||
)
|
||||
|
||||
stub_request(:post, token_url).to_return(body: { token: "1234" }.to_json)
|
||||
stub_request(:post, order_url)
|
||||
.with(body: /"amount":"AUD:10.0"/)
|
||||
.to_return(body: { order_id: "one" }.to_json)
|
||||
|
||||
url = taler.external_payment_url(order:)
|
||||
expect(url).to eq "#{backend_url}/orders/one"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,6 +51,10 @@ RSpec.describe Spree::PaymentMethod::Taler do
|
||||
let(:payment) { build(:payment, response_code: "taler-order-7") }
|
||||
let(:order_url) { "#{backend_url}/private/orders/taler-order-7" }
|
||||
|
||||
before do
|
||||
stub_request(:post, token_url).to_return(body: { token: "12345" }.to_json)
|
||||
end
|
||||
|
||||
it "returns an ActiveMerchant response" do
|
||||
order_status = "paid"
|
||||
stub_request(:get, order_url).to_return(body: { order_status: }.to_json)
|
||||
@@ -50,6 +76,50 @@ RSpec.describe Spree::PaymentMethod::Taler do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#credit" do
|
||||
let(:order_endpoint) { "#{backend_url}/private/orders/taler-order-8" }
|
||||
let(:refund_endpoint) { "#{order_endpoint}/refund" }
|
||||
let(:taler_refund_uri) {
|
||||
"taler://refund/backend.demo.taler.net/instances/sandbox/taler-order-8/"
|
||||
}
|
||||
|
||||
before do
|
||||
stub_request(:post, token_url).to_return(body: { token: "12345" }.to_json)
|
||||
end
|
||||
|
||||
it "starts the refund process" do
|
||||
order_status = { order_status: "paid" }
|
||||
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
|
||||
stub_request(:post, refund_endpoint).to_return(body: { taler_refund_uri: }.to_json)
|
||||
|
||||
order = create(:completed_order_with_totals)
|
||||
order.payments.create(
|
||||
amount: order.total, state: :completed,
|
||||
payment_method: taler,
|
||||
response_code: "taler-order-8",
|
||||
)
|
||||
expect {
|
||||
response = taler.credit(100, "taler-order-8", { payment: order.payments[0] })
|
||||
expect(response.success?).to eq true
|
||||
}.to enqueue_mail(PaymentMailer, :refund_available)
|
||||
end
|
||||
|
||||
it "raises an error if payment hasn't been taken yet" do
|
||||
order_status = { order_status: "claimed" }
|
||||
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
|
||||
|
||||
order = create(:completed_order_with_totals)
|
||||
order.payments.create(
|
||||
amount: order.total, state: :completed,
|
||||
payment_method: taler,
|
||||
response_code: "taler-order-8",
|
||||
)
|
||||
expect {
|
||||
taler.credit(100, "taler-order-8", { payment: order.payments[0] })
|
||||
}.to raise_error StandardError, "Unsupported action"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#void" do
|
||||
let(:order_endpoint) { "#{backend_url}/private/orders/taler-order-8" }
|
||||
let(:refund_endpoint) { "#{order_endpoint}/refund" }
|
||||
@@ -57,6 +127,10 @@ RSpec.describe Spree::PaymentMethod::Taler do
|
||||
"taler://refund/backend.demo.taler.net/instances/sandbox/taler-order-8/"
|
||||
}
|
||||
|
||||
before do
|
||||
stub_request(:post, token_url).to_return(body: { token: "12345" }.to_json)
|
||||
end
|
||||
|
||||
it "starts the refund process" do
|
||||
order_status = {
|
||||
order_status: "paid",
|
||||
|
||||
@@ -855,7 +855,8 @@ RSpec.describe Spree::Payment do
|
||||
|
||||
describe "available actions" do
|
||||
context "for most gateways" do
|
||||
let(:payment) { build_stubbed(:payment, source: build_stubbed(:credit_card)) }
|
||||
let(:payment) { build_stubbed(:payment, payment_method:) }
|
||||
let(:payment_method) { Spree::Gateway::StripeSCA.new }
|
||||
|
||||
it "can capture and void" do
|
||||
expect(payment.actions).to match_array %w(capture_and_complete_order void)
|
||||
|
||||
@@ -8,6 +8,7 @@ RSpec.describe Spree::Variant do
|
||||
it { is_expected.to have_many :semantic_links }
|
||||
it { is_expected.to belong_to(:product).required }
|
||||
it { is_expected.to belong_to(:supplier).required }
|
||||
it { is_expected.to belong_to(:hub).optional }
|
||||
it { is_expected.to have_many(:inventory_units) }
|
||||
it { is_expected.to have_many(:line_items) }
|
||||
it { is_expected.to have_many(:stock_items) }
|
||||
@@ -20,6 +21,9 @@ RSpec.describe Spree::Variant do
|
||||
it { is_expected.to have_many(:inventory_items) }
|
||||
it { is_expected.to have_many(:supplier_properties).through(:supplier) }
|
||||
|
||||
it { is_expected.to have_many(:source_variants).through(:variant_links_as_target) }
|
||||
it { is_expected.to have_many(:target_variants).through(:variant_links_as_source) }
|
||||
|
||||
describe "shipping category" do
|
||||
it "sets a shipping category if none provided" do
|
||||
variant = build(:variant, shipping_category: nil)
|
||||
@@ -1001,4 +1005,30 @@ RSpec.describe Spree::Variant do
|
||||
expect(variant.unit_presentation).to eq "My display"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create_linked_variant" do
|
||||
let(:user) { create(:user, enterprises: [enterprise]) }
|
||||
let(:supplier) { variant.supplier }
|
||||
let(:enterprise) { create(:enterprise) }
|
||||
|
||||
context "with create_linked_variants permissions on supplier" do
|
||||
let!(:enterprise_relationship) {
|
||||
create(:enterprise_relationship,
|
||||
parent: supplier,
|
||||
child: enterprise,
|
||||
permissions_list: [:create_linked_variants])
|
||||
}
|
||||
let(:variant) { create(:variant, price: 10.95, on_demand: false, on_hand: 5) }
|
||||
|
||||
it "clones the variant, retaining a link to the source" do
|
||||
linked_variant = variant.create_linked_variant(user)
|
||||
|
||||
expect(linked_variant.source_variants).to eq [variant]
|
||||
expect(linked_variant.hub).to eq enterprise
|
||||
expect(linked_variant.price).to eq 10.95
|
||||
expect(linked_variant.on_demand).to eq false
|
||||
expect(linked_variant.on_hand).to eq 5
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
285
spec/requests/admin/ajax_search_spec.rb
Normal file
285
spec/requests/admin/ajax_search_spec.rb
Normal file
@@ -0,0 +1,285 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "/admin/ajax_search" do
|
||||
include AuthenticationHelper
|
||||
|
||||
let(:admin_user) { create(:admin_user) }
|
||||
let(:regular_user) { create(:user) }
|
||||
|
||||
describe "GET /admin/ajax_search/producers" do
|
||||
context "when user is not logged in" do
|
||||
it "redirects to login" do
|
||||
get admin_ajax_search_producers_path
|
||||
|
||||
expect(response).to redirect_to %r|#/login$|
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is logged in without permissions" do
|
||||
before { login_as regular_user }
|
||||
|
||||
it "redirects to unauthorized" do
|
||||
get admin_ajax_search_producers_path
|
||||
|
||||
expect(response).to redirect_to('/unauthorized')
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is an admin" do
|
||||
before { login_as admin_user }
|
||||
|
||||
let!(:producer1) { create(:supplier_enterprise, name: "Apple Farm") }
|
||||
let!(:producer2) { create(:supplier_enterprise, name: "Berry Farm") }
|
||||
let!(:producer3) { create(:supplier_enterprise, name: "Cherry Orchard") }
|
||||
let!(:distributor) { create(:distributor_enterprise, name: "Distributor") }
|
||||
|
||||
it "returns producers sorted alphabetically by name" do
|
||||
get admin_ajax_search_producers_path
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response["results"].pluck("label")).to eq(['Apple Farm', 'Berry Farm',
|
||||
'Cherry Orchard'])
|
||||
expect(json_response["pagination"]["more"]).to be false
|
||||
end
|
||||
|
||||
it "filters producers by search query" do
|
||||
get admin_ajax_search_producers_path, params: { q: "berry" }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['Berry Farm'])
|
||||
expect(json_response["results"].pluck("value")).to eq([producer2.id])
|
||||
end
|
||||
|
||||
it "filters are case insensitive" do
|
||||
get admin_ajax_search_producers_path, params: { q: "BERRY" }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['Berry Farm'])
|
||||
end
|
||||
|
||||
it "filters with partial matches" do
|
||||
get admin_ajax_search_producers_path, params: { q: "Farm" }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['Apple Farm', 'Berry Farm'])
|
||||
end
|
||||
|
||||
it "excludes non-producer enterprises" do
|
||||
get admin_ajax_search_producers_path
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).not_to include('Distributor')
|
||||
end
|
||||
|
||||
context "with more than 30 producers" do
|
||||
before do
|
||||
create_list(:supplier_enterprise, 35) do |enterprise, i|
|
||||
enterprise.update!(name: "Producer #{(i + 1).to_s.rjust(2, '0')}")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns first page with 30 results and more flag as true" do
|
||||
get admin_ajax_search_producers_path, params: { page: 1 }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].length).to eq(30)
|
||||
expect(json_response["pagination"]["more"]).to be true
|
||||
end
|
||||
|
||||
it "returns remaining results on second page with more flag as false" do
|
||||
get admin_ajax_search_producers_path, params: { page: 2 }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].length).to eq(8)
|
||||
expect(json_response["pagination"]["more"]).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when user has enterprise permissions" do
|
||||
let!(:my_producer) { create(:supplier_enterprise, name: "My Producer") }
|
||||
let!(:other_producer) { create(:supplier_enterprise, name: "Other Producer") }
|
||||
let(:user_with_producer) { create(:user, enterprises: [my_producer]) }
|
||||
|
||||
before { login_as user_with_producer }
|
||||
|
||||
it "returns only managed producers" do
|
||||
get admin_ajax_search_producers_path
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['My Producer'])
|
||||
expect(json_response["results"].pluck("label")).not_to include('Other Producer')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /admin/ajax_search/categories" do
|
||||
context "when user is not logged in" do
|
||||
it "redirects to login" do
|
||||
get admin_ajax_search_categories_path
|
||||
|
||||
expect(response).to redirect_to %r|#/login$|
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is logged in without permissions" do
|
||||
before { login_as regular_user }
|
||||
|
||||
it "redirects to unauthorized" do
|
||||
get admin_ajax_search_categories_path
|
||||
|
||||
expect(response).to redirect_to('/unauthorized')
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is an admin" do
|
||||
before { login_as admin_user }
|
||||
|
||||
let!(:category1) { create(:taxon, name: "Vegetables") }
|
||||
let!(:category2) { create(:taxon, name: "Fruits") }
|
||||
let!(:category3) { create(:taxon, name: "Dairy") }
|
||||
|
||||
it "returns categories sorted alphabetically by name" do
|
||||
get admin_ajax_search_categories_path
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response["results"].pluck("label")).to eq(['Dairy', 'Fruits', 'Vegetables'])
|
||||
expect(json_response["pagination"]["more"]).to be false
|
||||
end
|
||||
|
||||
it "filters categories by search query" do
|
||||
get admin_ajax_search_categories_path, params: { q: "fruit" }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['Fruits'])
|
||||
expect(json_response["results"].pluck("value")).to eq([category2.id])
|
||||
end
|
||||
|
||||
it "filters are case insensitive" do
|
||||
get admin_ajax_search_categories_path, params: { q: "VEGETABLES" }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['Vegetables'])
|
||||
end
|
||||
|
||||
it "filters with partial matches" do
|
||||
get admin_ajax_search_categories_path, params: { q: "ege" }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['Vegetables'])
|
||||
end
|
||||
|
||||
context "with more than 30 categories" do
|
||||
before do
|
||||
create_list(:taxon, 35) do |taxon, i|
|
||||
taxon.update!(name: "Category #{(i + 1).to_s.rjust(2, '0')}")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns first page with 30 results and more flag as true" do
|
||||
get admin_ajax_search_categories_path, params: { page: 1 }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].length).to eq(30)
|
||||
expect(json_response["pagination"]["more"]).to be true
|
||||
end
|
||||
|
||||
it "returns remaining results on second page with more flag as false" do
|
||||
get admin_ajax_search_categories_path, params: { page: 2 }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].length).to eq(8)
|
||||
expect(json_response["pagination"]["more"]).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /admin/ajax_search/tax_categories" do
|
||||
context "when user is not logged in" do
|
||||
it "redirects to login" do
|
||||
get admin_ajax_search_tax_categories_path
|
||||
|
||||
expect(response).to redirect_to %r|#/login$|
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is logged in without permissions" do
|
||||
before { login_as regular_user }
|
||||
|
||||
it "redirects to unauthorized" do
|
||||
get admin_ajax_search_tax_categories_path
|
||||
|
||||
expect(response).to redirect_to('/unauthorized')
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is an admin" do
|
||||
before { login_as admin_user }
|
||||
|
||||
let!(:tax_cat1) { create(:tax_category, name: "GST") }
|
||||
let!(:tax_cat2) { create(:tax_category, name: "VAT") }
|
||||
let!(:tax_cat3) { create(:tax_category, name: "No Tax") }
|
||||
|
||||
it "returns tax categories sorted alphabetically by name" do
|
||||
get admin_ajax_search_tax_categories_path
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response["results"].pluck("label")).to eq(['GST', 'No Tax', 'VAT'])
|
||||
expect(json_response["pagination"]["more"]).to be false
|
||||
end
|
||||
|
||||
it "filters tax categories by search query" do
|
||||
get admin_ajax_search_tax_categories_path, params: { q: "vat" }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['VAT'])
|
||||
expect(json_response["results"].pluck("value")).to eq([tax_cat2.id])
|
||||
end
|
||||
|
||||
it "filters are case insensitive" do
|
||||
get admin_ajax_search_tax_categories_path, params: { q: "GST" }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['GST'])
|
||||
end
|
||||
|
||||
it "filters with partial matches" do
|
||||
get admin_ajax_search_tax_categories_path, params: { q: "tax" }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].pluck("label")).to eq(['No Tax'])
|
||||
end
|
||||
|
||||
context "with more than 30 tax categories" do
|
||||
before do
|
||||
create_list(:tax_category, 35) do |tax_cat, i|
|
||||
tax_cat.update!(name: "Tax Category #{(i + 1).to_s.rjust(2, '0')}")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns first page with 30 results and more flag as true" do
|
||||
get admin_ajax_search_tax_categories_path, params: { page: 1 }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].length).to eq(30)
|
||||
expect(json_response["pagination"]["more"]).to be true
|
||||
end
|
||||
|
||||
it "returns remaining results on second page with more flag as false" do
|
||||
get admin_ajax_search_tax_categories_path, params: { page: 2 }
|
||||
|
||||
json_response = response.parsed_body
|
||||
expect(json_response["results"].length).to eq(8)
|
||||
expect(json_response["pagination"]["more"]).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -59,4 +59,71 @@ RSpec.describe "Admin::ProductsV3" do
|
||||
expect(response).to redirect_to('/unauthorized')
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /admin/products/create_linked_variant" do
|
||||
let(:enterprise) { create(:supplier_enterprise) }
|
||||
let(:user) { create(:user, enterprises: [enterprise]) }
|
||||
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:variant) { create(:variant, display_name: "Original variant", supplier: supplier) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "checks for permission" do
|
||||
params = { variant_id: variant.id, product_index: 1 }
|
||||
|
||||
expect {
|
||||
post(admin_create_linked_variant_path, as: :turbo_stream, params:)
|
||||
expect(response).to redirect_to('/unauthorized')
|
||||
}.not_to change { variant.product.variants.count }
|
||||
end
|
||||
|
||||
context "With create_linked_variants permissions on supplier" do
|
||||
let!(:enterprise_relationship) {
|
||||
create(:enterprise_relationship,
|
||||
parent: supplier,
|
||||
child: enterprise,
|
||||
permissions_list: [:create_linked_variants])
|
||||
}
|
||||
|
||||
it "clones the variant, retaining link as source" do
|
||||
params = { variant_id: variant.id, product_index: 1 }
|
||||
|
||||
expect {
|
||||
post(admin_create_linked_variant_path, as: :turbo_stream, params:)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to match "Original variant" # cloned variant name
|
||||
}.to change { variant.product.variants.count }.by(1)
|
||||
|
||||
new_variant = variant.product.variants.order(:id).last
|
||||
# The new variant is a target of the original. It is a "sourced" variant.
|
||||
expect(variant.target_variants.first).to eq new_variant
|
||||
# The new variant's source is the original
|
||||
expect(new_variant.source_variants.first).to eq variant
|
||||
end
|
||||
|
||||
context "and I'm also owner of another enterprise" do
|
||||
let!(:enterprise2) { create(:enterprise) }
|
||||
let(:user) { create(:user, enterprises: [enterprise, enterprise2]) }
|
||||
|
||||
it "clones the variant, owned by my enterprise that has permission" do
|
||||
enterprise2.owner = user
|
||||
params = { variant_id: variant.id, product_index: 1 }
|
||||
|
||||
expect {
|
||||
post(admin_create_linked_variant_path, as: :turbo_stream, params:)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
}.to change { variant.product.variants.count }.by(1)
|
||||
|
||||
# The new variant is owned by my enterprise that has permission, not the other one
|
||||
new_variant = variant.product.variants.order(:id).last
|
||||
expect(new_variant.hub).to eq enterprise
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -157,8 +157,6 @@ RSpec.describe Spree::Admin::PaymentsController do
|
||||
|
||||
context "with no payment source" do
|
||||
it "redirect to payments page" do
|
||||
allow(payment).to receive(:payment_source).and_return(nil)
|
||||
|
||||
put(
|
||||
"/admin/orders/#{order.number}/payments/#{order.payments.first.id}/fire?e=void",
|
||||
params: {},
|
||||
|
||||
@@ -42,6 +42,7 @@ RSpec.describe Payments::WebhookPayload do
|
||||
}
|
||||
},
|
||||
order: {
|
||||
number: order.number,
|
||||
total: order.total,
|
||||
currency: order.currency,
|
||||
line_items: line_items
|
||||
@@ -72,6 +73,7 @@ RSpec.describe Payments::WebhookPayload do
|
||||
}
|
||||
},
|
||||
order: {
|
||||
number: "R555555555",
|
||||
total: 0.00,
|
||||
currency: "AUD",
|
||||
line_items: [
|
||||
|
||||
@@ -56,6 +56,7 @@ RSpec.describe Payments::WebhookService do
|
||||
}
|
||||
},
|
||||
order: {
|
||||
number: order.number,
|
||||
total: order.total,
|
||||
currency: order.currency,
|
||||
line_items: line_items
|
||||
|
||||
@@ -19,12 +19,25 @@ module TomSelectHelper
|
||||
tomselect_wrapper.find(:css, '.ts-dropdown div.create').click
|
||||
end
|
||||
|
||||
# Searches for and selects an option in a TomSelect dropdown with search functionality.
|
||||
# @param value [String] The text to search for and select from the dropdown
|
||||
# @param options [Hash] Configuration options
|
||||
# @option options [String] :from The name/id of the select field
|
||||
# @option options [Boolean] :remote_search If true, waits for search loading after interactions
|
||||
#
|
||||
# @example
|
||||
# tomselect_search_and_select("Apple", from: "fruit_selector")
|
||||
# tomselect_search_and_select("California", from: "state", remote_search: true)
|
||||
def tomselect_search_and_select(value, options)
|
||||
tomselect_wrapper = page.find_field(options[:from]).sibling(".ts-wrapper")
|
||||
tomselect_wrapper.find(".ts-control").click
|
||||
expect_tomselect_loading_completion(tomselect_wrapper, options)
|
||||
|
||||
# Use send_keys as setting the value directly doesn't trigger the search
|
||||
tomselect_wrapper.find(".ts-dropdown input.dropdown-input").send_keys(value)
|
||||
tomselect_wrapper.find(".ts-dropdown .ts-dropdown-content .option.active", text: value).click
|
||||
expect_tomselect_loading_completion(tomselect_wrapper, options)
|
||||
|
||||
tomselect_wrapper.find(".ts-dropdown .ts-dropdown-content .option", text: value).click
|
||||
end
|
||||
|
||||
def tomselect_select(value, options)
|
||||
@@ -64,4 +77,52 @@ module TomSelectHelper
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Validates both available options and selected options in a TomSelect dropdown.
|
||||
# @param from [String] The name/id of the select field
|
||||
# @param existing_options [Array<String>] List of options that should be available in the dropdown
|
||||
# @param selected_options [Array<String>] List of options that should currently be selected
|
||||
#
|
||||
# @example
|
||||
# expect_tomselect_existing_with_selected_options(
|
||||
# from: "category_selector",
|
||||
# existing_options: ["Fruit", "Vegetables", "Dairy"],
|
||||
# selected_options: ["Fruit"]
|
||||
# )
|
||||
def expect_tomselect_existing_with_selected_options(from:, existing_options:, selected_options:)
|
||||
tomselect_wrapper = page.find_field(from).sibling(".ts-wrapper")
|
||||
tomselect_control = tomselect_wrapper.find('.ts-control')
|
||||
|
||||
tomselect_control.click # open the dropdown (would work for remote vs non-remote dropdowns)
|
||||
|
||||
# validate existing options are present in the dropdown
|
||||
within(tomselect_wrapper) do
|
||||
existing_options.each do |option|
|
||||
expect(page).to have_css(
|
||||
".ts-dropdown .ts-dropdown-content .option",
|
||||
text: option
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# validate selected options are selected in the dropdown
|
||||
within(tomselect_wrapper) do
|
||||
selected_options.each do |option|
|
||||
expect(page).to have_css(
|
||||
"div[data-ts-item]",
|
||||
text: option
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# close the dropdown by clicking on the already selected option
|
||||
tomselect_wrapper.find(".ts-dropdown .ts-dropdown-content .option.active").click
|
||||
end
|
||||
|
||||
def expect_tomselect_loading_completion(tomselect_wrapper, options)
|
||||
return unless options[:remote_search]
|
||||
|
||||
expect(tomselect_wrapper).to have_css(".spinner")
|
||||
expect(tomselect_wrapper).not_to have_css(".spinner")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,18 +47,23 @@ create(:enterprise)
|
||||
uncheck 'to manage products'
|
||||
check 'to edit profile'
|
||||
check 'to add products to inventory'
|
||||
check 'to create linked variants'
|
||||
select2_select 'Two', from: 'enterprise_relationship_child_id'
|
||||
click_button 'Create'
|
||||
|
||||
# Wait for row to appear since have_relationship doesn't wait
|
||||
expect(page).to have_selector 'tr', count: 2
|
||||
# Permissions appear.. in a different order for some reason.
|
||||
expect_relationship_with_permissions e1, e2,
|
||||
['to add to order cycle',
|
||||
'to add products to inventory', 'to edit profile']
|
||||
'to create linked variants [BETA]',
|
||||
'to add products to inventory',
|
||||
'to edit profile']
|
||||
er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first
|
||||
expect(er).to be_present
|
||||
expect(er.permissions.map(&:name)).to match_array ['add_to_order_cycle', 'edit_profile',
|
||||
'create_variant_overrides']
|
||||
'create_variant_overrides',
|
||||
'create_linked_variants']
|
||||
end
|
||||
|
||||
it "attempting to create a relationship with invalid data" do
|
||||
|
||||
@@ -885,6 +885,47 @@ RSpec.describe '
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "removing enterprise managers" do
|
||||
let(:existing_user) { create(:user) }
|
||||
|
||||
before do
|
||||
distributor1.users << existing_user
|
||||
login_as logged_in_user
|
||||
visit edit_admin_enterprise_path(distributor1)
|
||||
scroll_to(:bottom)
|
||||
within ".side_menu" do
|
||||
find(:link, "Users").trigger("click")
|
||||
end
|
||||
end
|
||||
|
||||
context "as the enterprise owner" do
|
||||
let(:logged_in_user) { distributor1.owner }
|
||||
|
||||
it 'removes the manager as enterprise owner' do
|
||||
expect(page).to have_content existing_user.email
|
||||
|
||||
within "#manager-#{existing_user.id}" do
|
||||
accept_confirm do
|
||||
page.find("a.icon-trash").click
|
||||
end
|
||||
end
|
||||
|
||||
expect(page).not_to have_content existing_user.email
|
||||
end
|
||||
end
|
||||
|
||||
context "as the enterprise manager" do
|
||||
let(:logged_in_user) { existing_user }
|
||||
|
||||
it "is unable delete any other manager" do
|
||||
expect(page).to have_content existing_user.email
|
||||
within('.edit_enterprise') do
|
||||
expect(page).not_to have_selector('a.icon-trash')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "changing package" do
|
||||
|
||||
@@ -25,15 +25,17 @@ RSpec.describe "Admin -> Order -> Payments" do
|
||||
login_as distributor.owner
|
||||
end
|
||||
|
||||
it "allows to refund a Taler payment" do
|
||||
it "allows to void a Taler payment" do
|
||||
order_status = {
|
||||
order_status: "paid",
|
||||
contract_terms: {
|
||||
amount: "KUDOS:2",
|
||||
}
|
||||
}
|
||||
token_endpoint = "https://taler.example.com/private/token"
|
||||
order_endpoint = "https://taler.example.com/private/orders/taler-id-1"
|
||||
refund_endpoint = "https://taler.example.com/private/orders/taler-id-1/refund"
|
||||
stub_request(:post, token_endpoint).to_return(body: { token: "abc" }.to_json)
|
||||
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
|
||||
stub_request(:post, refund_endpoint).to_return(body: "{}")
|
||||
|
||||
@@ -49,4 +51,36 @@ RSpec.describe "Admin -> Order -> Payments" do
|
||||
expect(page).not_to have_link "Void"
|
||||
end
|
||||
end
|
||||
|
||||
it "allows to credit a Taler payment" do
|
||||
order_status = {
|
||||
order_status: "paid",
|
||||
contract_terms: {
|
||||
amount: "KUDOS:2",
|
||||
}
|
||||
}
|
||||
token_endpoint = "https://taler.example.com/private/token"
|
||||
order_endpoint = "https://taler.example.com/private/orders/taler-id-1"
|
||||
refund_endpoint = "https://taler.example.com/private/orders/taler-id-1/refund"
|
||||
stub_request(:post, token_endpoint).to_return(body: { token: "abc" }.to_json)
|
||||
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
|
||||
stub_request(:post, refund_endpoint).to_return(body: "{}")
|
||||
|
||||
visit spree.admin_order_payments_path(order.number)
|
||||
|
||||
within row_containing("Taler") do
|
||||
expect(page).to have_text "COMPLETED"
|
||||
expect(page).to have_link "Credit"
|
||||
|
||||
click_link class: "icon-credit"
|
||||
|
||||
expect(page).to have_text "COMPLETED"
|
||||
expect(page).not_to have_link "Credit"
|
||||
end
|
||||
|
||||
# Our payment system creates a new payment to show the credit.
|
||||
within row_containing("$-9.75") do
|
||||
expect(page).not_to have_link "Void"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
require "system_helper"
|
||||
|
||||
RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
RSpec.describe 'As an enterprise user, I can perform actions on the products screen' do
|
||||
include AdminHelper
|
||||
include WebHelper
|
||||
include AuthenticationHelper
|
||||
@@ -15,22 +15,6 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
login_as user
|
||||
end
|
||||
|
||||
let(:producer_search_selector) { 'input[placeholder="Select producer"]' }
|
||||
let(:categories_search_selector) { 'input[placeholder="Select category"]' }
|
||||
let(:tax_categories_search_selector) { 'input[placeholder="Search for tax categories"]' }
|
||||
|
||||
describe "with no products" do
|
||||
before { visit admin_products_url }
|
||||
it "can see the new product page" do
|
||||
expect(page).to have_content "Bulk Edit Products"
|
||||
expect(page).to have_text "No products found"
|
||||
# displays buttons to add products with the correct links
|
||||
expect(page).to have_link(class: "button", text: "New Product", href: "/admin/products/new")
|
||||
expect(page).to have_link(class: "button", text: "Import multiple products",
|
||||
href: admin_product_import_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "column selector" do
|
||||
let!(:product) { create(:simple_product) }
|
||||
|
||||
@@ -105,8 +89,6 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
end
|
||||
end
|
||||
|
||||
describe "columns"
|
||||
|
||||
describe "Changing producers, category and tax category" do
|
||||
let!(:variant_a1) {
|
||||
product_a.variants.first.tap{ |v|
|
||||
@@ -116,54 +98,7 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
}
|
||||
let!(:product_a) { create(:simple_product, name: "Apples", sku: "APL-00") }
|
||||
|
||||
context "when they are under 11" do
|
||||
before do
|
||||
create_list(:supplier_enterprise, 9, users: [user])
|
||||
create_list(:tax_category, 9)
|
||||
create_list(:taxon, 2)
|
||||
|
||||
visit admin_products_url
|
||||
end
|
||||
|
||||
it "should not display search input, change the producers, category and tax category" do
|
||||
producer_to_select = random_producer(variant_a1)
|
||||
category_to_select = random_category(variant_a1)
|
||||
tax_category_to_select = random_tax_category
|
||||
|
||||
within row_containing_name(variant_a1.display_name) do
|
||||
validate_tomselect_without_search!(
|
||||
page, "Producer",
|
||||
producer_search_selector
|
||||
)
|
||||
tomselect_select(producer_to_select, from: "Producer")
|
||||
end
|
||||
|
||||
within row_containing_name(variant_a1.display_name) do
|
||||
validate_tomselect_without_search!(
|
||||
page, "Category",
|
||||
categories_search_selector
|
||||
)
|
||||
tomselect_select(category_to_select, from: "Category")
|
||||
|
||||
validate_tomselect_without_search!(
|
||||
page, "Tax Category",
|
||||
tax_categories_search_selector
|
||||
)
|
||||
tomselect_select(tax_category_to_select, from: "Tax Category")
|
||||
end
|
||||
|
||||
click_button "Save changes"
|
||||
|
||||
expect(page).to have_content "Changes saved"
|
||||
|
||||
variant_a1.reload
|
||||
expect(variant_a1.supplier.name).to eq(producer_to_select)
|
||||
expect(variant_a1.primary_taxon.name).to eq(category_to_select)
|
||||
expect(variant_a1.tax_category.name).to eq(tax_category_to_select)
|
||||
end
|
||||
end
|
||||
|
||||
context "when they are over 11" do
|
||||
context "when there are products" do
|
||||
before do
|
||||
create_list(:supplier_enterprise, 11, users: [user])
|
||||
create_list(:tax_category, 11)
|
||||
@@ -181,9 +116,13 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
tax_category_to_select = random_tax_category
|
||||
|
||||
within row_containing_name(variant_a1.display_name) do
|
||||
tomselect_search_and_select(producer_to_select, from: "Producer")
|
||||
tomselect_search_and_select(category_to_select, from: "Category")
|
||||
tomselect_search_and_select(tax_category_to_select, from: "Tax Category")
|
||||
tomselect_search_and_select(producer_to_select, from: "Producer", remote_search: true)
|
||||
tomselect_search_and_select(category_to_select, from: "Category", remote_search: true)
|
||||
tomselect_search_and_select(
|
||||
tax_category_to_select,
|
||||
from: "Tax Category",
|
||||
remote_search: true
|
||||
)
|
||||
end
|
||||
|
||||
click_button "Save changes"
|
||||
@@ -260,24 +199,24 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
|
||||
describe "Cloning product" do
|
||||
it "shows the cloned product on page when clicked on the cloned option" do
|
||||
# TODO, variant supplier missing, needs to be copied from variant and not product
|
||||
within "table.products" do
|
||||
# Gather input values, because page.content doesn't include them.
|
||||
input_content = page.find_all('input[type=text]').map(&:value).join
|
||||
|
||||
# Products does not include the cloned product.
|
||||
expect(input_content).not_to match /COPY OF Apples/
|
||||
end
|
||||
|
||||
click_product_clone "Apples"
|
||||
|
||||
expect(page).to have_content "Successfully cloned the product"
|
||||
within "table.products" do
|
||||
# Gather input values, because page.content doesn't include them.
|
||||
input_content = page.find_all('input[type=text]').map(&:value).join
|
||||
# Product list includes the cloned product.
|
||||
expect(all_input_values).to match /COPY OF Apples/
|
||||
|
||||
# Products include the cloned product.
|
||||
expect(input_content).to match /COPY OF Apples/
|
||||
# And I can perform actions on the new product
|
||||
within row_containing_name "COPY OF Apples" do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
expect(page).to have_link "Edit"
|
||||
expect(page).to have_link "Clone"
|
||||
# expect(page).to have_link "Delete" # it's not a proper link :/
|
||||
|
||||
fill_in "Name", with: "My copy of Apples"
|
||||
end
|
||||
|
||||
click_button "Save changes"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -298,6 +237,98 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
end
|
||||
end
|
||||
|
||||
describe "Create linked variant" do
|
||||
let!(:variant) { create(:variant, display_name: "My box", supplier: producer) }
|
||||
let!(:linked_variant) {
|
||||
variant.create_linked_variant(user).tap{ |v| v.update! display_name: "My linked variant" }
|
||||
}
|
||||
let!(:other_producer) { create(:supplier_enterprise) }
|
||||
let!(:other_variant) {
|
||||
create(:variant, display_name: "My friends box", supplier: other_producer)
|
||||
}
|
||||
let!(:enterprise_relationship) {
|
||||
# Other producer grants me access to manage their variant
|
||||
create(:enterprise_relationship, parent: other_producer, child: producer,
|
||||
permissions_list: [:manage_products])
|
||||
}
|
||||
|
||||
context "with create_linked_variants permission for my, and other's variants" do
|
||||
it "creates a linked variant" do
|
||||
create(:enterprise_relationship, parent: producer, child: producer,
|
||||
permissions_list: [:create_linked_variants])
|
||||
enterprise_relationship.permissions.create! name: :create_linked_variants
|
||||
|
||||
visit admin_products_url
|
||||
|
||||
# Check my own variant
|
||||
within row_containing_name("My box") do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
|
||||
expect(page).to have_link "Create linked variant"
|
||||
end
|
||||
close_action_menu
|
||||
|
||||
# Check my own linked variant
|
||||
within row_containing_name("My linked variant") do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
|
||||
expect(page).not_to have_link "Create linked variant"
|
||||
end
|
||||
close_action_menu
|
||||
|
||||
# Create linked variant sourced from my friend
|
||||
within row_containing_name("My friends box") do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
|
||||
click_link "Create linked variant"
|
||||
end
|
||||
|
||||
expect(page).to have_content "Successfully created linked variant"
|
||||
|
||||
within "table.products" do
|
||||
# There are now two copies
|
||||
expect(all_input_values).to match /My friends box.*My friends box/
|
||||
# One of them is designated as a linked variant
|
||||
expect(page).to have_content "🔗"
|
||||
|
||||
last_box = page.all(row_containing_name("My friends box")).last
|
||||
# Close action menu (shouldn't need this, it should close itself)
|
||||
last_box.click
|
||||
|
||||
# And I can perform actions on the new product
|
||||
within last_box do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
expect(page).to have_link "Edit"
|
||||
# expect(page).to have_link "Clone" # tofix: menu is partially obscured
|
||||
# expect(page).to have_link "Delete" # it's not a proper link
|
||||
|
||||
fill_in "Name", with: "My copy of Apples"
|
||||
end
|
||||
click_button "Save changes"
|
||||
|
||||
# initially obscured by the previous message, then disappears before capybara sees it.
|
||||
# expect(page).to have_content "Changes saved"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without create_linked_variants permission" do
|
||||
it "does not show the option in the menu" do
|
||||
visit admin_products_url
|
||||
|
||||
within row_containing_name("My box") do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
expect(page).not_to have_link "Create linked variant"
|
||||
end
|
||||
|
||||
within row_containing_name("My friends box") do
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
expect(page).not_to have_link "Create linked variant"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "delete" do
|
||||
let!(:product_a) { create(:simple_product, name: "Apples", sku: "APL-00") }
|
||||
let(:delete_option_selector) { "a[data-controller='modal-link'].delete" }
|
||||
@@ -527,90 +558,6 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise manager" do
|
||||
let(:supplier_managed1) { create(:supplier_enterprise, name: 'Supplier Managed 1') }
|
||||
let(:supplier_managed2) { create(:supplier_enterprise, name: 'Supplier Managed 2') }
|
||||
let(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Supplier Unmanaged') }
|
||||
let(:supplier_permitted) { create(:supplier_enterprise, name: 'Supplier Permitted') }
|
||||
let(:distributor_managed) { create(:distributor_enterprise, name: 'Distributor Managed') }
|
||||
let(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Distributor Unmanaged') }
|
||||
let!(:product_supplied) { create(:product, supplier_id: supplier_managed1.id, price: 10.0) }
|
||||
let!(:product_not_supplied) { create(:product, supplier_id: supplier_unmanaged.id) }
|
||||
let!(:product_supplied_permitted) {
|
||||
create(:product, name: 'Product Permitted', supplier_id: supplier_permitted.id, price: 10.0)
|
||||
}
|
||||
let(:product_supplied_inactive) {
|
||||
create(:product, supplier_id: supplier_managed1.id, price: 10.0)
|
||||
}
|
||||
|
||||
let!(:supplier_permitted_relationship) do
|
||||
create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed1,
|
||||
permissions_list: [:manage_products])
|
||||
end
|
||||
|
||||
before do
|
||||
enterprise_user = create(:user)
|
||||
enterprise_user.enterprise_roles.build(enterprise: supplier_managed1).save
|
||||
enterprise_user.enterprise_roles.build(enterprise: supplier_managed2).save
|
||||
enterprise_user.enterprise_roles.build(enterprise: distributor_managed).save
|
||||
|
||||
login_as enterprise_user
|
||||
end
|
||||
|
||||
it "shows only products that I supply" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
# displays permitted product list only
|
||||
expect(page).to have_selector row_containing_name(product_supplied.name)
|
||||
expect(page).to have_selector row_containing_name(product_supplied_permitted.name)
|
||||
expect(page).not_to have_selector row_containing_name(product_not_supplied.name)
|
||||
end
|
||||
|
||||
it "shows only suppliers that I manage or have permission to" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
within row_containing_placeholder(product_supplied.name) do
|
||||
expect(page).to have_select(
|
||||
'_products_0_variants_attributes_0_supplier_id',
|
||||
options: [
|
||||
'Select producer',
|
||||
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
|
||||
], selected: supplier_managed1.name
|
||||
)
|
||||
end
|
||||
|
||||
within row_containing_placeholder(product_supplied_permitted.name) do
|
||||
expect(page).to have_select(
|
||||
'_products_1_variants_attributes_0_supplier_id',
|
||||
options: [
|
||||
'Select producer',
|
||||
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
|
||||
], selected: supplier_permitted.name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "shows inactive products that I supply" do
|
||||
product_supplied_inactive
|
||||
|
||||
visit spree.admin_products_path
|
||||
|
||||
expect(page).to have_selector row_containing_name(product_supplied_inactive.name)
|
||||
end
|
||||
|
||||
it "allows me to update a product" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
within row_containing_name(product_supplied.name) do
|
||||
fill_in "Name", with: "Pommes"
|
||||
end
|
||||
click_button "Save changes"
|
||||
|
||||
expect(page).to have_content "Changes saved"
|
||||
expect(page).to have_selector row_containing_name("Pommes")
|
||||
end
|
||||
end
|
||||
|
||||
def open_action_menu
|
||||
page.find(".vertical-ellipsis-menu").click
|
||||
end
|
||||
|
||||
@@ -86,14 +86,14 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
find('button[aria-label="On Hand"]').click
|
||||
find('input[id$="_price"]').fill_in with: "11.1"
|
||||
|
||||
select supplier.name, from: 'Producer'
|
||||
select taxon.name, from: 'Category'
|
||||
|
||||
if stock == "on_hand"
|
||||
find('input[id$="_on_hand_desired"]').fill_in with: "66"
|
||||
elsif stock == "on_demand"
|
||||
find('input[id$="_on_demand_desired"]').check
|
||||
end
|
||||
|
||||
tomselect_select supplier.name, from: 'Producer'
|
||||
tomselect_select taxon.name, from: 'Category'
|
||||
end
|
||||
|
||||
expect(page).to have_content "1 product modified."
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
require "system_helper"
|
||||
|
||||
RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
RSpec.describe 'As an enterprise user, I can browse my products' do
|
||||
include AdminHelper
|
||||
include WebHelper
|
||||
include AuthenticationHelper
|
||||
include FileHelper
|
||||
|
||||
let(:producer) { create(:supplier_enterprise) }
|
||||
let(:producer) { create(:supplier_enterprise, name: "My Enterprise") }
|
||||
let(:user) { create(:user, enterprises: [producer]) }
|
||||
|
||||
before do
|
||||
@@ -19,15 +19,25 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
let(:categories_search_selector) { 'input[placeholder="Search for categories"]' }
|
||||
let(:tax_categories_search_selector) { 'input[placeholder="Search for tax categories"]' }
|
||||
|
||||
describe "with no products" do
|
||||
before { visit admin_products_url }
|
||||
it "can see the new product page" do
|
||||
expect(page).to have_content "Bulk Edit Products"
|
||||
expect(page).to have_text "No products found"
|
||||
# displays buttons to add products with the correct links
|
||||
expect(page).to have_link(class: "button", text: "New Product", href: "/admin/products/new")
|
||||
expect(page).to have_link(class: "button", text: "Import multiple products",
|
||||
href: admin_product_import_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe "listing" do
|
||||
let!(:p1) { create(:product, name: "Product1") }
|
||||
let!(:p2) { create(:product, name: "Product2") }
|
||||
|
||||
before do
|
||||
visit admin_products_url
|
||||
end
|
||||
|
||||
it "displays a list of products" do
|
||||
visit admin_products_path
|
||||
|
||||
within ".products" do
|
||||
# displays table header
|
||||
expect(page).to have_selector "th", text: "Name"
|
||||
@@ -96,13 +106,19 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
visit spree.admin_products_path
|
||||
|
||||
within row_containing_name "Variant1" do
|
||||
expect(page).to have_select "Producer", with_options: ["Producer A", "Producer B"],
|
||||
selected: "Producer A"
|
||||
expect_tomselect_existing_with_selected_options(
|
||||
from: 'Producer',
|
||||
existing_options: ["Producer A", "Producer B"],
|
||||
selected_options: ["Producer A"]
|
||||
)
|
||||
end
|
||||
|
||||
within row_containing_name "Variant2a" do
|
||||
expect(page).to have_select "Producer", with_options: ["Producer A", "Producer B"],
|
||||
selected: "Producer B"
|
||||
expect_tomselect_existing_with_selected_options(
|
||||
from: 'Producer',
|
||||
existing_options: ["Producer A", "Producer B"],
|
||||
selected_options: ["Producer B"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -129,6 +145,34 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
expect(page).to have_select "variant_unit_with_scale", selected: "Items"
|
||||
expect(page).to have_field "variant_unit_name", with: "packet"
|
||||
end
|
||||
|
||||
context "with sourced variant" do
|
||||
let(:source_producer) { create(:supplier_enterprise) }
|
||||
let(:p3) { create(:product, name: "Product3", supplier_id: source_producer.id) }
|
||||
|
||||
let!(:v3_source) { p3.variants.first }
|
||||
let!(:v3_sourced) {
|
||||
create(:variant, display_name: "Variant3-sourced", product: p3, supplier: source_producer,
|
||||
hub: producer)
|
||||
}
|
||||
let!(:enterprise_relationship) {
|
||||
# Other producer grants me access to manage their variant
|
||||
create(:enterprise_relationship, parent: source_producer, child: producer,
|
||||
permissions_list: [:manage_products])
|
||||
}
|
||||
|
||||
before do
|
||||
v3_sourced.source_variants << v3_source
|
||||
visit admin_products_url
|
||||
end
|
||||
|
||||
it "shows sourced variant with indicator" do
|
||||
within row_containing_name("Variant3-sourced") do
|
||||
expect(page).to have_selector 'span[title*="Sourced from: "]'
|
||||
expect(page).to have_selector 'span[title*="Hub: My Enterprise"]'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "sorting" do
|
||||
@@ -463,4 +507,85 @@ RSpec.describe 'As an enterprise user, I can manage my products' do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "as an enterprise manager" do
|
||||
let(:supplier_managed1) { create(:supplier_enterprise, name: 'Supplier Managed 1') }
|
||||
let(:supplier_managed2) { create(:supplier_enterprise, name: 'Supplier Managed 2') }
|
||||
let(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Supplier Unmanaged') }
|
||||
let(:supplier_permitted) { create(:supplier_enterprise, name: 'Supplier Permitted') }
|
||||
let(:distributor_managed) { create(:distributor_enterprise, name: 'Distributor Managed') }
|
||||
let(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Distributor Unmanaged') }
|
||||
let!(:product_supplied) { create(:product, supplier_id: supplier_managed1.id, price: 10.0) }
|
||||
let!(:product_not_supplied) { create(:product, supplier_id: supplier_unmanaged.id) }
|
||||
let!(:product_supplied_permitted) {
|
||||
create(:product, name: 'Product Permitted', supplier_id: supplier_permitted.id, price: 10.0)
|
||||
}
|
||||
let(:product_supplied_inactive) {
|
||||
create(:product, supplier_id: supplier_managed1.id, price: 10.0)
|
||||
}
|
||||
|
||||
let!(:supplier_permitted_relationship) do
|
||||
create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed1,
|
||||
permissions_list: [:manage_products])
|
||||
end
|
||||
|
||||
before do
|
||||
enterprise_user = create(:user)
|
||||
enterprise_user.enterprise_roles.build(enterprise: supplier_managed1).save
|
||||
enterprise_user.enterprise_roles.build(enterprise: supplier_managed2).save
|
||||
enterprise_user.enterprise_roles.build(enterprise: distributor_managed).save
|
||||
|
||||
login_as enterprise_user
|
||||
end
|
||||
|
||||
it "shows only products that I supply" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
# displays permitted product list only
|
||||
expect(page).to have_selector row_containing_name(product_supplied.name)
|
||||
expect(page).to have_selector row_containing_name(product_supplied_permitted.name)
|
||||
expect(page).not_to have_selector row_containing_name(product_not_supplied.name)
|
||||
end
|
||||
|
||||
it "shows only suppliers that I manage or have permission to" do
|
||||
visit spree.admin_products_path
|
||||
existing_options = [supplier_managed1.name, supplier_managed2.name, supplier_permitted.name]
|
||||
|
||||
within row_containing_placeholder(product_supplied.name) do
|
||||
expect_tomselect_existing_with_selected_options(
|
||||
existing_options:,
|
||||
from: '_products_0_variants_attributes_0_supplier_id',
|
||||
selected_options: [supplier_managed1.name]
|
||||
)
|
||||
end
|
||||
|
||||
within row_containing_placeholder(product_supplied_permitted.name) do
|
||||
expect_tomselect_existing_with_selected_options(
|
||||
existing_options:,
|
||||
from: '_products_1_variants_attributes_0_supplier_id',
|
||||
selected_options: [supplier_permitted.name]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it "shows inactive products that I supply" do
|
||||
product_supplied_inactive
|
||||
|
||||
visit spree.admin_products_path
|
||||
|
||||
expect(page).to have_selector row_containing_name(product_supplied_inactive.name)
|
||||
end
|
||||
|
||||
it "allows me to update a product" do
|
||||
visit spree.admin_products_path
|
||||
|
||||
within row_containing_name(product_supplied.name) do
|
||||
fill_in "Name", with: "Pommes"
|
||||
end
|
||||
click_button "Save changes"
|
||||
|
||||
expect(page).to have_content "Changes saved"
|
||||
expect(page).to have_selector row_containing_name("Pommes")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -350,8 +350,8 @@ RSpec.describe 'As an enterprise user, I can update my products' do
|
||||
click_on "On Hand" # activate popout
|
||||
fill_in "On Hand", with: "3"
|
||||
|
||||
select producer.name, from: 'Producer'
|
||||
select taxon.name, from: 'Category'
|
||||
tomselect_select producer.name, from: 'Producer'
|
||||
tomselect_select taxon.name, from: 'Category'
|
||||
end
|
||||
|
||||
expect {
|
||||
@@ -586,8 +586,8 @@ RSpec.describe 'As an enterprise user, I can update my products' do
|
||||
fill_in "Name", with: "Nice box"
|
||||
fill_in "SKU", with: "APL-02"
|
||||
|
||||
select producer.name, from: 'Producer'
|
||||
select taxon.name, from: 'Category'
|
||||
tomselect_select producer.name, from: 'Producer'
|
||||
tomselect_select taxon.name, from: 'Category'
|
||||
end
|
||||
|
||||
expect {
|
||||
|
||||
@@ -370,6 +370,7 @@ RSpec.describe "As a consumer, I want to checkout my order" do
|
||||
Spree::PaymentMethod::Taler.create!(
|
||||
name: "Taler",
|
||||
environment: "test",
|
||||
preferred_backend_url: "https://taler.example.com/",
|
||||
distributors: [distributor]
|
||||
)
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ RSpec.describe "admin/products_v3/_filters.html.haml" do
|
||||
end
|
||||
let(:spree_current_user) { build(:enterprise_user) }
|
||||
|
||||
it "shows the producer filter when there are options" do
|
||||
it "shows the producer filter with the default option initially" do
|
||||
allow(view).to receive_messages locals.merge(
|
||||
producer_options: [
|
||||
["Ada's Apples", 1],
|
||||
@@ -27,9 +27,7 @@ RSpec.describe "admin/products_v3/_filters.html.haml" do
|
||||
|
||||
is_expected.to have_content "Producers"
|
||||
is_expected.to have_select "producer_id", options: [
|
||||
"All producers",
|
||||
"Ada's Apples",
|
||||
"Ben's Bananas",
|
||||
"All producers"
|
||||
], selected: nil
|
||||
end
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ RSpec.describe "registration/steps/_details.html.haml" do
|
||||
it "uses Google Maps when it is enabled" do
|
||||
allow(view).to receive_messages(using_google_maps?: true)
|
||||
|
||||
is_expected.to match /<ui-gmap-google-map center='map.center' zoom='map.zoom'>/
|
||||
is_expected.to match /<ui-gmap-google-map center="map.center" zoom="map.zoom">/
|
||||
end
|
||||
|
||||
it "uses OpenStreetMap when it is enabled" do
|
||||
ContentConfig.open_street_map_enabled = true
|
||||
allow(view).to receive_messages(using_google_maps?: false)
|
||||
|
||||
is_expected.to match /<div class='map-container--registration' id='open-street-map'>/
|
||||
is_expected.to match /<div class="map-container--registration" id="open-street-map">/
|
||||
end
|
||||
end
|
||||
|
||||
49
yarn.lock
49
yarn.lock
@@ -816,9 +816,9 @@
|
||||
"@babel/helper-plugin-utils" "^7.28.6"
|
||||
|
||||
"@babel/preset-env@^7.28.5":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.0.tgz#c55db400c515a303662faaefd2d87e796efa08d0"
|
||||
integrity sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==
|
||||
version "7.29.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.2.tgz#5a173f22c7d8df362af1c9fe31facd320de4a86c"
|
||||
integrity sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.29.0"
|
||||
"@babel/helper-compilation-targets" "^7.28.6"
|
||||
@@ -900,15 +900,10 @@
|
||||
"@babel/types" "^7.4.4"
|
||||
esutils "^2.0.2"
|
||||
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4":
|
||||
version "7.28.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
|
||||
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
|
||||
|
||||
"@babel/runtime@^7.28.4":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b"
|
||||
integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.28.4", "@babel/runtime@^7.8.4":
|
||||
version "7.29.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e"
|
||||
integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==
|
||||
|
||||
"@babel/template@^7.28.6":
|
||||
version "7.28.6"
|
||||
@@ -5156,9 +5151,9 @@ mimic-fn@^2.1.0:
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
mini-css-extract-plugin@^2.9.4:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.1.tgz#a7f0bb890f4e1ce6dfc124bd1e6d6fcd3b359844"
|
||||
integrity sha512-k7G3Y5QOegl380tXmZ68foBRRjE9Ljavx835ObdvmZjQ639izvZD8CS7BkWw1qKPPzHsGL/JDhl0uyU1zc2rJw==
|
||||
version "2.10.2"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz#5c85ec9450c05d26e32531b465a15a08c3a57253"
|
||||
integrity sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==
|
||||
dependencies:
|
||||
schema-utils "^4.0.0"
|
||||
tapable "^2.2.1"
|
||||
@@ -5275,9 +5270,9 @@ node-addon-api@^7.0.0:
|
||||
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
|
||||
|
||||
node-forge@^1:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
|
||||
integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2"
|
||||
integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==
|
||||
|
||||
node-int64@^0.4.0:
|
||||
version "0.4.0"
|
||||
@@ -5482,14 +5477,14 @@ picocolors@1.1.1, picocolors@^1.1.1:
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
|
||||
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
|
||||
|
||||
picomatch@^4.0.2, picomatch@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
|
||||
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||
|
||||
pify@^2.3.0:
|
||||
version "2.3.0"
|
||||
@@ -7116,9 +7111,9 @@ tr46@^5.1.0:
|
||||
punycode "^2.3.1"
|
||||
|
||||
trix@*:
|
||||
version "2.1.17"
|
||||
resolved "https://registry.yarnpkg.com/trix/-/trix-2.1.17.tgz#a47c4ee1925a7abb26aee5c094ec7ef9fe49575a"
|
||||
integrity sha512-nkHg7VgIItGVx1CFA645dDlAoCgah+9gv80Yc+97aS8jkZmO5K4MxkSqmU9t/C8upqZB8uhfJs90epoDfYz/6Q==
|
||||
version "2.1.18"
|
||||
resolved "https://registry.yarnpkg.com/trix/-/trix-2.1.18.tgz#d87a5be64c10ecdab64f5af47f941b5318c08225"
|
||||
integrity sha512-DWOdTsz3n9PO3YBc1R6pGh9MG1cXys/2+rouc/qsISncjc2MBew2UOW8nXh3NjUOjobKsXCIPR6LB02abg2EYg==
|
||||
dependencies:
|
||||
dompurify "^3.2.5"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user