Resolve conflicts

This commit is contained in:
Konrad
2026-03-02 20:58:25 +01:00
908 changed files with 14762 additions and 17100 deletions

View File

@@ -1,2 +0,0 @@
defaults
IE 11

1
.gitattributes vendored
View File

@@ -8,4 +8,3 @@
# Same thing for following files, but they don't have an sh extension
pre-commit eol=lf
webpack-dev-server eol=lf
install-bundler eol=lf

View File

@@ -1,4 +1,4 @@
#### What? Why?
## What? Why?
- Closes # <!-- Insert issue number here. -->
@@ -7,7 +7,7 @@
#### What should we test?
## What should we test?
<!-- List which features should be tested and how.
This can be similar to the Steps to Reproduce in the issue.
Also think of other parts of the app which could be affected
@@ -16,7 +16,7 @@
- Visit ... page.
-
#### Release notes
## Release notes
<!-- Please select one for your PR and delete the other. -->
@@ -33,12 +33,12 @@ Changelog Category (reviewers may add a label for the release notes):
The title of the pull request will be included in the release notes.
#### Dependencies
## Dependencies
<!-- Does this PR depend on another one?
Add the link or remove this section. -->
#### Documentation updates
## Documentation updates
<!-- Are there any wiki pages that need updating after merging this PR?
List them here or remove this section. -->

View File

@@ -31,6 +31,8 @@ updates:
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 7
# Only specific requirements are specified in Gemfile, so don't touch it.
versioning-strategy: lockfile-only
@@ -39,6 +41,8 @@ updates:
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 7
# Only specific requirements are specified in package.json, so don't touch it.
versioning-strategy: lockfile-only

View File

@@ -46,7 +46,7 @@ jobs:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
uses: supercharge/redis-github-action@1.8.1
with:
redis-version: 6
@@ -124,7 +124,7 @@ jobs:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
uses: supercharge/redis-github-action@1.8.1
with:
redis-version: 6
@@ -211,7 +211,7 @@ jobs:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
uses: supercharge/redis-github-action@1.8.1
with:
redis-version: 6
@@ -290,7 +290,7 @@ jobs:
- uses: actions/checkout@v3
- name: Setup redis
uses: supercharge/redis-github-action@1.4.0
uses: supercharge/redis-github-action@1.8.1
with:
redis-version: 6

2
.rspec
View File

@@ -1 +1 @@
--require base_spec_helper
--require spec_helper

View File

@@ -1 +1 @@
3.2.9
3.4.8

View File

@@ -1,4 +1,4 @@
FROM ruby:3.2.9-alpine3.19 AS base
FROM ruby:3.4.8-alpine3.19 AS base
ENV LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
TZ=Europe/London \
@@ -31,4 +31,4 @@ FROM development-base
COPY . $RAILS_ROOT
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs "$(nproc)"
COPY --from=yarn-dependencies $RAILS_ROOT/node_modules ./node_modules
COPY --from=yarn-dependencies $RAILS_ROOT/node_modules ./node_modules

View File

@@ -83,11 +83,8 @@ RUN wget https://chromedriver.storage.googleapis.com/2.41/chromedriver_linux64.z
# Copy code and install app dependencies
COPY . /usr/src/app/
# Install Bundler
RUN ./script/install-bundler
# Install front-end dependencies
RUN yarn install
# Run bundler install in parallel with the amount of available CPUs
RUN bundle install --jobs="$(nproc)"
RUN bundle install --jobs="$(nproc)"

12
Gemfile
View File

@@ -18,7 +18,7 @@ gem 'activemerchant'
gem 'angular-rails-templates'
gem 'ransack', '~> 4.1.0'
gem 'responders'
gem 'webpacker', '~> 5'
gem 'shakapacker', '8.4.0'
# Indirect dependency but we access it directly in JS specs.
# It turns out to be hard to upgrade but please do if you can.
@@ -49,7 +49,7 @@ gem 'acts_as_list', '1.0.4'
gem 'cancancan', '~> 1.15.0'
gem 'digest'
gem 'ffaker'
gem 'highline', '2.0.3' # Necessary for the install generator
gem 'highline'
gem 'json'
gem 'monetize', '~> 1.11'
gem 'paranoia', '~> 2.4'
@@ -57,7 +57,8 @@ gem 'state_machines-activerecord'
gem 'stringex', '~> 2.8.5', require: false
gem 'paypal-sdk-merchant', '1.117.2'
gem 'stripe', '~> 13'
gem 'stripe', '~> 15'
gem "taler"
gem 'devise'
gem 'devise-encryptable'
@@ -111,7 +112,7 @@ gem "turbo_power"
gem "turbo-rails"
gem 'combine_pdf'
gem 'wicked_pdf'
gem 'wicked_pdf', github: "openfoodfoundation/wicked_pdf", branch: "master"
gem 'wkhtmltopdf-binary'
gem 'immigrant'
@@ -126,7 +127,8 @@ gem 'angular_rails_csrf'
gem 'jquery-rails', '4.4.0'
gem 'jquery-ui-rails', '~> 4.2'
gem "select2-rails", github: "openfoodfoundation/select2-rails", branch: "v349_with_thor_v1"
gem "select2-rails", github: "openfoodfoundation/select2-rails",
branch: "v349_with-relaxed-dependencies"
gem 'good_migrations'

View File

@@ -10,13 +10,21 @@ GIT
GIT
remote: https://github.com/openfoodfoundation/select2-rails.git
revision: fc240e85fbdf1878ff3c39d972c0cd9a312f5ed4
branch: v349_with_thor_v1
revision: 9693e0cc5b04938da46692d3fa83aa8934791981
branch: v349_with-relaxed-dependencies
specs:
select2-rails (3.4.9)
sass-rails
thor (>= 0.14)
GIT
remote: https://github.com/openfoodfoundation/wicked_pdf.git
revision: bce498de547cdf00d037fdbec29fae844d69ee8e
branch: master
specs:
wicked_pdf (2.8.1)
activesupport
ostruct
GIT
remote: https://github.com/podia/stimulus_reflex_testing.git
revision: abac2ee34de347c589795b4d1a8e83e0baafb201
@@ -104,11 +112,12 @@ GEM
rails-html-sanitizer (~> 1.6)
active_model_serializers (0.8.4)
activemodel (>= 3.0)
active_storage_validations (1.1.4)
activejob (>= 5.2.0)
activemodel (>= 5.2.0)
activestorage (>= 5.2.0)
activesupport (>= 5.2.0)
active_storage_validations (3.0.3)
activejob (>= 6.1.4)
activemodel (>= 6.1.4)
activestorage (>= 6.1.4)
activesupport (>= 6.1.4)
marcel (>= 1.0.3)
activejob (7.1.6)
activesupport (= 7.1.6)
globalid (>= 0.3.6)
@@ -201,12 +210,12 @@ GEM
bigdecimal (3.3.1)
bindata (2.5.1)
bindex (0.8.1)
bootsnap (1.19.0)
bootsnap (1.22.0)
msgpack (~> 1.2)
bugsnag (6.28.0)
concurrent-ruby (~> 1.0)
builder (3.3.0)
bullet (8.0.8)
bullet (8.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
cable_ready (5.0.6)
@@ -233,7 +242,7 @@ GEM
marcel (~> 1.0)
nokogiri (~> 1.10, >= 1.10.4)
rubyzip (>= 1.3.0, < 3)
cgi (0.5.0)
cgi (0.5.1)
childprocess (5.0.0)
choice (0.2.0)
chronic (0.10.2)
@@ -248,7 +257,7 @@ GEM
combine_pdf (1.0.31)
matrix
ruby-rc4 (>= 0.1.5)
concurrent-ruby (1.3.5)
concurrent-ruby (1.3.6)
connection_pool (2.5.5)
cookiejar (0.3.4)
crack (1.0.1)
@@ -258,9 +267,9 @@ GEM
css_parser (1.21.1)
addressable
csv (3.3.5)
cuprite (0.15)
cuprite (0.17)
capybara (~> 3.0)
ferrum (~> 0.14.0)
ferrum (~> 0.17.0)
database_cleaner (2.1.0)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.2.2)
@@ -269,7 +278,7 @@ GEM
database_cleaner-core (2.0.1)
datafoodconsortium-connector (1.2.0)
virtual_assembly-semantizer (~> 1.0, >= 1.0.5)
date (3.5.0)
date (3.5.1)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
@@ -304,9 +313,9 @@ GEM
eventmachine (>= 1.0.0.beta.1)
email_validator (2.2.4)
activemodel
erb (6.0.0)
erb (6.0.1)
erubi (1.13.1)
et-orbi (1.3.0)
et-orbi (1.4.0)
tzinfo
eventmachine (1.2.7)
eventmachine_httpserver (0.2.1)
@@ -317,19 +326,22 @@ GEM
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.2)
faraday (2.14.1)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-follow_redirects (0.4.0)
faraday (>= 1, < 3)
faraday-net_http (3.1.1)
net-http
ferrum (0.14)
faraday-net_http (3.4.2)
net-http (~> 0.5)
ferrum (0.17.1)
addressable (~> 2.5)
base64 (~> 0.2)
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (>= 0.6, < 0.8)
websocket-driver (~> 0.7)
ffaker (2.25.0)
ffi (1.17.2)
ffi (1.17.3)
flipper (1.3.6)
concurrent-ruby (< 2)
flipper-active_record (1.3.6)
@@ -360,8 +372,8 @@ GEM
foreman (0.90.0)
thor (~> 1.4)
formatador (0.2.5)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
fugit (1.12.1)
et-orbi (~> 1.4)
raabro (~> 1.4)
fuubar (2.5.1)
rspec-core (~> 3.0)
@@ -379,7 +391,7 @@ GEM
temple (>= 0.8.2)
thor
tilt
haml_lint (0.67.0)
haml_lint (0.68.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@@ -387,8 +399,10 @@ GEM
sysexits (~> 1.1)
hashdiff (1.2.1)
hashery (2.1.2)
hashie (5.0.0)
highline (2.0.3)
hashie (5.1.0)
logger
highline (3.1.2)
reline
htmlentities (4.4.2)
http_parser.rb (0.8.0)
i18n (1.14.7)
@@ -415,10 +429,11 @@ GEM
activerecord (>= 3.0)
invisible_captcha (2.3.0)
rails (>= 5.2)
io-console (0.8.1)
io-console (0.8.2)
ipaddress (0.8.3)
irb (1.15.3)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
@@ -428,7 +443,7 @@ GEM
thor (>= 0.14, < 2.0)
jquery-ui-rails (4.2.1)
railties (>= 3.2.16)
json (2.15.2)
json (2.18.1)
json-canonicalization (1.0.0)
json-jwt (1.17.0)
activesupport (>= 4.2)
@@ -454,7 +469,7 @@ GEM
activesupport (>= 4.2)
jwt (2.10.2)
base64
knapsack_pro (8.4.0)
knapsack_pro (9.2.2)
rake
language_server-protocol (3.17.0.5)
launchy (3.0.0)
@@ -464,11 +479,12 @@ GEM
launchy (>= 2.2, < 4)
link_header (0.0.8)
lint_roller (1.1.0)
listen (3.9.0)
listen (3.10.0)
logger
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0)
loofah (2.24.1)
loofah (2.25.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.9.0)
@@ -491,7 +507,8 @@ GEM
logger
mini_mime (1.1.5)
mini_portile2 (2.8.6)
minitest (5.26.2)
minitest (6.0.1)
prism (~> 1.5)
monetize (1.13.0)
money (~> 6.12)
money (6.16.0)
@@ -500,8 +517,8 @@ GEM
multi_json (1.17.0)
multi_xml (0.6.0)
mutex_m (0.3.0)
net-http (0.7.0)
uri
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.5.12)
date
net-protocol
@@ -513,7 +530,7 @@ GEM
net-protocol
newrelic_rpm (9.24.0)
nio4r (2.7.5)
nokogiri (1.18.10)
nokogiri (1.19.1)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri-html5-inference (0.3.0)
@@ -530,7 +547,7 @@ GEM
logger
rack (>= 2.2.3)
rack-protection
omniauth-rails_csrf_protection (1.0.2)
omniauth-rails_csrf_protection (2.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth_openid_connect (0.8.0)
@@ -551,6 +568,7 @@ GEM
webfinger (~> 2.0)
orm_adapter (0.5.0)
ostruct (0.6.1)
package_json (0.2.0)
pagy (9.4.0)
paper_trail (17.0.0)
activerecord (>= 7.1)
@@ -558,7 +576,7 @@ GEM
parallel (1.27.0)
paranoia (2.6.4)
activerecord (>= 5.1, < 7.2)
parser (3.3.10.0)
parser (3.3.10.2)
ast (~> 2.4.1)
racc
paypal-sdk-core (0.3.4)
@@ -576,15 +594,16 @@ GEM
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.6.0)
prism (1.9.0)
private_address_check (0.5.0)
pry (0.15.2)
pry (0.16.0)
coderay (~> 1.1)
method_source (~> 1.0)
psych (5.2.6)
reline (>= 0.6.0)
psych (5.3.1)
date
stringio
public_suffix (7.0.0)
public_suffix (7.0.2)
puffing-billy (4.0.2)
addressable (~> 2.5)
em-http-request (~> 1.1, >= 1.1.0)
@@ -600,7 +619,7 @@ GEM
railties (>= 4.2)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.21)
rack (2.2.22)
rack-mini-profiler (2.3.4)
rack (>= 1.2.0)
rack-oauth2 (2.3.0)
@@ -613,7 +632,7 @@ GEM
rack-protection (3.2.0)
base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6)
rack-proxy (0.7.7)
rack
rack-rewrite (1.5.1)
rack-session (1.0.2)
@@ -674,7 +693,7 @@ GEM
activesupport (>= 6.1.5)
i18n
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
rb-inotify (0.11.1)
ffi (~> 1.0)
rdf (3.3.4)
bcp47_spec (~> 0.2)
@@ -683,7 +702,7 @@ GEM
logger (~> 1.5)
ostruct (~> 0.6)
readline (~> 0.0)
rdoc (6.16.0)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
tsort
@@ -692,16 +711,16 @@ GEM
redcarpet (3.6.1)
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.26.1)
redis-client (0.26.4)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
rexml (3.4.4)
roadie (5.2.1)
css_parser (~> 1.4)
@@ -756,7 +775,7 @@ GEM
rswag-ui (2.17.0)
actionpack (>= 5.2, < 8.2)
railties (>= 5.2, < 8.2)
rubocop (1.81.7)
rubocop (1.84.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -764,25 +783,25 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.48.0)
rubocop-ast (1.49.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
prism (~> 1.7)
rubocop-capybara (2.22.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-factory_bot (2.28.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.34.2)
rubocop-rails (2.34.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.8.0)
rubocop-rspec (3.9.0)
lint_roller (~> 1.1)
rubocop (~> 1.81)
rubocop-rspec_rails (2.32.0)
@@ -797,33 +816,32 @@ GEM
ffi (~> 1.12)
logger
rubyzip (2.4.1)
rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
rugged (1.9.0)
sanitize (7.0.0)
crass (~> 1.0.2)
nokogiri (>= 1.16.8)
sass (3.4.25)
sass-rails (5.0.8)
railties (>= 5.2.0)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
sd_notify (0.1.1)
securerandom (0.4.1)
semantic_range (3.0.0)
semantic_range (3.1.0)
shakapacker (8.4.0)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
shoulda-matchers (7.0.1)
activesupport (>= 7.1)
sidekiq (7.2.4)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.19.0)
sidekiq-scheduler (5.0.3)
sidekiq (7.3.10)
base64
connection_pool (>= 2.3.0, < 3)
logger
rack (>= 2.2.4, < 3.3)
redis-client (>= 0.23.0, < 1)
sidekiq-scheduler (6.0.1)
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
sidekiq (>= 7.3, < 9)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@@ -834,7 +852,7 @@ GEM
caxlsx (<= 4.0)
csv
rodf
spring (4.4.0)
spring (4.4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-rubocop (0.4.0)
@@ -867,21 +885,22 @@ GEM
railties (>= 5.2)
redis (>= 4.0, < 6.0)
stringex (2.8.6)
stringio (3.1.8)
stripe (13.5.1)
stringio (3.2.0)
stripe (15.5.0)
swd (2.0.3)
activesupport (>= 3)
attr_required (>= 0.0.5)
faraday (~> 2.0)
faraday-follow_redirects
sysexits (1.2.0)
temple (0.8.2)
taler (0.2.0)
temple (0.10.4)
terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4)
thor (1.4.0)
thor (1.5.0)
thread-local (1.1.0)
tilt (2.6.1)
timeout (0.4.4)
tilt (2.7.0)
timeout (0.6.0)
tsort (0.2.0)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
@@ -902,8 +921,8 @@ GEM
simplecov_json_formatter
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uniform_notifier (1.17.0)
unicode-emoji (4.2.0)
uniform_notifier (1.18.0)
uri (1.1.1)
valid_email2 (5.2.3)
activemodel (>= 3.2)
@@ -940,26 +959,18 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.4.4)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.9.2)
websocket-driver (0.7.7)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
whenever (1.1.0)
chronic (>= 0.6.3)
wicked_pdf (2.8.2)
activesupport
ostruct
wkhtmltopdf-binary (0.12.6.10)
xml-simple (1.1.8)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.3)
zeitwerk (2.7.4)
PLATFORMS
ruby
@@ -1017,7 +1028,7 @@ DEPENDENCIES
good_migrations
haml
haml_lint
highline (= 2.0.3)
highline
i18n
i18n-js (~> 3.9.0)
i18n-tasks
@@ -1081,6 +1092,7 @@ DEPENDENCIES
rubocop-rspec_rails
sd_notify
select2-rails!
shakapacker (= 8.4.0)
shoulda-matchers
sidekiq
sidekiq-scheduler
@@ -1094,7 +1106,8 @@ DEPENDENCIES
stimulus_reflex
stimulus_reflex_testing!
stringex (~> 2.8.5)
stripe (~> 13)
stripe (~> 15)
taler
turbo-rails
turbo_power
undercover
@@ -1106,13 +1119,12 @@ DEPENDENCIES
web!
web-console
webmock
webpacker (~> 5)
whenever
wicked_pdf
wicked_pdf!
wkhtmltopdf-binary
RUBY VERSION
ruby 3.2.9p265
ruby 3.4.8p72
BUNDLED WITH
2.4.19
4.0.3

View File

@@ -1,5 +1,5 @@
# Foreman Procfile. Start all dev server processes with: `foreman start`
rails: DEV_CACHING=true bundle exec rails s -p 3000
webpack: ./bin/webpack-dev-server
webpack: ./bin/shakapacker-dev-server
sidekiq: DEV_CACHING=true bundle exec sidekiq -q mailers -q default

View File

@@ -1,5 +1,5 @@
# Foreman Procfile for Docker env. Start all dev server processes with: `bundle exec foreman start -f Procfile.docker`
webpack: WEBPACKER_DEV_SERVER_HOST=0.0.0.0 ./bin/webpack-dev-server
webpack: SHAKAPACKER_DEV_SERVER_HOST=0.0.0.0 ./bin/shakapacker-dev-server
sidekiq: DEV_CACHING=true bundle exec sidekiq -q mailers -q default
rails: WEBPACKER_DEV_SERVER_HOST=0.0.0.0 DEV_CACHING=true bundle exec rails s -p 3000 -b 0.0.0.0
rails: SHAKAPACKER_DEV_SERVER_HOST=0.0.0.0 DEV_CACHING=true bundle exec rails s -p 3000 -b 0.0.0.0

View File

@@ -1,5 +0,0 @@
angular.module('Darkswarm').controller "HomeCtrl", ($scope) ->
$scope.brandStoryExpanded = false
$scope.toggleBrandStory = ->
$scope.brandStoryExpanded = !$scope.brandStoryExpanded

View File

@@ -1,2 +0,0 @@
angular.module('Darkswarm').controller "TmpCtrl", ($scope)->
$scope.test = {foo: "bar"}

View File

@@ -1 +0,0 @@
@import './mail/all.scss';

View File

@@ -1,3 +0,0 @@
@import '../../../webpacker/css/admin/globals/palette.scss';
@import 'email';
@import 'payments_list';

View File

@@ -1,17 +1,18 @@
# frozen_string_literal: true
class SearchableDropdownComponent < ViewComponent::Base
REMOVED_SEARCH_PLUGIN = { 'tom-select-options-value': '{ "plugins": [] }' }.freeze
MINIMUM_OPTIONS_FOR_SEARCH_FIELD = 11 # at least 11 options are required for the search field
def initialize(
form:,
name:,
options:,
selected_option:,
placeholder_value:,
form: nil,
placeholder_value: '',
include_blank: false,
aria_label: '',
multiple: false,
remote_url: nil,
other_attrs: {}
)
@f = form
@@ -21,13 +22,15 @@ class SearchableDropdownComponent < ViewComponent::Base
@placeholder_value = placeholder_value
@include_blank = include_blank
@aria_label = aria_label
@multiple = multiple
@remote_url = remote_url
@other_attrs = other_attrs
end
private
attr_reader :f, :name, :options, :selected_option, :placeholder_value, :include_blank,
:aria_label, :other_attrs
:aria_label, :multiple, :remote_url, :other_attrs
def classes
"fullwidth #{'no-input' if remove_search_plugin?}"
@@ -36,11 +39,33 @@ class SearchableDropdownComponent < ViewComponent::Base
def data
{
controller: "tom-select",
'tom-select-placeholder-value': placeholder_value
}.merge(remove_search_plugin? ? REMOVED_SEARCH_PLUGIN : {})
'tom-select-placeholder-value': placeholder_value,
'tom-select-options-value': tom_select_options_value,
'tom-select-remote-url-value': remote_url,
}
end
def tom_select_options_value
plugins = []
plugins << 'virtual_scroll' if @remote_url.present?
plugins << 'dropdown_input' unless remove_search_plugin?
plugins << 'remove_button' if multiple
{
plugins:,
maxItems: multiple ? nil : 1,
}
end
def uses_form_builder?
f.present?
end
def remove_search_plugin?
@remove_search_plugin ||= options.count < MINIMUM_OPTIONS_FOR_SEARCH_FIELD
# Remove the search plugin when:
# - the select is multiple (it already includes a search field), or
# - there is no remote URL and the options are below the search threshold
@remove_search_plugin ||= multiple ||
(@remote_url.nil? && options.count < MINIMUM_OPTIONS_FOR_SEARCH_FIELD)
end
end

View File

@@ -1 +1,4 @@
= f.select name, options_for_select(options, selected_option), { include_blank: }, class: classes, data:, 'aria-label': aria_label, **other_attrs
- if uses_form_builder?
= f.select name, options, { selected: selected_option, include_blank:, multiple: }, class: classes, data:, 'aria-label': aria_label, **other_attrs
- else
= select_tag name, options_for_select(options, selected_option), include_blank:, multiple:, class: classes, data:, 'aria-label': aria_label, **other_attrs

View File

@@ -1,4 +1,4 @@
= render ConfirmModalComponent.new(id: dom_id(@order, :ship), confirm_reflexes: "click->Admin::OrdersReflex#ship", controller: "orders", reflex: "Admin::Orders#ship") do
= render ConfirmModalComponent.new(id: dom_id(@order, :ship), confirm_actions: "click->modal#close", confirm_reflexes: "click->Admin::OrdersReflex#ship", controller: "orders", reflex: "Admin::Orders#ship") do
%div{class: "margin-bottom-30"}
%p= t('spree.admin.orders.shipment.mark_as_shipped_message_html')
%div{class: "margin-bottom-30"}

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
class WebhookEndpointFormComponent < ViewComponent::Base
def initialize(webhooks:, webhook_type:)
@webhooks = webhooks
@webhook_type = webhook_type
end
private
attr_reader :webhooks, :webhook_type
def is_webhook_payment_status?
webhook_type == "payment_status_changed"
end
end

View File

@@ -0,0 +1,27 @@
-# Create new endpoints
- if webhooks.empty? # Only one allowed for now.
%tr
%td= t("components.webhook_endpoint_form.event_types.#{webhook_type}")
%td
= form_with(url: helpers.account_webhook_endpoints_path, id: "#{webhook_type}_webhook_endpoint") do |f|
= f.url_field :'webhook_endpoint[url]', id: "#{webhook_type}_webhook_endpoint_url", placeholder: t('components.webhook_endpoint_form.url.create_placeholder'), required: true, size: 64
= f.hidden_field :'webhook_endpoint[webhook_type]', id: "#{webhook_type}_webhook_endpoint_webhook_type", value: webhook_type
%td.actions
= button_tag t(:create), class: 'button primary tiny no-margin', form: "#{webhook_type}_webhook_endpoint"
-# Existing endpoints
- webhooks.each do |webhook_endpoint|
%tr
%td= t("components.webhook_endpoint_form.event_types.#{webhook_type}")
%td= webhook_endpoint.url
%td.actions.endpoints-actions
- if webhook_endpoint.persisted?
= button_to helpers.account_webhook_endpoint_path(webhook_endpoint), method: :delete,
class: "tiny alert no-margin",
data: { confirm: I18n.t(:are_you_sure) } do
= I18n.t(:delete)
- if is_webhook_payment_status?
= form_tag helpers.webhook_endpoint_test_account_path(webhook_endpoint), class: "button_to", 'data-turbo': true do
= button_tag type: "submit", class: "tiny alert no-margin", data: { confirm: I18n.t(:are_you_sure) } do
= I18n.t("components.webhook_endpoint_form.test_endpoint")

View File

@@ -7,6 +7,7 @@ module Admin
before_action :init_filters_params
before_action :init_pagination_params
before_action :init_none_tag
def index
fetch_products
@@ -27,7 +28,7 @@ module Admin
flash[:success] = I18n.t('admin.products_v3.bulk_update.success')
redirect_to [:index,
{ page: @page, per_page: @per_page, search_term: @search_term,
producer_id: @producer_id, category_id: @category_id }]
producer_id: @producer_id, category_id: @category_id, tags_name_in: @tags }]
elsif product_set.errors.present?
@error_counts = { saved: product_set.saved_count, invalid: product_set.invalid.count }
@@ -119,7 +120,7 @@ module Admin
@search_term = params[:search_term] || params[:_search_term]
@producer_id = params[:producer_id] || params[:_producer_id]
@category_id = params[:category_id] || params[:_category_id]
@tags = params[:tags_name_in] || params[:_tags_name_in]
@tags = params[:tags_name_in] || []
end
def init_pagination_params
@@ -179,6 +180,8 @@ module Admin
product_query = OpenFoodNetwork::Permissions.new(spree_current_user)
.editable_products.merge(product_scope_with_includes).ransack(ransack_query).result
product_query = apply_tags_filter(product_query)
# Postgres requires ORDER BY expressions to appear in the SELECT list when using DISTINCT.
# When the current ransack sort uses the computed stock columns, include them in the select
# so the generated COUNT/DISTINCT query is valid.
@@ -225,12 +228,51 @@ module Admin
query.merge!(Spree::Variant::SEARCH_KEY => @search_term)
end
query.merge!(variants_primary_taxon_id_in: @category_id) if @category_id.present?
query.merge!(variants_tags_name_in: @tags) if @tags.present?
query.merge!(@q) if @q
query
end
# Apply tags filter with OR logic:
# - Products with variants having selected tags
# - OR products with variants having no tags (when "None" is selected)
#
# Note: This cannot be implemented using Ransack because Ransack applies
# AND semantics across associations and cannot express OR logic that combines
# the presence and absence of the same associated records.
def apply_tags_filter(base_query)
return base_query if @tags.blank?
tag_names = Array(@tags).dup
has_none_tag = (tag_names.delete(@none_tag_value) == @none_tag_value)
queries = []
if tag_names.any?
# Products with at least one variant having one of the selected tags
tagged_product_ids = Spree::Variant
.joins(taggings: :tag)
.where(tags: { name: tag_names })
.select(:product_id)
queries << base_query.where(id: tagged_product_ids)
end
if has_none_tag
# Products where no variants have any tags
tagged_product_ids = Spree::Variant
.joins(:taggings)
.select(:product_id)
queries << base_query.where.not(id: tagged_product_ids)
end
return base_query if queries.empty?
# Combine queries using ActiveRecord's or method
queries.reduce { |combined, query| combined.or(query) }
end
# Optimise by pre-loading required columns
def product_query_includes
[
@@ -289,6 +331,10 @@ module Admin
t('.error')
end
end
def init_none_tag
@none_tag_value = '""'
end
end
end
# rubocop:enable Metrics/ClassLength

View File

@@ -4,6 +4,7 @@ module Admin
class ReportsController < Spree::Admin::BaseController
include ActiveStorage::SetCurrent
include ReportsActions
include Reports::AjaxSearch
helper ReportsHelper

View File

@@ -61,7 +61,7 @@ module Admin
def destroy
if @object.destroy
flash[:success] = flash_message_for(@object, :successfully_removed)
flash[:success] = Spree.t(:successfully_removed)
respond_with(@object) do |format|
format.html { redirect_to collection_url }
format.js { render partial: "spree/admin/shared/destroy" }
@@ -76,7 +76,7 @@ module Admin
protected
def resource_not_found
flash[:error] = flash_message_for(model_class.new, :not_found)
flash[:error] = Spree.t(:not_found)
redirect_to collection_url
end

View File

@@ -6,7 +6,7 @@ module Admin
class StripeAccountsController < Spree::Admin::BaseController
def connect
payload = params.permit(:enterprise_id).to_h
key = Openfoodnetwork::Application.config.secret_token
key = Rails.application.secret_key_base
url_params = { state: JWT.encode(payload, key, 'HS256'), scope: "read_write" }
redirect_to Stripe::OAuth.authorize_url(url_params)
end

View File

@@ -30,7 +30,7 @@ module Admin
status = :ok
if @rule.destroy
flash[:success] = Spree.t(:successfully_removed, resource: "Tag Rule")
flash[:success] = Spree.t(:successfully_removed, resource: Spree.t(:tag_rule))
else
flash.now[:error] = t(".destroy_error")
status = :internal_server_error

View File

@@ -14,7 +14,7 @@ module Admin
)
if @voucher.save
flash[:success] = I18n.t(:successfully_created, resource: "Voucher")
flash[:success] = I18n.t(:successfully_created, resource: Spree.t(:voucher))
redirect_to edit_admin_enterprise_path(@enterprise, anchor: :vouchers_panel)
else
render_error

View File

@@ -0,0 +1,108 @@
# frozen_string_literal: true
module Reports
module AjaxSearch
extend ActiveSupport::Concern
def search_enterprise_fees
report = report_class.new(spree_current_user, params, render: false)
fee_ids = enterprise_fee_ids(report.search.result)
query = EnterpriseFee.where(id: fee_ids)
render json: build_search_response(query)
end
def search_enterprise_fee_owners
report = report_class.new(spree_current_user, params, render: false)
owner_ids = enterprise_fee_owner_ids(report.search.result)
query = Enterprise.where(id: owner_ids)
render json: build_search_response(query)
end
def search_distributors
query = frontend_data.distributors
render json: build_search_response(query)
end
def search_order_cycles
query = frontend_data.order_cycles
render json: build_search_response(query)
end
def search_order_customers
query = frontend_data.order_customers
render json: build_search_response(query)
end
def search_suppliers
query = frontend_data.orders_suppliers
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}%"
# Handle different model types
if query.model == OrderCycle
query.where("order_cycles.name ILIKE ?", pattern)
elsif query.model == Customer
query.where("customers.email ILIKE ?", pattern)
else
query.where("name ILIKE ?", pattern)
end
end
def paginated_items(query, page, per_page)
if query.model == Customer
query.order(:email).offset((page - 1) * per_page).limit(per_page).pluck(:email, :id)
elsif query.model == OrderCycle
query.order('order_cycles.orders_close_at DESC')
.offset((page - 1) * per_page)
.limit(per_page).pluck(
:name, :id
)
else
query.order(:name).offset((page - 1) * per_page).limit(per_page).pluck(:name, :id)
end
end
def format_results(items)
items.map { |label, value| { value:, label: } }
end
def frontend_data
@frontend_data ||= Reporting::FrontendData.new(spree_current_user)
end
def enterprise_fee_owner_ids(orders)
EnterpriseFee.where(id: enterprise_fee_ids(orders)).select(:enterprise_id)
end
def enterprise_fee_ids(orders)
Spree::Adjustment.enterprise_fee.where(order_id: orders.select(:id)).select(:originator_id)
end
end
end

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
module PaymentGateways
class TalerController < BaseController
include OrderCompletion
# The Taler merchant backend has taken the payment.
# Now we just need to confirm that and update our local database
# before finalising the order.
def confirm
payment = Spree::Payment.find(params[:payment_id])
@order = payment.order
process_payment_completion!
end
end
end

View File

@@ -68,7 +68,7 @@ module Spree
destroy_before
if @object.destroy
flash[:success] = flash_message_for(@object, :successfully_removed)
flash[:success] = Spree.t(:successfully_removed)
end
redirect_to location_after_save

View File

@@ -11,10 +11,11 @@ module Spree
respond_to :html
PAYMENT_METHODS = %w{
Spree::PaymentMethod::Check
Spree::Gateway::PayPalExpress
Spree::Gateway::StripeSCA
}.index_with(&:constantize).freeze
Spree::PaymentMethod::Check
Spree::PaymentMethod::Taler
}.freeze
def create
force_environment
@@ -95,7 +96,7 @@ module Spree
@payment_method = PaymentMethod.find(params[:pm_id])
end
else
@payment_method = PAYMENT_METHODS.fetch(params[:provider_type], PaymentMethod).new
@payment_method = PaymentMethod.new(type: params[:provider_type])
end
render partial: 'provider_settings'
@@ -117,7 +118,7 @@ module Spree
end
def validate_payment_method_provider
valid_payment_methods = Rails.application.config.spree.payment_methods.map(&:to_s)
valid_payment_methods = PAYMENT_METHODS
return if valid_payment_methods.include?(params[:payment_method][:type])
flash[:error] = Spree.t(:invalid_payment_provider)
@@ -133,11 +134,9 @@ module Spree
end
def load_providers
providers = Gateway.providers.sort_by(&:name)
providers = PAYMENT_METHODS.dup
unless show_stripe?
providers.reject! { |provider| stripe_provider?(provider) }
end
providers.delete("Spree::Gateway::StripeSCA") unless show_stripe?
providers
end
@@ -164,10 +163,6 @@ module Spree
@payment_method.try(:type) == "Spree::Gateway::StripeSCA"
end
def stripe_provider?(provider)
provider.name.ends_with?("StripeSCA")
end
def base_params
@base_params ||= PermittedAttributes::PaymentMethod.new(params[:payment_method]).
call.to_h.with_indifferent_access

View File

@@ -95,8 +95,7 @@ module Spree
private
def load_payment_source
if @payment.payment_method.is_a?(Spree::Gateway) &&
@payment.payment_method.payment_profiles_supported? &&
if @payment.payment_method.is_a?(Gateway::StripeSCA) &&
params[:card].present? &&
(params[:card] != 'new')
@payment.source = CreditCard.find_by(id: params[:card])

View File

@@ -16,7 +16,7 @@ module Spree
@url_filters = ::ProductFilters.new.extract(request.query_parameters)
if @object.destroy
flash[:success] = flash_message_for(@object, :successfully_removed)
flash[:success] = Spree.t(:successfully_removed)
end
# if destroy fails it won't show any errors to the user
redirect_to spree.admin_product_product_properties_url(params[:product_id], @url_filters)

View File

@@ -36,7 +36,7 @@ module Spree
end
@object.touch :deleted_at
flash[:success] = flash_message_for(@object, :successfully_removed)
flash[:success] = Spree.t(:successfully_removed)
respond_with(@object) do |format|
format.html { redirect_to collection_url }

View File

@@ -5,7 +5,7 @@ module Spree
class TaxCategoriesController < ::Admin::ResourceController
def destroy
if @object.destroy
flash[:success] = flash_message_for(@object, :successfully_removed)
flash[:success] = Spree.t(:successfully_removed)
respond_with(@object) do |format|
format.html { redirect_to collection_url }
format.js { render partial: "spree/admin/shared/destroy" }

View File

@@ -9,7 +9,6 @@ module Spree
include Spree::Core::ControllerHelpers::Auth
include Spree::Core::ControllerHelpers::Common
include Spree::Core::ControllerHelpers::Order
include CablecarResponses
helper 'spree/base'
@@ -24,14 +23,12 @@ module Spree
if spree_user_signed_in?
flash[:success] = t('devise.success.logged_in_succesfully')
render cable_ready: cable_car.redirect_to(
url: return_url_or_default(after_sign_in_path_for(spree_current_user))
)
redirect_to return_url_or_default(after_sign_in_path_for(spree_current_user))
else
render status: :unauthorized, cable_ready: cable_car.inner_html(
"#login-feedback",
partial("layouts/alert", locals: { type: "alert", message: t('devise.failure.invalid') })
)
message = t('devise.failure.invalid')
render turbo_stream: turbo_stream.update(
'login-feedback', partial: 'layouts/alert', locals: { message:, type: 'alert' }
), status: :unprocessable_entity
end
end
@@ -60,11 +57,13 @@ module Spree
end
def render_unconfirmed_response
render status: :unprocessable_entity, cable_ready: cable_car.inner_html(
"#login-feedback",
partial("layouts/alert", locals: { type: "alert", message: t(:email_unconfirmed),
unconfirmed: true, tab: "login" })
)
message = t(:email_unconfirmed)
render turbo_stream: turbo_stream.update(
'login-feedback',
partial: 'layouts/alert', locals: { type: "alert", message:, unconfirmed: true,
tab: "login", email: params.dig(:spree_user, :email) }
), status: :unprocessable_entity
end
def ensure_valid_locale_persisted

View File

@@ -3,7 +3,6 @@
module Spree
class UsersController < ::BaseController
include I18nHelper
include CablecarResponses
layout 'darkswarm'
@@ -25,34 +24,17 @@ module Spree
@unconfirmed_email = spree_current_user.unconfirmed_email
end
# Endpoint for queries to check if a user is already registered
def registered_email
registered = Spree::User.find_by(email: params[:email]).present?
if registered
render status: :ok, cable_ready: cable_car.
inner_html(
"#login-feedback",
partial("layouts/alert",
locals: { type: "alert", message: t('devise.failure.already_registered') })
).
dispatch_event(name: "login:modal:open")
else
head :not_found
end
end
def create
@user = Spree::User.new(user_params)
if @user.save
flash[:success] = t('devise.user_registrations.spree_user.signed_up_but_unconfirmed')
render cable_ready: cable_car.redirect_to(url: main_app.root_path)
redirect_to main_app.root_path
else
render status: :unprocessable_entity, cable_ready: cable_car.morph(
"#signup-tab",
partial("layouts/signup_tab", locals: { signup_form_user: @user })
)
render turbo_stream: turbo_stream.update(
'signup-tab',
partial: 'layouts/signup_tab', locals: { signup_form_user: @user }
), status: :unprocessable_entity
end
end
@@ -97,13 +79,10 @@ module Spree
end
def render_alert_timestamp_error_message
render cable_ready: cable_car.inner_html(
"#signup-feedback",
partial("layouts/alert",
locals: {
type: "alert",
message: InvisibleCaptcha.timestamp_error_message
})
render turbo_stream: turbo_stream.update(
'signup-feedback',
partial: 'layouts/alert',
locals: { type: "alert", message: InvisibleCaptcha.timestamp_error_message }
)
end
end

View File

@@ -3,7 +3,6 @@
class UserConfirmationsController < DeviseController
# Needed for access to current_ability, so we can authorize! actions
include Spree::Core::ControllerHelpers::Auth
include CablecarResponses
# GET /resource/confirmation?confirmation_token=abcdef
def show
@@ -29,12 +28,12 @@ class UserConfirmationsController < DeviseController
set_flash_message(:error, :confirmation_not_sent)
end
else
render cable_ready: cable_car.inner_html(
"##{params[:tab] || 'forgot'}-feedback",
partial("layouts/alert",
locals: { type: "success", message: t("devise.confirmations.send_instructions") })
flash.now[:sucess] = t("devise.confirmations.send_instructions")
return render turbo_stream: turbo_stream.update(
"#{params[:tab] || 'forgot'}-feedback",
partial: 'shared/flashes', locals: { flashes: flash }
)
return
end
respond_with_navigational(resource){ redirect_to login_path }

View File

@@ -1,37 +1,40 @@
# frozen_string_literal: true
class UserPasswordsController < Spree::UserPasswordsController
include CablecarResponses
layout 'darkswarm'
def create
return render_unconfirmed_response if user_unconfirmed?
self.resource = resource_class.send_reset_password_instructions(raw_params[resource_name])
status = :ok
if resource.errors.empty?
render cable_ready: cable_car.inner_html(
"#forgot-feedback",
partial("layouts/alert", locals: { type: "success", message: t(:password_reset_sent) })
)
message, type = [t(:password_reset_sent), :success]
else
render status: :not_found, cable_ready: cable_car.inner_html(
"#forgot-feedback",
partial("layouts/alert", locals: { type: "alert", message: t(:email_not_found) })
)
message, type = [t(:email_not_found), :alert]
status = :not_found
end
render turbo_stream: turbo_stream.update(
'forgot-feedback',
partial: 'layouts/alert',
locals: { type:, message:, tab: 'forgot',
unconfirmed: false, email: params.dig(:spree_user, :email) }
), status:
end
private
def render_unconfirmed_response
render status: :unprocessable_entity, cable_ready: cable_car.inner_html(
"#forgot-feedback",
partial("layouts/alert",
locals: { type: "alert", message: t(:email_unconfirmed),
unconfirmed: true, tab: "forgot" })
)
message, type, unconfirmed, tab = [t(:email_unconfirmed), :alert, true, 'forgot']
render turbo_stream: turbo_stream.update(
'forgot-feedback',
partial: 'layouts/alert',
locals: { type:, message:, tab:,
unconfirmed:, email: params.dig(:spree_user, :email) }
), status: :unprocessable_entity
end
def user_unconfirmed?

View File

@@ -90,11 +90,13 @@ class VoucherAdjustmentsController < BaseController
voucher_code: voucher_params[:voucher_code], enterprise: @order.distributor
)
voucher = vine_voucher_validator.validate
errors = vine_voucher_validator.errors
return nil if vine_voucher_validator.errors[:not_found_voucher].present?
return nil if errors[:not_found_voucher].present?
if vine_voucher_validator.errors.present?
@order.errors.add(:voucher_code, I18n.t('checkout.errors.add_voucher_error'))
if errors.present?
message = errors[:invalid_voucher] || I18n.t('checkout.errors.add_voucher_error')
@order.errors.add(:voucher_code, message)
return nil
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class WebhookEndpointsController < BaseController
before_action :load_resource, only: :destroy
before_action :load_resource, only: [:destroy, :test]
def create
webhook_endpoint = spree_current_user.webhook_endpoints.new(webhook_endpoint_params)
@@ -25,12 +25,30 @@ class WebhookEndpointsController < BaseController
redirect_to redirect_path
end
def test
at = Time.zone.now
test_payload = Payments::WebhookPayload.test_data.to_hash
WebhookDeliveryJob.perform_later(@webhook_endpoint.url, "payment.completed", test_payload, at:)
flash[:success] = t(".success")
respond_with do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.update(
:flashes, partial: "shared/flashes", locals: { flashes: flash }
)
end
end
end
private
def load_resource
@webhook_endpoint = spree_current_user.webhook_endpoints.find(params[:id])
end
def webhook_endpoint_params
params.require(:webhook_endpoint).permit(:url)
params.require(:webhook_endpoint).permit(:url, :webhook_type)
end
def redirect_path

View File

@@ -72,7 +72,23 @@ module ApplicationHelper
end
end
# Update "v1" to invalidate existing cache key
def cache_key_with_locale(key, locale)
Array.wrap(key) + [locale.to_s, I18nDigests.for_locale(locale)]
Array.wrap(key) + ["v3", locale.to_s, I18nDigests.for_locale(locale)]
end
def pdf_stylesheet_pack_tag(source)
# With shakapacker dev server running, the wicked_pdf_stylesheet_pack_tag will produce a
# relative path, because we don't have `config.action_controller.asset_host`. Relative path
# can't be resolved by `wkhtmltopdf`. So we pass the wepacker dev server host and port to
# the shakapacker helper, so it generates the correct url.
# For more info: https://stackoverflow.com/questions/58490299/how-to-include-css-stylesheet-into-wicked-pdf/60541688#60541688
if running_in_development?
options = { media: "all",
host: "#{Shakapacker.dev_server.host}:#{Shakapacker.dev_server.port}" }
stylesheet_pack_tag(source, **options)
else
wicked_pdf_stylesheet_pack_tag(source)
end
end
end

View File

@@ -34,29 +34,8 @@ module ReportsHelper
end
end
def fee_name_options(orders)
EnterpriseFee.where(id: enterprise_fee_ids(orders))
.pluck(:name, :id)
end
def fee_owner_options(orders)
Enterprise.where(id: enterprise_fee_owner_ids(orders))
.pluck(:name, :id)
end
delegate :currency_symbol, to: :'Spree::Money'
def enterprise_fee_owner_ids(orders)
EnterpriseFee.where(id: enterprise_fee_ids(orders))
.pluck(:enterprise_id)
end
def enterprise_fee_ids(orders)
Spree::Adjustment.enterprise_fee
.where(order_id: orders.map(&:id))
.pluck(:originator_id)
end
def datepicker_time(datetime)
datetime = Time.zone.parse(datetime) if datetime.is_a? String
datetime.strftime('%Y-%m-%d %H:%M')

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
module Spree
module Admin
module PaymentMethodsHelper
def payment_method_type_name(class_name)
scope = "spree.admin.payment_methods.providers"
key = class_name.demodulize.downcase
I18n.t(key, scope:)
end
def payment_method_type_options(providers)
providers.map { |p| [payment_method_type_name(p), p] }
end
end
end
end

View File

@@ -7,11 +7,9 @@ class PaymentMailer < ApplicationMailer
@payment = payment
@order = @payment.order
@hide_ofn_navigation = @payment.order.distributor.hide_ofn_navigation
subject = I18n.t('spree.payment_mailer.authorize_payment.subject',
distributor: @order.distributor.name)
I18n.with_locale valid_locale(@order.user) do
mail(to: @order.email,
subject:,
subject: default_i18n_subject(distributor: @order.distributor.name),
reply_to: @order.distributor.contact.email)
end
end
@@ -19,11 +17,20 @@ class PaymentMailer < ApplicationMailer
def authorization_required(payment)
@order = payment.order
shop_owner = @order.distributor.owner
subject = I18n.t('spree.payment_mailer.authorization_required.subject',
order: @order)
I18n.with_locale valid_locale(shop_owner) do
mail(to: shop_owner.email,
subject:,
mail(to: shop_owner.email, reply_to: @order.email)
end
end
def refund_available(payment, taler_order_status_url)
@order = payment.order
@shop = @order.distributor.name
@amount = payment.display_amount
@taler_order_status_url = taler_order_status_url
I18n.with_locale valid_locale(@order.user) do
mail(to: @order.email,
subject: default_i18n_subject(shop: @shop),
reply_to: @order.email)
end
end

View File

@@ -12,7 +12,10 @@ class ApplicationRecord < ActiveRecord::Base
self.include_root_in_json = true
def self.image_service
ENV["S3_BUCKET"].present? ? :amazon_public : :local
return :local if ENV["S3_BUCKET"].blank?
return :amazon_public if ENV["S3_ENDPOINT"].blank?
:s3_compatible_storage_public
end
# We might have a development environment without S3 but with a database

View File

@@ -111,14 +111,14 @@ class Enterprise < ApplicationRecord
end
validates :logo,
processable_image: true,
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
processable_file: true,
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
validates :promo_image,
processable_image: true,
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
processable_file: true,
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
validates :white_label_logo,
processable_image: true,
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
processable_file: true,
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
validates :terms_and_conditions, content_type: {
in: "application/pdf",
message: I18n.t(:enterprise_terms_and_conditions_type_error),

View File

@@ -29,11 +29,11 @@ class EnterpriseGroup < ApplicationRecord
has_one_attached :promo_image, service: image_service
validates :logo,
processable_image: true,
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
processable_file: true,
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
validates :promo_image,
processable_image: true,
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
processable_file: true,
content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
scope :by_position, -> { order('position ASC') }
scope :on_front_page, -> { where(on_front_page: true) }

View File

@@ -4,7 +4,7 @@ class Invoice < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :order, class_name: 'Spree::Order'
serialize :data, Hash, coder: YAML
serialize :data, type: Hash, coder: YAML
before_validation :serialize_order
after_create :cancel_previous_invoices
default_scope { order(created_at: :desc) }

View File

@@ -95,8 +95,8 @@ class Invoice
def display_line_item_tax_rate(item)
all_tax_adjustments.select { |a|
a.adjustable.type == 'Spree::LineItem' && a.adjustable.id == item.id
}.map(&:originator).map { |tr|
number_to_percentage(tr.amount * 100, precision: 1)
}.map(&:originator).map(&:amount).sort.map { |amount|
number_to_percentage(amount * 100, precision: 1)
}.join(", ")
end

View File

@@ -4,5 +4,5 @@ class ReportRenderingOptions < ApplicationRecord
self.belongs_to_required_by_default = false
belongs_to :user, class_name: "Spree::User"
serialize :options, Hash, coder: YAML
serialize :options, type: Hash, coder: YAML
end

View File

@@ -6,6 +6,11 @@ module Spree
class Ability
include CanCan::Ability
REPORTS_SEARCH_ACTIONS = [
:search_enterprise_fees, :search_enterprise_fee_owners, :search_distributors,
:search_suppliers, :search_order_cycles, :search_order_customers
].freeze
def initialize(user)
clear_aliased_actions
@@ -260,7 +265,8 @@ module Spree
can [:admin, :index, :import], ::Admin::DfcProductImportsController
# Reports page
can [:admin, :index, :show, :create], ::Admin::ReportsController
can [:admin, :index, :show, :create, *REPORTS_SEARCH_ACTIONS],
::Admin::ReportsController
can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments,
:orders_and_fulfillment, :products_and_inventory, :order_cycle_management,
:packing, :enterprise_fee_summary, :bulk_coop, :suppliers], :report
@@ -392,7 +398,7 @@ module Spree
end
# Reports page
can [:admin, :index, :show, :create], ::Admin::ReportsController
can [:admin, :index, :show, :create, *REPORTS_SEARCH_ACTIONS], ::Admin::ReportsController
can [:admin, :customers, :group_buys, :sales_tax, :payments,
:orders_and_distributors, :orders_and_fulfillment, :products_and_inventory,
:order_cycle_management, :xero_invoices, :enterprise_fee_summary, :bulk_coop], :report

View File

@@ -31,7 +31,6 @@ module Spree
preference :admin_products_per_page, :integer, default: 10
# Should only be true if you don't need to track inventory
preference :allow_backorder_shipping, :boolean, default: false
preference :allow_checkout_on_gateway_error, :boolean, default: false
preference :allow_guest_checkout, :boolean, default: true
preference :currency_decimal_mark, :string, default: "."
preference :currency_symbol_position, :string, default: "before"

View File

@@ -5,7 +5,7 @@ module Spree
acts_as_taggable
include PaymentMethodDistributors
delegate :authorize, :purchase, :capture, :void, :credit, to: :provider
delegate :authorize, :purchase, :capture, :void, :credit, :refund, to: :provider
validates :name, :type, presence: true
@@ -35,6 +35,10 @@ module Spree
end
def method_missing(method, *)
message = "Deprecated delegation of Gateway##{method}"
Alert.raise(message)
raise message if Rails.env.local?
if @provider.nil? || !@provider.respond_to?(method)
super
else
@@ -42,10 +46,6 @@ module Spree
end
end
def payment_profiles_supported?
false
end
def method_type
'gateway'
end

View File

@@ -35,10 +35,6 @@ module Spree
ActiveMerchant::Billing::StripePaymentIntentsGateway
end
def payment_profiles_supported?
true
end
def stripe_account_id
StripeAccount.find_by(enterprise_id: preferred_enterprise_id)&.stripe_user_id
end
@@ -84,7 +80,7 @@ module Spree
end
# NOTE: this method is required by Spree::Payment::Processing
def void(payment_intent_id, _creditcard, gateway_options)
def void(payment_intent_id, gateway_options)
payment_intent_response = Stripe::PaymentIntent.retrieve(
payment_intent_id, stripe_account: stripe_account_id
)
@@ -101,7 +97,13 @@ module Spree
end
# NOTE: this method is required by Spree::Payment::Processing
def credit(money, _creditcard, payment_intent_id, gateway_options)
def credit(money, payment_intent_id, gateway_options)
gateway_options[:stripe_account] = stripe_account_id
provider.refund(money, payment_intent_id, gateway_options)
end
# NOTE: this method is required by Spree::Payment::Processing
def refund(money, payment_intent_id, gateway_options)
gateway_options[:stripe_account] = stripe_account_id
provider.refund(money, payment_intent_id, gateway_options)
end

View File

@@ -2,6 +2,8 @@
module Spree
class Image < Asset
ACCEPTED_CONTENT_TYPES = %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
has_one_attached :attachment, service: image_service do |attachment|
attachment.variant :mini, resize_to_fill: [48, 48]
attachment.variant :small, resize_to_fill: [227, 227]
@@ -11,8 +13,8 @@ module Spree
validates :attachment,
attached: true,
processable_image: true,
content_type: %r{\Aimage/(png|jpeg|gif|jpg|svg\+xml|webp)\Z}
processable_file: true,
content_type: ACCEPTED_CONTENT_TYPES
validate :no_attachment_errors
def self.default_image_url(size)

View File

@@ -24,6 +24,8 @@ module Spree
before_validation :copy_price
before_validation :copy_tax_category
before_validation :copy_dimensions
before_validation :copy_product_name, on: :create
before_validation :copy_variant_name, on: :create
validates :quantity, numericality: {
only_integer: true,
@@ -250,6 +252,18 @@ module Spree
adjustments.enterprise_fee
end
def full_variant_name
return variant_name if variant_name.present?
variant.full_name
end
def full_product_name
return product_name if product_name.present?
variant.product.name
end
private
def computed_weight_from_variant
@@ -274,6 +288,18 @@ module Spree
order.create_tax_charge!
end
def copy_product_name
return if variant.nil? || variant.product.nil?
self.product_name = variant.product.name
end
def copy_variant_name
return if variant.nil?
self.variant_name = variant.full_name
end
def update_inventory_before_destroy
# This is necessary before destroying the line item
# so that update_inventory will restore stock to the variant

View File

@@ -437,18 +437,13 @@ module Spree
#
# Returns:
# - true if all pending_payments processed successfully
# - true if a payment failed, ie. raised a GatewayError
# which gets rescued and converted to TRUE when
# :allow_checkout_gateway_error is set to true
# - false if a payment failed, ie. raised a GatewayError
# which gets rescued and converted to FALSE when
# :allow_checkout_on_gateway_error is set to false
# which gets rescued and converted to FALSE
#
def process_payments!
process_each_payment(&:process!)
rescue Core::GatewayError => e
result = !!Spree::Config[:allow_checkout_on_gateway_error]
errors.add(:base, e.message) && (return result)
errors.add(:base, e.message) && (return false)
end
def process_payments_offline!

View File

@@ -34,7 +34,7 @@ module Spree
# invalidate previously entered payments
after_create :invalidate_old_payments
after_save :create_payment_profile, if: :profiles_supported?
after_save :create_payment_profile
# update the order totals, etc.
after_save :ensure_correct_adjustment, :update_order
@@ -101,6 +101,24 @@ module Spree
end
after_transition to: :completed, do: :set_captured_at
after_transition do |payment, transition|
# Catch any exceptions to prevent any rollback potentially
# preventing payment from going through
ActiveSupport::Notifications.instrument(
"ofn.payment_transition", payment: payment, event: transition.to
)
rescue StandardError => e
Rails.logger.fatal "ActiveSupport::Notification.instrument failed params: " \
"<event_type:ofn.payment_transition> " \
"<payment_id:#{payment.id}> " \
"<event:#{transition.to}>"
Alert.raise(
e,
metadata: {
event_tye: "ofn.payment_transition", payment_id: payment.id, event: transition.to
}
)
end
end
def money
@@ -199,18 +217,13 @@ module Spree
errors.blank?
end
def profiles_supported?
payment_method.respond_to?(:payment_profiles_supported?) &&
payment_method.payment_profiles_supported?
end
def create_payment_profile
return unless source.is_a?(CreditCard)
return unless source.try(:save_requested_by_customer?)
return unless source.number || source.gateway_payment_profile_id
return unless source.gateway_customer_profile_id.nil?
payment_method.create_profile(self)
payment_method.try(:create_profile, self)
rescue ActiveMerchant::ConnectionError => e
gateway_error e
end

View File

@@ -58,16 +58,7 @@ module Spree
protect_from_connection_error do
check_environment
response = if payment_method.payment_profiles_supported?
# Gateways supporting payment profiles will need access to credit
# card object because this stores the payment profile information
# so supply the authorization itself as well as the credit card,
# rather than just the authorization code
payment_method.void(response_code, source, gateway_options)
else
# Standard ActiveMerchant void usage
payment_method.void(response_code, gateway_options)
end
response = payment_method.void(response_code, gateway_options)
record_response(response)
@@ -86,20 +77,11 @@ module Spree
credit_amount = calculate_refund_amount(credit_amount)
response = if payment_method.payment_profiles_supported?
payment_method.credit(
(credit_amount * 100).round,
source,
response_code,
gateway_options
)
else
payment_method.credit(
(credit_amount * 100).round,
response_code,
gateway_options
)
end
response = payment_method.credit(
(credit_amount * 100).round,
response_code,
gateway_options
)
record_response(response)
@@ -125,20 +107,11 @@ module Spree
refund_amount = calculate_refund_amount(refund_amount)
response = if payment_method.payment_profiles_supported?
payment_method.refund(
(refund_amount * 100).round,
source,
response_code,
gateway_options
)
else
payment_method.refund(
(refund_amount * 100).round,
response_code,
gateway_options
)
end
response = payment_method.refund(
(refund_amount * 100).round,
response_code,
gateway_options
)
record_response(response)
@@ -183,6 +156,7 @@ module Spree
options.merge!({ billing_address: order.bill_address.try(:active_merchant_hash),
shipping_address: order.ship_address.try(:active_merchant_hash) })
options.merge!(payment: self)
options
end

View File

@@ -52,10 +52,6 @@ module Spree
.where(environment: [Rails.env, "", nil])
}
def self.providers
Rails.application.config.spree.payment_methods
end
def configured?
!stripe? || stripe_configured?
end
@@ -93,12 +89,8 @@ module Spree
type.demodulize.downcase
end
def self.find_with_destroyed(*args)
unscoped { find(*args) }
end
def payment_profiles_supported?
false
def self.find_with_destroyed(*)
unscoped { find(*) }
end
def source_required?
@@ -117,11 +109,6 @@ module Spree
distributors.include?(distributor)
end
def self.clean_name
i18n_key = "spree.admin.payment_methods.providers.#{name.demodulize.downcase}"
I18n.t(i18n_key)
end
private
def distributor_validation

View File

@@ -0,0 +1,118 @@
# frozen_string_literal: true
require "taler"
module Spree
class PaymentMethod
# GNU Taler is a distributed, open source payment system.
# You need a hosted Taler backend server to process payments.
#
# For testing, you can use the official demo backend:
#
# - Merchant UX: https://backend.demo.taler.net
# - Username: sandbox
# - Password: sandbox
#
# Configure this payment method for testing with:
#
# - backend_url: https://backend.demo.taler.net/instances/sandbox
# - api_key: sandbox
class Taler < PaymentMethod
preference :backend_url, :string
preference :api_key, :password
def actions
%w{void}
end
def can_void?(payment)
payment.state == "completed"
end
# Name of the view to display during checkout
def method_type
"check" # empty view
end
def external_gateway?
true
end
# The backend provides this URL. It can look like this:
# https://backend.demo.taler.net/instances/blog/orders/2026..?token=S8Y..&session_id=b0b..
def external_payment_url(options)
order = options.fetch(:order)
payment = load_payment(order)
payment.source ||= self
payment.response_code ||= create_taler_order(payment)
payment.save! if payment.changed?
taler_order.status_url
end
# Main method called by Spree::Payment::Processing during checkout
# when the user is redirected back to the app.
#
# The payment has already been made and we need to verify the success.
def purchase(_money, _source, gateway_options)
payment = gateway_options[:payment]
return unless payment.response_code
taler_order = taler_order(id: payment.response_code)
status = taler_order.fetch("order_status")
success = (status == "paid")
message = I18n.t(status, default: status, scope: "taler.order_status")
ActiveMerchant::Billing::Response.new(success, message)
end
def void(response_code, gateway_options)
payment = gateway_options[:payment]
taler_order = taler_order(id: response_code)
status = taler_order.fetch("order_status")
if status == "claimed"
return ActiveMerchant::Billing::Response.new(true, "Already expired")
end
raise "Unsupported action" if status != "paid"
amount = taler_order.fetch("contract_terms")["amount"]
taler_order.refund(refund: amount, reason: "void")
PaymentMailer.refund_available(payment, taler_order.status_url).deliver_later
ActiveMerchant::Billing::Response.new(true, "Refund initiated")
end
private
def load_payment(order)
order.payments.checkout.where(payment_method: self).last
end
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}"
urls = Rails.application.routes.url_helpers
fulfillment_url = urls.payment_gateways_confirm_taler_url(payment_id: payment.id)
taler_order.create(
amount: taler_amount,
summary: I18n.t("payment_method_taler.order_summary"),
fulfillment_url:,
)
end
def taler_order(id: nil)
@taler_order ||= ::Taler::Order.new(
backend_url: preferred_backend_url,
password: preferred_api_key,
id:,
)
end
end
end
end

View File

@@ -251,7 +251,7 @@ module Spree
transaction do
ExchangeVariant.
where(exchange_variants: { variant_id: variants.with_deleted.
select(:id) }).destroy_all
select(:id) }).destroy_all
yield
end

View File

@@ -35,8 +35,8 @@ module Spree
taxons
.pluck('spree_taxons.id, enterprises.id AS enterprise_id')
.each_with_object({}) do |(taxon_id, enterprise_id), collection|
collection[enterprise_id.to_i] ||= Set.new
collection[enterprise_id.to_i] << taxon_id
collection[enterprise_id.to_i] ||= Set.new
collection[enterprise_id.to_i] << taxon_id
end
end

View File

@@ -167,8 +167,8 @@ module Spree
# In Rails 3, merging two scopes on the same column will consider only the last scope.
def self.in_distributor(distributor)
where(id: ExchangeVariant.select(:variant_id).
joins(:exchange).
where('exchanges.incoming = ? AND exchanges.receiver_id = ?', false, distributor))
joins(:exchange).
where('exchanges.incoming = ? AND exchanges.receiver_id = ?', false, distributor))
end
def self.indexed
@@ -179,11 +179,11 @@ module Spree
# "where(id:" is necessary so that the returned relation has no includes
# The relation without includes will not be readonly and allow updates on it
where(spree_variants: { id: joins(:prices).
where(deleted_at: nil).
where('spree_prices.currency' =>
where(deleted_at: nil).
where('spree_prices.currency' =>
currency || CurrentConfig.get(:currency)).
where.not(spree_prices: { amount: nil }).
select("spree_variants.id") })
where.not(spree_prices: { amount: nil }).
select("spree_variants.id") })
end
def self.linked_to(semantic_id)

View File

@@ -2,5 +2,11 @@
# Records a webhook url to send notifications to
class WebhookEndpoint < ApplicationRecord
WEBHOOK_TYPES = %w(order_cycle_opened payment_status_changed).freeze
validates :url, presence: true
validates :webhook_type, presence: true, inclusion: { in: WEBHOOK_TYPES }
scope :order_cycle_opened, -> { where(webhook_type: "order_cycle_opened") }
scope :payment_status, -> { where(webhook_type: "payment_status_changed") }
end

View File

@@ -60,8 +60,8 @@ class ProductScopeQuery
def product_query_includes
[
image: { attachment_attachment: :blob },
variants: [:default_price, :stock_items, :variant_overrides]
{ image: { attachment_attachment: :blob },
variants: [:default_price, :stock_items, :variant_overrides] }
]
end

View File

@@ -42,10 +42,10 @@ class DfcCatalogImporter
.includes(:semantic_links).references(:semantic_links)
.where.not(semantic_links: { semantic_id: present_ids })
.select do |variant|
# Variants that were in the same catalog before:
variant.semantic_links.map(&:semantic_id).any? do |semantic_id|
FdcUrlBuilder.new(semantic_id).catalog_url == catalog_url
end
# Variants that were in the same catalog before:
variant.semantic_links.map(&:semantic_id).any? do |semantic_id|
FdcUrlBuilder.new(semantic_id).catalog_url == catalog_url
end
end
end
end

View File

@@ -11,7 +11,9 @@ class ImageImporter
image = Spree::Image.create do |img|
PrivateAddressCheck.only_public_connections do
img.attachment.attach(io: valid_url.open, filename:, metadata:)
io = valid_url.open
content_type = Marcel::MimeType.for(io)
img.attachment.attach(io:, filename:, metadata:, content_type:)
end
end
product.image = image if image

View File

@@ -51,8 +51,8 @@ class LineItemSyncer
def destroy_obsolete_items(order)
order.line_items.
where(variant_id: subscription_line_items.
select(&:marked_for_destruction?).
map(&:variant_id)).
select(&:marked_for_destruction?).
map(&:variant_id)).
destroy_all
end
@@ -77,7 +77,7 @@ class LineItemSyncer
end
def add_order_update_issue(order, line_item)
issue_description = "#{line_item.product.name} - #{line_item.variant.full_name}"
issue_description = "#{line_item.product.name} - #{line_item.full_variant_name}"
issue_description << " - #{stock_issue_description(line_item)}" if line_item.insufficient_stock?
order_update_issues.add(order, issue_description)
end

View File

@@ -11,10 +11,12 @@ module OrderCycles
.merge(coordinator_name: order_cycle.coordinator.name)
# Endpoints for coordinator owner
webhook_endpoints = order_cycle.coordinator.owner.webhook_endpoints
webhook_endpoints = order_cycle.coordinator.owner.webhook_endpoints.order_cycle_opened
# Plus unique endpoints for distributor owners (ignore duplicates)
webhook_endpoints |= order_cycle.distributors.map(&:owner).flat_map(&:webhook_endpoints)
webhook_endpoints |= order_cycle.distributors.map(&:owner).flat_map { |owner|
owner.webhook_endpoints.order_cycle_opened
}
webhook_endpoints.each do |endpoint|
WebhookDeliveryJob.perform_later(endpoint.url, event, webhook_payload, at:)

View File

@@ -130,12 +130,11 @@ module Orders
def order_cycle_fees
return @order_cycle_fees if defined? @order_cycle_fees
return [] unless order_cycle && distributor
@order_cycle_fees = begin
fees = []
return fees unless order_cycle && distributor
order_cycle.exchanges.supplying_to(distributor).each do |exchange|
exchange.enterprise_fees.per_item.each do |enterprise_fee|
fee_value = FeeValue.new(fee: enterprise_fee, role: exchange.role)

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
# Called by "ActiveSupport::Notifications" when an "ofn.payment_transition" occurs
# Event originate from Spree::Payment event machine
#
module Payments
class StatusChangedListenerService
def call(_name, started, _finished, _unique_id, payload)
event = "payment.#{payload[:event]}"
Payments::WebhookService.create_webhook_job(payment: payload[:payment], event:, at: started)
end
end
end

View File

@@ -0,0 +1,84 @@
# frozen_string_literal: true
module Payments
class WebhookPayload
def initialize(payment:, order:, enterprise:)
@payment = payment
@order = order
@enterprise = enterprise
end
def to_hash
{
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)
}.with_indifferent_access
end
def self.test_data
new(payment: test_payment, order: test_order, enterprise: test_enterprise)
end
def self.test_payment
{
updated_at: Time.zone.now,
amount: 0.00,
state: "completed"
}
end
def self.test_order
order = Spree::Order.new(
total: 0.00,
currency: "AUD",
)
tax_category = Spree::TaxCategory.new(name: "VAT")
product = Spree::Product.new(name: "Test product")
Spree::Variant.new(product:, display_name: "")
order.line_items << Spree::LineItem.new(
quantity: 1,
price: 20.00,
tax_category:,
product:,
unit_presentation: "1kg"
)
order
end
def self.test_enterprise
enterprise = Enterprise.new(
abn: "65797115831",
acn: "",
name: "TEST Enterprise",
)
enterprise.address = Spree::Address.new(
address1: "1 testing street",
address2: "",
city: "TestCity",
zipcode: "1234"
)
enterprise
end
private_class_method :test_payment, :test_order, :test_enterprise
private
def line_items
@order.line_items.map do |li|
li.slice(:quantity, :price)
.merge(
tax_category_name: li.tax_category&.name,
product_name: li.product.name,
name_to_display: li.display_name,
unit_to_display: li.unit_presentation
)
end
end
end
end

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
# Create a webhook payload for a payment status event.
# The payload will be delivered asynchronously.
module Payments
class WebhookService
def self.create_webhook_job(payment:, event:, at:)
order = payment.order
payload = WebhookPayload.new(payment:, order:, enterprise: order.distributor).to_hash
coordinator = payment.order.order_cycle.coordinator
webhook_urls(coordinator).each do |url|
WebhookDeliveryJob.perform_later(url, event, payload, at:)
end
end
def self.webhook_urls(coordinator)
# url for coordinator owner
webhook_urls = coordinator.owner.webhook_endpoints.payment_status.pluck(:url)
# plus url for coordinator manager (ignore duplicate)
users_webhook_urls = coordinator.users.flat_map do |user|
user.webhook_endpoints.payment_status.pluck(:url)
end
webhook_urls | users_webhook_urls
end
end
end

View File

@@ -14,12 +14,12 @@ module PermittedAttributes
def self.attributes
basic_permitted_attributes + [
group_ids: [], user_ids: [],
shipping_method_ids: [], payment_method_ids: [],
address_attributes: PermittedAttributes::Address.attributes,
business_address_attributes: PermittedAttributes::BusinessAddress.attributes,
producer_properties_attributes: [:id, :property_name, :value, :_destroy],
custom_tab_attributes: PermittedAttributes::CustomTab.attributes,
{ group_ids: [], user_ids: [],
shipping_method_ids: [], payment_method_ids: [],
address_attributes: PermittedAttributes::Address.attributes,
business_address_attributes: PermittedAttributes::BusinessAddress.attributes,
producer_properties_attributes: [:id, :property_name, :value, :_destroy],
custom_tab_attributes: PermittedAttributes::CustomTab.attributes },
]
end

View File

@@ -25,8 +25,8 @@ module PermittedAttributes
private
def attributes
self.class.basic_attributes + [incoming_exchanges: permitted_exchange_attributes,
outgoing_exchanges: permitted_exchange_attributes]
self.class.basic_attributes + [{ incoming_exchanges: permitted_exchange_attributes,
outgoing_exchanges: permitted_exchange_attributes }]
end
def permitted_exchange_attributes

View File

@@ -11,7 +11,7 @@ module PermittedAttributes
[:name, :description, :type, :active,
:environment, :display_on, :tag_list,
:preferred_enterprise_id, :preferred_server, :preferred_login, :preferred_password,
:calculator_type, :preferred_api_key,
:calculator_type, :preferred_api_key, :preferred_backend_url,
:preferred_signature, :preferred_solution, :preferred_landing_page, :preferred_logourl,
:preferred_test_mode, :calculator_type, { distributor_ids: [] },
{ calculator_attributes: PermittedAttributes::Calculator.attributes }]

View File

@@ -26,11 +26,11 @@ module PermittedAttributes
def other_permitted_attributes
[
subscription_line_items_attributes: [
:id, :quantity, :variant_id, :price_estimate, :_destroy
],
bill_address_attributes: PermittedAttributes::Address.attributes,
ship_address_attributes: PermittedAttributes::Address.attributes
{ subscription_line_items_attributes: [
:id, :quantity, :variant_id, :price_estimate, :_destroy
],
bill_address_attributes: PermittedAttributes::Address.attributes,
ship_address_attributes: PermittedAttributes::Address.attributes }
]
end
end

View File

@@ -2,6 +2,11 @@
module Vine
class VoucherValidatorService
VINE_ERRORS = {
# https://github.com/openfoodfoundation/vine/blob/main/app/Enums/ApiResponse.php
"This voucher has expired." => :expired,
}.freeze
attr_reader :voucher_code, :errors
def initialize(voucher_code:, enterprise:)
@@ -42,8 +47,10 @@ module Vine
end
def handle_errors(response)
if response[:status] == 400
errors[:invalid_voucher] = I18n.t("vine_voucher_validator_service.errors.invalid_voucher")
if [400, 409].include?(response[:status])
message = response[:body] && JSON.parse(response[:body]).dig("meta", "message")
key = VINE_ERRORS.fetch(message, :invalid_voucher)
errors[:invalid_voucher] = I18n.t("vine_voucher_validator_service.errors.#{key}")
elsif response[:status] == 404
errors[:not_found_voucher] =
I18n.t("vine_voucher_validator_service.errors.not_found_voucher")

View File

@@ -18,7 +18,7 @@
%span{ "ofn-with-tip": '{{ orderCycle.producerNames }}', "ng-show": 'orderCycle.producers.length > 3' }
{{ orderCycle.producers.length }}
= t('.suppliers')
%span{ "ng-hide": 'orderCycle.producers.length > 3', "ng-bind": 'orderCycle.producerNames' }
%span{ "ng-hide": 'orderCycle.producers.length > 3', "ng-bind-html": 'orderCycle.producerNames' }
%td.coordinator{ "ng-show": 'columns.coordinator.visible', "ng-bind-html": 'orderCycle.coordinator.name' }
%td.shops{ "ng-show": 'columns.shops.visible' }
%span{ "ofn-with-tip": '{{ orderCycle.shopNames }}', "ng-show": 'orderCycle.shops.length > 3' }

View File

@@ -23,7 +23,7 @@
- select_tag_options = { class: "fullwidth",
multiple: true ,
data: { controller: "tom-select", "tom-select-placeholder-value": t(".select_tag"), "tom-select-options-value": '{ "maxItems": 5 , "plugins": { "remove_button": {} , "no_active_items": {}, "checkbox_options": { "checkedClassNames": ["ts-checked"], "uncheckedClassNames": ["ts-unchecked"] } } }' } }
= select_tag :tags_name_in, options_for_select(available_tags, tags), select_tag_options
= select_tag :tags_name_in, options_for_select(available_tags.unshift([t('.tags.none'), @none_tag_value]), tags), select_tag_options
.submit
.search-button
= button_tag t(".search"), class: "secondary icon-search relaxed", name: nil

View File

@@ -7,5 +7,6 @@
#no-products-actions
%a{ href: "/admin/products/new", class: "button icon-plus", icon: "icon-plus" }
= t(:new_product)
%a{ href: "/admin/products/import", class: "button icon-upload secondary", icon: "icon-upload" }
%a{ href: admin_product_import_path, class: "button icon-upload secondary", icon: "icon-upload" }
= t(".import_products")

View File

@@ -13,6 +13,8 @@
= hidden_field_tag :search_term, @search_term
= hidden_field_tag :producer_id, @producer_id
= hidden_field_tag :category_id, @category_id
- @tags.each do |tag|
= hidden_field_tag 'tags_name_in[]', tag
%table.products{ 'data-column-preferences-target': "table", class: (hide_producer_column?(producer_options) ? 'hide-producer' : '') }
%colgroup

View File

@@ -9,7 +9,7 @@
%td.col-sku.field.naked_inputs
= f.text_field :sku, 'aria-label': t('admin.products_page.columns.sku')
= error_message_on variant, :sku
%td.col-unir_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
%td.col-unit_scale.field.naked_inputs{ 'data-controller': 'toggle-control', 'data-toggle-control-match-value': 'items' }
= f.hidden_field :variant_unit
= f.hidden_field :variant_unit_scale
= f.select :variant_unit_with_scale,

View File

@@ -1,23 +1,50 @@
- search_url_query = {report_type: :enterprise_fee_summary, report_subtype: :enterprise_fees_with_tax_report_by_order}
.row
.alpha.two.columns= label_tag nil, t(:report_hubs)
.omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :distributor_id_in,
options: [],
selected_option: params.dig(:q, :distributor_id_in),
multiple: true,
remote_url: admin_reports_search_distributors_url))
.row
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
.omega.fourteen.columns
= f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true})
= render(SearchableDropdownComponent.new(form: f,
name: :order_cycle_id_in,
options: [],
selected_option: params.dig(:q, :order_cycle_id_in),
multiple: true,
remote_url: admin_reports_search_order_cycles_url))
.row
.alpha.two.columns= label_tag nil, t(:fee_name)
.omega.fourteen.columns
= f.select(:enterprise_fee_id_in, fee_name_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_id_in)}, {class: "select2 fullwidth", multiple: true})
= render(SearchableDropdownComponent.new(form: f,
name: :enterprise_fee_id_in,
options: [],
selected_option: params.dig(:q, :enterprise_fee_id_in),
multiple: true,
remote_url: admin_reports_search_enterprise_fees_url(search_url_query)))
.row
.alpha.two.columns= label_tag nil, t(:fee_owner)
.omega.fourteen.columns
= f.select(:enterprise_fee_owner_id_in, fee_owner_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_owner_id_in)}, {class: "select2 fullwidth", multiple: true})
= render(SearchableDropdownComponent.new(form: f,
name: :enterprise_fee_owner_id_in,
options: [],
selected_option: params.dig(:q, :enterprise_fee_owner_id_in),
multiple: true,
remote_url: admin_reports_search_enterprise_fee_owners_url(search_url_query)))
.row
.alpha.two.columns= label_tag nil, t(:report_customers)
.omega.fourteen.columns
= f.select(:customer_id_in, customer_email_options(@data.order_customers), {selected: params.dig(:q, :customer_id_in)}, {class: "select2 fullwidth", multiple: true})
= render(SearchableDropdownComponent.new(form: f,
name: :customer_id_in,
options: [],
selected_option: params.dig(:q, :customer_id_in),
multiple: true,
remote_url: admin_reports_search_order_customers_url))

View File

@@ -1,27 +1,57 @@
- search_url_query = {report_type: :enterprise_fee_summary, report_subtype: :enterprise_fees_with_tax_report_by_producer}
.row
.alpha.two.columns= label_tag nil, t(:report_hubs)
.omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true})
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(form: f,
name: :distributor_id_in,
options: [],
selected_option: params.dig(:q, :distributor_id_in),
multiple: true,
remote_url: admin_reports_search_distributors_url))
.row
.alpha.two.columns= label_tag nil, t(:report_producers)
.omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@data.orders_suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true})
.omega.fourteen.columns
= render(SearchableDropdownComponent.new(name: :supplier_id_in,
options: [],
selected_option: params.dig(:supplier_id_in),
multiple: true,
remote_url: admin_reports_search_suppliers_url))
.row
.alpha.two.columns= label_tag nil, t(:report_customers_cycle)
.omega.fourteen.columns
= f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true})
= render(SearchableDropdownComponent.new(form: f,
name: :order_cycle_id_in,
options: [],
selected_option: params.dig(:q, :order_cycle_id_in),
multiple: true,
remote_url: admin_reports_search_order_cycles_url))
.row
.alpha.two.columns= label_tag nil, t(:fee_name)
.omega.fourteen.columns
= f.select(:enterprise_fee_id_in, fee_name_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_id_in)}, {class: "select2 fullwidth", multiple: true})
= render(SearchableDropdownComponent.new(form: f,
name: :enterprise_fee_id_in,
options: [],
selected_option: params.dig(:q, :enterprise_fee_id_in),
multiple: true,
remote_url: admin_reports_search_enterprise_fees_url(search_url_query)))
.row
.alpha.two.columns= label_tag nil, t(:fee_owner)
.omega.fourteen.columns
= f.select(:enterprise_fee_owner_id_in, fee_owner_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_owner_id_in)}, {class: "select2 fullwidth", multiple: true})
= render(SearchableDropdownComponent.new(form: f,
name: :enterprise_fee_owner_id_in,
options: [],
selected_option: params.dig(:q, :enterprise_fee_owner_id_in),
multiple: true,
remote_url: admin_reports_search_enterprise_fee_owners_url(search_url_query)))
.row
.alpha.two.columns= label_tag nil, t(:report_customers)
.omega.fourteen.columns
= f.select(:customer_id_in, customer_email_options(@data.order_customers), {selected: params.dig(:q, :customer_id_in)}, {class: "select2 fullwidth", multiple: true})
= render(SearchableDropdownComponent.new(form: f,
name: :customer_id_in,
options: [],
selected_option: params.dig(:q, :customer_id_in),
multiple: true,
remote_url: admin_reports_search_order_customers_url))

View File

@@ -6,21 +6,21 @@
%p
= t :brandstory_intro
#brand-story-text.hide-show.slideable
%p
= t :brandstory_part1
%p
= t :brandstory_part2
%p
= t :brandstory_part3
%p
= t :brandstory_part4
%p
%strong
= t :brandstory_part5_strong
%p
= t :brandstory_part6
%a.text-vbig{"slide-toggle" => "#brand-story-text", "ng-click" => "toggleBrandStory()"}
%i.ofn-i_005-caret-down{"ng-hide" => "brandStoryExpanded"}
%i.ofn-i_006-caret-up{ "ng-show" => "brandStoryExpanded"}
%details#brand-story-text
%summary
%i.ofn-i_005-caret-down
%i.ofn-i_006-caret-up
.brand-story-content
%p
= t :brandstory_part1
%p
= t :brandstory_part2
%p
= t :brandstory_part3
%p
= t :brandstory_part4
%p
%strong
= t :brandstory_part5_strong
%p
= t :brandstory_part6

View File

@@ -5,7 +5,7 @@
- content_for :page_alert do
= render "shared/menu/alert"
%div{"ng-controller" => "HomeCtrl"}
%div
= render "home/tagline"
#panes

View File

@@ -1,5 +1,5 @@
.alert-box{ class: "#{type}" }
= message
- if local_assigns[:unconfirmed]
%a{ "data-action": "login-modal#resend_confirmation", "data-tab": local_assigns[:tab] }
= link_to spree_user_confirmation_path(spree_user: { email: }, tab: local_assigns[:tab]), data: { turbo_method: :post } do
= t('devise.confirmations.resend_confirmation_email')

View File

@@ -1,12 +1,12 @@
#forgot-tab
= form_with url: spree_user_password_path, scope: :spree_user, data: { remote: "true" } do |form|
= form_with url: spree_user_password_path, scope: :spree_user, data: { turbo: true } do |form|
.row
.large-12.columns#forgot-feedback
.row
.large-12.columns
= form.label :email, t(:signup_email)
= form.email_field :email, { tabindex: 1, inputmode: "email", "data-login-modal-target": "email", "data-action": "input->login-modal#emailOnInput" }
= form.email_field :email, { tabindex: 1, inputmode: "email" }
.row
.large-12.columns
= form.submit t(:reset_password), { class: "button primary", tabindex: 2 }

View File

@@ -1,5 +1,5 @@
#login-content
= form_with url: spree_user_session_path, scope: :spree_user, data: { remote: "true" } do |form|
= form_with url: spree_user_session_path, scope: :spree_user, data: { turbo: true } do |form|
.row
.large-12.columns#login-feedback
- confirmation_result = request.query_parameters[:validation]
@@ -10,7 +10,7 @@
.row
.large-12.columns
= form.label :email, t(:email)
= form.email_field :email, { tabindex: 1, inputmode: "email", autocomplete: "off", "data-login-modal-target": "email", "data-action": "input->login-modal#emailOnInput" }
= form.email_field :email, { tabindex: 1, inputmode: "email", autocomplete: "off" }
.row
.large-12.columns
= form.label :password, t(:password)

View File

@@ -1,14 +1,14 @@
- signup_form_user = Spree::User.new if local_assigns[:signup_form_user].nil?
#signup-tab
= form_with model: signup_form_user, url: spree.account_path, scope: :user, data: { remote: "true" } do |form|
= form_with model: signup_form_user, url: spree.account_path, scope: :user, data: { turbo: true } do |form|
.row
.large-12.columns#signup-feedback
.row
.large-12.columns
= form.label :email, t(:signup_email)
= form.email_field :email, { tabindex: 1, "data-login-modal-target": "email", "data-action": "input->login-modal#emailOnInput" }
= form.email_field :email, { tabindex: 1 }
= form.error_message_on :email
.row
.large-12.columns

View File

@@ -13,12 +13,12 @@
- else
= favicon_link_tag "/favicon-staging.ico"
%link{href: "https://fonts.googleapis.com/css?family=Roboto:400,300italic,400italic,300,700,700italic|Oswald:300,400,700", rel: "stylesheet", type: "text/css"}
%link{href: asset_pack_path("media/fonts/OFN-v2.woff"), rel: "preload", as: "font", crossorigin: "anonymous"}
%link{href: asset_pack_path("static/OFN-v2.woff"), rel: "preload", as: "font", crossorigin: "anonymous"}
= render "layouts/matomo_tag"
= language_meta_tags
= stylesheet_pack_tag "darkswarm", "data-turbo-track": "reload", media: "screen"
= javascript_pack_tag "application", "data-turbo-track": "reload"
= javascript_pack_tag "application", "data-turbo-track": "reload", defer: false # do not use defer because our javascript currently depend on order of execution of loaded script.
= render "layouts/shopfront_script" if @shopfront_layout
= render "layouts/bugsnag_js"

View File

@@ -5,7 +5,7 @@
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%title
= Spree::Config[:site_name]
= stylesheet_link_tag 'mail', media: "screen"
= stylesheet_pack_tag "mail", media: "screen"
%body{:bgcolor => "#FFFFFF" }
- unless @hide_ofn_navigation
%table.head-wrap

View File

@@ -16,7 +16,7 @@
= stylesheet_pack_tag "darkswarm", media: "screen"
= javascript_include_tag "darkswarm/all"
= javascript_pack_tag "application"
= javascript_pack_tag "application", defer: false # do not use defer because our javascript currently depend on order of execution of loaded script.
= csrf_meta_tags

View File

@@ -1,2 +1,2 @@
= t('spree.payment_mailer.authorization_required.message', order_number: @order.number)
= t(".message", order_number: @order.number)
= link_to spree.edit_admin_order_url(@order), spree.edit_admin_order_url(@order)

View File

@@ -1,2 +1,2 @@
= t('spree.payment_mailer.authorize_payment.instructions', distributor: @payment.order.distributor.name, amount: @payment.display_amount)
= t(".instructions", distributor: @payment.order.distributor.name, amount: @payment.display_amount)
= link_to main_app.authorize_payment_url(@payment), main_app.authorize_payment_url(@payment)

Some files were not shown because too many files have changed in this diff Show More