Compare commits

...

48 Commits

Author SHA1 Message Date
dependabot[bot]
0d04dad080 Bump bullet from 8.0.8 to 8.1.0
Bumps [bullet](https://github.com/flyerhzm/bullet) from 8.0.8 to 8.1.0.
- [Changelog](https://github.com/flyerhzm/bullet/blob/main/CHANGELOG.md)
- [Commits](https://github.com/flyerhzm/bullet/compare/8.0.8...8.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 05:52:00 +00:00
Maikel
e98cf78b4c Merge pull request #13819 from openfoodfoundation/dependabot/bundler/haml_lint-0.68.0
Bump haml_lint from 0.67.0 to 0.68.0
2026-01-02 16:50:21 +11:00
dependabot[bot]
3173c79e8f Bump haml_lint from 0.67.0 to 0.68.0
Bumps [haml_lint](https://github.com/sds/haml-lint) from 0.67.0 to 0.68.0.
- [Release notes](https://github.com/sds/haml-lint/releases)
- [Changelog](https://github.com/sds/haml-lint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sds/haml-lint/compare/v0.67.0...v0.68.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 09:04:24 +00:00
David Cook
ca14d557c1 Merge pull request #13814 from mkllnk/dfc-events-unauthorised
Correctly respond to unauthorised requests on DFC events endpoint
2025-12-22 10:42:23 +11:00
Gaetan Craig-Riou
59a3a5bd92 Merge pull request #13816 from openfoodfoundation/dependabot/bundler/dotenv-3.2.0
Bump dotenv from 3.1.8 to 3.2.0
2025-12-22 09:44:15 +11:00
dependabot[bot]
a226088f5c Bump dotenv from 3.1.8 to 3.2.0
Bumps [dotenv](https://github.com/bkeepers/dotenv) from 3.1.8 to 3.2.0.
- [Release notes](https://github.com/bkeepers/dotenv/releases)
- [Changelog](https://github.com/bkeepers/dotenv/blob/main/Changelog.md)
- [Commits](https://github.com/bkeepers/dotenv/compare/v3.1.8...v3.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 09:04:48 +00:00
Maikel
c65fcc1072 Merge pull request #13812 from openfoodfoundation/dependabot/bundler/aws-sdk-s3-1.208.0
Bump aws-sdk-s3 from 1.207.0 to 1.208.0
2025-12-19 10:41:27 +11:00
Maikel
3bb68ec07e Merge pull request #13811 from openfoodfoundation/dependabot/bundler/webmock-3.26.1
Bump webmock from 3.25.1 to 3.26.1
2025-12-19 10:39:45 +11:00
Maikel Linke
2c97638aa1 Enhance readability 2025-12-19 10:21:02 +11:00
Maikel Linke
ceee9671d9 Replace method with name conflict causing error 2025-12-19 10:16:46 +11:00
dependabot[bot]
6b494be7ff Bump aws-sdk-s3 from 1.207.0 to 1.208.0
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.207.0 to 1.208.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-version: 1.208.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-18 19:38:08 +00:00
dependabot[bot]
a6855e6bc1 Bump webmock from 3.25.1 to 3.26.1
Bumps [webmock](https://github.com/bblimke/webmock) from 3.25.1 to 3.26.1.
- [Release notes](https://github.com/bblimke/webmock/releases)
- [Changelog](https://github.com/bblimke/webmock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bblimke/webmock/compare/v3.25.1...v3.26.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-17 09:01:49 +00:00
Gaetan Craig-Riou
7ca43eb4a1 Merge pull request #13810 from openfoodfoundation/dependabot/bundler/aws-sdk-s3-1.207.0
Bump aws-sdk-s3 from 1.206.0 to 1.207.0
2025-12-17 10:21:50 +11:00
dependabot[bot]
74b5ac559f Bump aws-sdk-s3 from 1.206.0 to 1.207.0
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.206.0 to 1.207.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 09:02:51 +00:00
Maikel
07c236497c Merge pull request #13808 from openfoodfoundation/dependabot/bundler/vcr-6.3.1
Bump vcr from 6.2.0 to 6.3.1
2025-12-16 14:50:27 +11:00
Filipe
caf2ff9bb4 Merge pull request #13752 from deivid-rodriguez/always_generate_button_tags
Always generate `<button>` tags, rather than `<input>` of type "button"
2025-12-15 18:46:38 +00:00
Filipe
1b2a17d7e4 Merge pull request #13754 from deivid-rodriguez/no-changes-after-delete-customer
Properly handle changes in `code` attribute when a customer is deleted
2025-12-15 17:58:04 +00:00
Filipe
ce46115139 Merge pull request #13648 from deivid-rodriguez/improve-enterprise-removal
Improve enterprise removal
2025-12-15 16:54:05 +00:00
dependabot[bot]
9fd2ff7620 Bump vcr from 6.2.0 to 6.3.1
Bumps [vcr](https://github.com/vcr/vcr) from 6.2.0 to 6.3.1.
- [Release notes](https://github.com/vcr/vcr/releases)
- [Changelog](https://github.com/vcr/vcr/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vcr/vcr/compare/v6.2.0...v6.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 09:02:43 +00:00
Gaetan Craig-Riou
98a25c1c7f Merge pull request #13805 from openfoodfoundation/dependabot/bundler/newrelic_rpm-9.24.0
Bump newrelic_rpm from 9.23.0 to 9.24.0
2025-12-15 09:26:49 +11:00
Ahmed Ejaz
6b78f8b855 Merge pull request #13804 from rioug/13802-revert-enbling-variant-tag-no-inventory
[Inventory] display inventory link for user who manage enterprises with inventory and enterprises without inventory
2025-12-13 01:13:34 +05:00
Ahmed Ejaz
1e2b28c559 Update all locales with the latest Transifex translations 2025-12-13 01:05:55 +05:00
dependabot[bot]
12b86a35af Bump newrelic_rpm from 9.23.0 to 9.24.0
Bumps [newrelic_rpm](https://github.com/newrelic/newrelic-ruby-agent) from 9.23.0 to 9.24.0.
- [Release notes](https://github.com/newrelic/newrelic-ruby-agent/releases)
- [Changelog](https://github.com/newrelic/newrelic-ruby-agent/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/newrelic/newrelic-ruby-agent/compare/9.23.0...9.24.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 09:02:49 +00:00
Maikel
4577bde692 Merge pull request #13772 from mkllnk/dfc-webhook
Import farm data from LiteFarm
2025-12-12 16:59:30 +11:00
Maikel Linke
af6be02ba4 Fix social media import 2025-12-12 16:46:43 +11:00
Maikel Linke
0dabca583f Set stronger secret password for managers
And avoid depending on Devise for this.
2025-12-12 15:17:57 +11:00
Maikel Linke
d7603755bf Mark Litefarm farms as primary producers 2025-12-12 14:46:11 +11:00
Maikel Linke
f9d255a266 Continue on fail of Litefarm import
And report validation errors.
2025-12-12 14:03:37 +11:00
Gaetan Craig-Riou
bcf4507795 Only show hub with inventory enabled 2025-12-12 13:04:44 +11:00
Gaetan Craig-Riou
9967ba2d06 Allow managing inventory and variant tag at the same time
For users with enterprise with inventory and enterpises with variant
tag, allow access to the inventory page and display variant tag only for
the enterprises its enabled for.
2025-12-12 12:38:22 +11:00
Maikel Linke
f90f71cf68 Update real Litefarm data 2025-12-10 17:18:17 +11:00
Maikel Linke
fe8a0a908e Import DFC country by name or ISO code 2025-12-10 17:17:51 +11:00
Maikel Linke
bf6176c883 Test failed image import 2025-12-10 16:24:54 +11:00
Maikel Linke
ffdfb7d450 Doc: explain when OIDC secrets are required 2025-12-10 16:24:54 +11:00
Maikel Linke
3aa4c2a25f Import more fields from Litefarm 2025-12-10 16:24:54 +11:00
Maikel Linke
3331aaa382 Fetch data from URL provided by Litefarm
So we don't have to distinguish between staging and production. They
will provide the right URL.
2025-12-10 16:24:54 +11:00
Maikel Linke
b302dcfbec Update existing enterprises 2025-12-10 16:24:54 +11:00
Maikel Linke
7dfc4d21ca Record updated Litefarm data 2025-12-10 16:24:53 +11:00
Maikel Linke
f332a6934b Move growing enterprise creation to own class 2025-12-10 16:24:53 +11:00
Maikel Linke
baad0135f9 Import enterprises and owners with minimal data
Still missing:

* Check for existing enterprises.
* Import all the available data.
2025-12-10 16:24:53 +11:00
Maikel Linke
1973e36634 Extract token and HTTP layer for re-use
Calling a webhook as a platform and fetching enterprise data will have
the same auth.
2025-12-10 16:24:53 +11:00
Maikel Linke
2e62531232 Authenticate only as platform to call webhooks 2025-12-10 16:24:53 +11:00
Maikel Linke
d811103a71 Add dummy webhook endpoint for LiteFarm 2025-12-10 16:24:53 +11:00
David Rodríguez
c526e72539 Improve enterprise removal (failure case)
Make sure failure to delete due to dependent objects is handled through
activemodel errors and not by rescuing
`ActiveRecord::DeleteRestrictionError` exceptions.

Previously we would display two alert prompts, and we would weirdly
display the content of our 500 error page on top of the screen.

Now, we display a flash error message explaining the reason to fail to
remove it.
2025-11-27 19:10:15 +01:00
David Rodríguez
e217a6fca8 Make enterprise unit specs about removal consistent
And not dependent on implementation details.
2025-11-27 19:09:45 +01:00
David Rodríguez
6aa7ef3c21 Improve enterprise removal (success case)
Make enterprise removal use turbo, which provides the following
benefits:

* More responsive removal since there's no full page reload.
* A success flash message (previously nothing was displayed).
* No double alert prompt.

It also goes in the direction of removing mrujs in favor of turbo.
2025-11-27 19:09:35 +01:00
David Rodríguez
dc631026d4 Properly handle changes in code attribute when a customer is deleted
Previously, `null` and empty value would be confused when a customer is
removed, resulting in incorrect pending changes being added, and thus a
"You have unsaved changes" message getting displayed and the save button
not getting disabled.
2025-11-25 07:44:00 +01:00
David Rodríguez
c05532c166 Always generate <button> tags, rather than <input> of type "button" 2025-11-24 12:11:03 +01:00
43 changed files with 1042 additions and 164 deletions

View File

@@ -158,8 +158,8 @@ GEM
zeitwerk (>= 2.4, < 3.0)
acts_as_list (1.0.4)
activerecord (>= 4.2)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
aes_key_wrap (1.1.0)
afm (0.2.2)
angular-rails-templates (1.4.0)
@@ -176,8 +176,8 @@ GEM
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.4.0)
aws-partitions (1.1191.0)
aws-sdk-core (3.239.2)
aws-partitions (1.1196.0)
aws-sdk-core (3.240.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -188,7 +188,7 @@ GEM
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.206.0)
aws-sdk-s3 (1.208.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
@@ -206,7 +206,7 @@ GEM
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)
@@ -248,10 +248,10 @@ GEM
combine_pdf (1.0.31)
matrix
ruby-rc4 (>= 0.1.5)
concurrent-ruby (1.3.5)
connection_pool (2.5.5)
concurrent-ruby (1.3.6)
connection_pool (3.0.2)
cookiejar (0.3.4)
crack (1.0.0)
crack (1.0.1)
bigdecimal
rexml
crass (1.0.6)
@@ -289,7 +289,7 @@ GEM
diff-lcs (1.6.2)
digest (3.2.1)
docile (1.4.1)
dotenv (3.1.8)
dotenv (3.2.0)
drb (2.2.3)
em-http-request (1.1.7)
addressable (>= 2.3.4)
@@ -379,7 +379,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
@@ -491,7 +491,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)
@@ -511,7 +512,7 @@ GEM
timeout
net-smtp (0.5.1)
net-protocol
newrelic_rpm (9.23.0)
newrelic_rpm (9.24.0)
nio4r (2.7.5)
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
@@ -576,7 +577,7 @@ GEM
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.6.0)
prism (1.7.0)
private_address_check (0.5.0)
pry (0.15.2)
coderay (~> 1.1)
@@ -584,7 +585,7 @@ GEM
psych (5.2.6)
date
stringio
public_suffix (6.0.2)
public_suffix (7.0.0)
puffing-billy (4.0.2)
addressable (~> 2.5)
em-http-request (~> 1.1, >= 1.1.0)
@@ -875,7 +876,7 @@ GEM
faraday (~> 2.0)
faraday-follow_redirects
sysexits (1.2.0)
temple (0.8.2)
temple (0.10.4)
terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4)
thor (1.4.0)
@@ -902,8 +903,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)
@@ -913,7 +914,8 @@ GEM
public_suffix
validates_lengths_from_database (0.8.0)
activerecord (>= 4)
vcr (6.2.0)
vcr (6.3.1)
base64
view_component (4.1.1)
actionview (>= 7.1.0, < 8.2)
activesupport (>= 7.1.0, < 8.2)
@@ -935,7 +937,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.25.1)
webmock (3.26.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)

View File

@@ -38,16 +38,13 @@ angular.module("admin.indexUtils").directive "objForUpdate", (switchClass, pendi
# To ensure the customer is still updated, we check on the $destroy event to see if
# the attribute has changed, if so we queue up the change.
scope.$on '$destroy', (value) ->
# No update
return if scope.object()[scope.attr] is scope.savedValue
currentValue = scope.object()[scope.attr] || ""
# For some reason the code attribute is removed from the object when cleared, so we add
# an emptyvalue so it gets updated properly
if scope.attr is "code" and scope.object()[scope.attr] is undefined
scope.object()["code"] = ""
# No update
return if currentValue is scope.savedValue
# Queuing up change
addPendingChange(scope.attr, scope.object()[scope.attr])
addPendingChange(scope.attr, currentValue)
# private

View File

@@ -15,8 +15,8 @@ module Admin
def index
# Fetch DFC catalog JSON for preview
@catalog_url = params.require(:catalog_url).strip
@catalog_json = api.call(@catalog_url)
catalog = DfcCatalog.from_json(@catalog_json)
@catalog_data = api.call(@catalog_url)
catalog = DfcCatalog.from_json(@catalog_data)
# Render table and let user decide which ones to import.
@items = list_products(catalog)

View File

@@ -162,6 +162,18 @@ module Admin
end
end
def destroy
if @object.destroy
flash.now[:success] = flash_message_for(@object, :successfully_removed)
else
flash.now[:error] = @object.errors.full_messages.to_sentence
end
respond_to do |format|
format.turbo_stream { render :destroy, status: :ok }
end
end
protected
def delete_custom_tab

View File

@@ -44,6 +44,8 @@ module Admin
def load_data
@hubs = OpenFoodNetwork::Permissions.new(spree_current_user).
variant_override_hubs.by_name
# Only display the ones with inventory enabled
@hubs = @hubs.select { |p| helpers.feature?(:inventory, p) }
# Used in JS to look up the name of the producer of each product
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).

View File

@@ -4,7 +4,7 @@ module ManagerInvitations
extend ActiveSupport::Concern
def create_new_manager(email, enterprise)
password = Devise.friendly_token
password = SecureRandom.base58(64)
new_user = Spree::User.create(email:, unconfirmed_email: email, password:)
new_user.reset_password_token = Devise.friendly_token
# Same time as used in Devise's lib/devise/models/recoverable.rb.

View File

@@ -63,8 +63,11 @@ module EnterprisesHelper
url = object_url(enterprise)
name = t(:delete)
options = {}
options[:class] = "delete-resource"
options[:data] = { action: 'remove', confirm: enterprise_confirm_delete_message(enterprise) }
options[:data] = {
turbo: true,
'turbo-method': 'delete',
'turbo-confirm': enterprise_confirm_delete_message(enterprise)
}
link_to_with_icon 'icon-trash', name, url, options
end

View File

@@ -50,11 +50,11 @@ class Enterprise < ApplicationRecord
has_many :distributed_orders, class_name: 'Spree::Order',
foreign_key: 'distributor_id',
inverse_of: :distributor,
dependent: :restrict_with_exception
dependent: :restrict_with_error
belongs_to :address, class_name: 'Spree::Address'
belongs_to :business_address, optional: true, class_name: 'Spree::Address', dependent: :destroy
has_many :enterprise_fees, dependent: :restrict_with_exception
has_many :enterprise_fees, dependent: :restrict_with_error
has_many :enterprise_roles, dependent: :destroy
has_many :users, through: :enterprise_roles
belongs_to :owner, class_name: 'Spree::User',
@@ -62,21 +62,22 @@ class Enterprise < ApplicationRecord
has_many :distributor_payment_methods,
inverse_of: :distributor,
foreign_key: :distributor_id,
dependent: :restrict_with_exception
dependent: :restrict_with_error
has_many :distributor_shipping_methods,
inverse_of: :distributor,
foreign_key: :distributor_id,
dependent: :restrict_with_exception
dependent: :restrict_with_error
has_many :payment_methods, through: :distributor_payment_methods
has_many :shipping_methods, through: :distributor_shipping_methods
has_many :customers, dependent: :destroy
has_many :inventory_items, dependent: :destroy
has_many :tag_rules, dependent: :destroy
has_one :stripe_account, dependent: :destroy
has_many :vouchers, dependent: :restrict_with_exception
has_many :vouchers, dependent: :restrict_with_error
has_many :connected_apps, dependent: :destroy
has_many :dfc_permissions, dependent: :destroy
has_one :custom_tab, dependent: :destroy
has_one :semantic_link, as: :subject, dependent: :delete
delegate :latitude, :longitude, :city, :state_name, to: :address

View File

@@ -185,16 +185,11 @@ module ProductImport
order('is_primary_producer ASC, name').
map { |e| @editable_enterprises[e.name] = e.id }
return unless inventory_enabled?
return unless OpenFoodNetwork::FeatureToggle.enabled?(:inventory, *@current_user.enterprises)
@inventory_permissions = permissions.variant_override_enterprises_per_hub
end
def inventory_enabled?
!OpenFoodNetwork::FeatureToggle.enabled?(:variant_tag, *@current_user.enterprises) &&
OpenFoodNetwork::FeatureToggle.enabled?(:inventory, *@current_user.enterprises)
end
def open_spreadsheet
if accepted_mimetype
Roo::Spreadsheet.open(@file, extension: accepted_mimetype, encoding: Encoding::UTF_8)

View File

@@ -12,7 +12,7 @@
= form_with url: main_app.import_admin_dfc_product_imports_path, html: { "data-controller": "checked" } do |form|
-# This is a very inefficient way of holding a json blob. Maybe base64 encode or store as a temporary file
= form.hidden_field :enterprise_id, value: @enterprise.id
= form.hidden_field :catalog_json, value: @catalog_json
= form.hidden_field :catalog_json, value: @catalog_data.to_json
%table
%thead

View File

@@ -22,7 +22,7 @@
%tbody
= f.fields_for :collection do |enterprise_form|
- enterprise = enterprise_form.object
%tr{class: "enterprise-#{enterprise.id}"}
%tr{class: "enterprise-#{enterprise.id}", id: "resource-#{enterprise.id}"}
%td= link_to enterprise.name, main_app.edit_admin_enterprise_path(enterprise)
%td
= enterprise_form.check_box :is_primary_producer

View File

@@ -0,0 +1,4 @@
- unless flash[:error]
= turbo_stream.remove "resource-#{@object.id}"
= turbo_stream.append "flashes" do
= render(partial: 'admin/shared/flashes', locals: { flashes: flash })

View File

@@ -5,7 +5,7 @@
%h6= t('admin.product_import.index.choose_import_type')
%br
- options = { "#{t('admin.product_import.index.product_list')}" => :product_list }
- options = options.merge("#{t('admin.product_import.index.inventories')}" => :inventories) if inventory_enabled?(spree_current_user.enterprises)
- options = options.merge("#{t('admin.product_import.index.inventories')}" => :inventories) if feature?(:inventory, *spree_current_user.enterprises)
= select_tag "settings[import_into]",
options_for_select(options),
{ "data-controller": "tom-select", class: "primary inline no-search", "ng-model": "settings.import_into" }

View File

@@ -5,7 +5,7 @@
%i.icon-external-link
= t('admin.product_import.index.product_list_template')
- if inventory_enabled?(spree_current_user.enterprises)
- if feature?(:inventory, *spree_current_user.enterprises)
%a.download{href: '/inventory_template.csv'}
%i.icon-external-link
= t('admin.product_import.index.inventory_template')

View File

@@ -81,7 +81,7 @@
= error_message_on variant, :tax_category
- if variant_tag_enabled?(spree_current_user)
%td.col-tags.field.naked_inputs
= render TagListInputComponent.new(name: f.field_name(:tag_list), tags: variant.tag_list, autocomplete_url: variant_tag_rules_admin_tag_rules_path(enterprise_id: variant.supplier_id), placeholder: t('.add_a_tag'), aria_label: t('admin.products_page.columns.tags'))
= render TagListInputComponent.new(name: f.field_name(:tag_list), tags: variant.tag_list, autocomplete_url: variant_tag_rules_admin_tag_rules_path(enterprise_id: variant.supplier_id), placeholder: t('.add_a_tag'), aria_label: t('admin.products_page.columns.tags')) if feature?(:variant_tag, variant.supplier)
%td.col-inherits_properties.align-left
-# empty
%td.align-right

View File

@@ -2,5 +2,5 @@
%ul#sub_nav.inline-menu
= tab :products, :products_v3, url: admin_products_path
= tab :properties
= tab :variant_overrides, url: main_app.admin_inventory_path, match_path: '/inventory' if inventory_enabled?(spree_current_user.enterprises)
= tab :variant_overrides, url: main_app.admin_inventory_path, match_path: '/inventory' if feature?(:inventory, *spree_current_user.enterprises)
= tab :import, url: main_app.admin_product_import_path, match_path: '/product_import'

View File

@@ -44,7 +44,7 @@ Flipper.register(:enterprise_with_no_inventory) do |actor|
# This group applies to enterprises only, so we return false if the actor is not an Enterprise
next false unless actor.actor.instance_of? Enterprise
# Uses 2025-08-11 as filter because variant tag did not exist before that, enterprise created
# Uses 2025-08-11 as filter because variant tag did not exist before that, enterprise created
# after never had access to the inventory
enterprise_with_variant_override = Enterprise
.where(id: VariantOverride.joins(:hub).select(:hub_id))
@@ -60,17 +60,17 @@ Flipper.register(:enterprise_with_inventory) do |actor|
# This group applies to enterprises only, so we return false if the actor is not an Enterprise
next false unless actor.actor.instance_of? Enterprise
# Uses 2025-08-11 as filter because variant tag did not exist before that, enterprise created
# Uses 2025-08-11 as filter because variant tag did not exist before that, enterprise created
# after never had access to the inventory
enterprise_with_variant_override = Enterprise
.where(id: VariantOverride.joins(:hub).select(:hub_id))
.where(created_at: ..."2025-08-11")
.distinct
enterprise_with_variant_override.exists?(actor.id)
# Entperprise with inventory and with variant tag not manually enabled.
enterprise_with_variant_override.exists?(actor.id) && !Flipper.enabled?(:variant_tag, actor)
end
Flipper::UI.configure do |config|
config.descriptions_source = ->(_keys) do
# return has to be hash of {String key => String description}

View File

@@ -11,7 +11,7 @@
# `button_to` view helper will render `<button>` element, regardless of whether
# or not the content is passed as the first argument or as a block.
# Rails.application.config.action_view.button_to_generates_button_tag = true
Rails.application.config.action_view.button_to_generates_button_tag = true
# `stylesheet_link_tag` view helper will not render the media attribute by default.
Rails.application.config.action_view.apply_stylesheet_media_default = false

View File

@@ -487,7 +487,7 @@ de_DE:
create_and_add_another: "Erstellen und weitere hinzufügen"
create: "Neu"
cancel: "Abbrechen"
cancel_order: "Abbrechen"
cancel_order: "Stornieren"
resume: "Fortsetzen"
save: "Speichern"
edit: "Bearbeiten"
@@ -2002,6 +2002,7 @@ de_DE:
invoice_column_price_per_unit_without_taxes: "Einzelpreis (zzgl. Steuern)"
invoice_column_tax_rate: "Steuersatz"
invoice_tax_total: "Umsatzsteuersumme:"
invoice_cancel_and_replace_invoice: "ersetzt Rechnung"
tax_invoice: "RECHNUNG"
tax_total: "davon Steuern (%{rate}):"
invoice_shipping_category_delivery: "Lieferoptionen"
@@ -4088,6 +4089,9 @@ de_DE:
line_item_adjustments: "Anpassungen der Einzelposten"
order_adjustments: "Bestellanpassungen"
order_total: "Bestellung insgesamt"
invoices:
index:
order_has_changed: "Die Bestellung hat sich seit Erstellung der letzten Rechnung geändert. Die hier angezeigte Rechnung ist möglicherweise nicht mehr aktuell."
overview:
enterprises_header:
ofn_with_tip: Unternehmen sind Produzenten und/oder Läden und sind die grundlegende Organisationseinheit innerhalb des Open Food Network.

View File

@@ -35,10 +35,10 @@ es:
fee_type: Tipo de Comisión
spree/order:
payment_state: Estado del pago
shipment_state: Provincia de envío
shipment_state: Estado de envío
completed_at: Completado en
number: Número
state: Provincia
state: Estado
email: E-mail del consumidor
spree/payment:
amount: Cantidad
@@ -907,7 +907,7 @@ es:
products_total_html:
one: "<strong>%{count}producto</strong> encontrado para tus criterios de búsqueda. Mostrando %{from} de %{to}"
many: "<strong> %{count}productos </strong>encontrados para tus criterios de búsqueda. Mostrando %{from} de %{to}"
other: "<strong>%{count}productos</strong> encontrados para tus criterios de búsqueda. Mostrando %{from} de %{to}"
other: "<strong>%{count} productos</strong> encontrados para tus criterios de búsqueda. Mostrando %{from} de %{to}"
per_page:
show: Mostrar
per_page: "%{num} por página"
@@ -3198,6 +3198,7 @@ es:
product_importer_products_save_error: No se guardó ningún producto con éxito
product_import_file_not_found_notice: 'Archivo no encontrado o no se pudo abrir'
product_import_no_data_in_spreadsheet_notice: 'No se encontraron datos en la hoja de cálculo'
product_import_inventory_disable: La importación a inventarios no está disponible.
order_choosing_hub_notice: Tu Grupo se ha seleccionado.
order_cycle_selecting_notice: Se ha seleccionado el ciclo de pedido.
adjustments_tax_rate_error: "^Comprueba que los impuestos para este ajuste es correcta."

View File

@@ -165,6 +165,7 @@ fr_BE:
currency_not_supported: "La carte ne prend pas en charge la devise spécifiée."
do_not_honor: "La carte a été refusée pour une raison inconnue."
do_not_try_again: "La carte a été refusée pour une raison inconnue."
duplicate_transaction: "Une transaction avec le même montant et les mêmes informations de carte de crédit a été soumise très récemment."
fraudulent: "Le paiement a été refusé car Stripe soupçonne une opération frauduleuse."
generic_decline: "La carte a été refusée pour une raison inconnue."
incorrect_pin: "Le code PIN saisi est incorrect. Ce code de refus s'applique uniquement aux paiements effectués avec un lecteur de carte."
@@ -3768,6 +3769,7 @@ fr_BE:
display_currency: "Afficher devise "
choose_currency: "Choisir devise "
mail_method_settings: "Paramètres du moyen mail"
mail_settings_notice_html: "<b>Les modifications apportées ici seront temporaires</b>et peuvent changer à la prochaine mise à jour. <br> Si vous souhaitez réaliser des changements permanents, un administrateur système doit se charger de mettre à jour les informations et provisionner le serveur en utilisant<a href='https://github.com/openfoodfoundation/ofn-install'> ofn-install </a>."
general: "Généralement"
enable_mail_delivery: "Possible de livrer le mail"
send_mails_as: "Envoyer les mails comme "

View File

@@ -526,7 +526,7 @@ hu:
create_and_add_another: "Hozz létre és adj hozzá egy másikat"
create: "Létrehozás"
cancel: "Mégsem"
cancel_order: "Mégsem"
cancel_order: "Törlés"
resume: "Összefoglaló"
save: "Mentés"
edit: "Szerkesztés"

View File

@@ -0,0 +1,71 @@
# frozen_string_literal: true
# Webhook events
module DfcProvider
class EventsController < DfcProvider::ApplicationController
rescue_from JSON::ParserError, with: -> do
head :bad_request
end
# Trigger a webhook event.
#
# The only supported event is a `refresh` event of permissions.
# It means that our permissions to access data on another platform changed.
# We will need to pull the updated data.
def create
return if rendered_errors?
event = JSON.parse(request.body.read)
enterprises_url = event["enterpriseUrlid"]
if enterprises_url.blank?
render status: :bad_request, json: {
success: false,
message: "Missing parameter `enterpriseUrlid`",
}
return
end
importer = DfcImporter.new
importer.import_enterprise_profiles(current_user.id, enterprises_url)
if importer.errors.blank?
render json: { success: true }
else
render json: { success: true, messages: error_messages(importer.errors) }
end
end
private
def rendered_errors?
unless current_user.is_a? ApiUser
render_message(
:unauthorized,
"You need to authenticate as authorised platform (client_id).",
)
return true
end
unless current_user.id == "lf-dev"
render_message(
:unauthorized,
"Your client_id is not authorised on this platform.",
)
return true
end
false
end
def render_message(status, message)
render status:, json: { success: false, message: }
end
def error_messages(errors)
errors.map do |error|
id = error.record.try(:semantic_link)&.semantic_id
"#{id}: #{error.message}"
end
end
end
end

View File

@@ -0,0 +1,39 @@
# frozen_string_literal: true
# Fetch data from another platform and store it locally.
class DfcImporter
attr_reader :errors
def import_enterprise_profiles(platform, enterprises_url)
raise "unsupported platform" if platform != "lf-dev"
api = DfcPlatformRequest.new(platform)
body = api.call(enterprises_url)
graph = DfcIo.import(body).to_a
farms = graph.select { |item| item.semanticType == "dfc-b:Enterprise" }
farms.each { |farm| import_profile(farm) }
end
def import_profile(farm)
owner = find_or_import_user(farm)
enterprise = EnterpriseImporter.new(owner, farm).import
enterprise.save! if enterprise.changed?
enterprise.address.save! if enterprise.address.changed?
rescue ActiveRecord::RecordInvalid => e
Alert.raise(e, farm: DfcIo.export(farm))
@errors ||= []
@errors << e
end
def find_or_import_user(farm)
email = farm.mainContact.emails.first
user = Spree::User.find_by(email:)
return user if user
Spree::User.create!(
email:,
password: SecureRandom.base58(64),
)
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
# Request a JSON document from a DFC API authenticating as platform.
class DfcPlatformRequest
def initialize(platform)
@platform = platform
end
def call(url, data = nil, method: nil)
OidcRequest.new(request_token).call(url, data, method:).body
end
def request_token
connection = Faraday.new(
request: { timeout: 5 },
) do |f|
f.request :url_encoded
f.response :json
f.response :raise_error
end
url = ApiUser.token_endpoint(@platform)
data = {
grant_type: "client_credentials",
client_id: ENV.fetch("OPENID_APP_ID", nil),
client_secret: ENV.fetch("OPENID_APP_SECRET", nil),
scope: "ReadEnterprise",
}
response = connection.post(url, data)
response.body["access_token"]
end
end

View File

@@ -1,8 +1,5 @@
# frozen_string_literal: true
require "private_address_check"
require "private_address_check/tcpsocket_ext"
# Request a JSON document from a DFC API with authentication.
#
# All DFC API interactions are authenticated via OIDC tokens. If the user's
@@ -15,16 +12,15 @@ class DfcRequest
def call(url, data = nil, method: nil)
begin
response = request(url, data, method:)
request = OidcRequest.new(@user.oidc_account.token)
response = request.call(url, data, method:)
rescue Faraday::UnauthorizedError, Faraday::ForbiddenError
raise unless token_stale?
# If access was denied and our token is stale then refresh and retry:
refresh_access_token!
response = request(url, data, method:)
rescue Faraday::ServerError => e
Alert.raise(e, { dfc_request: { data: } })
raise
request = OidcRequest.new(@user.oidc_account.token)
response = request.call(url, data, method:)
end
response.body
@@ -32,41 +28,10 @@ class DfcRequest
private
def request(url, data = nil, method: nil)
only_public_connections do
if method == :put
connection.put(url, data)
elsif data
connection.post(url, data)
else
connection.get(url)
end
end
end
def token_stale?
@user.oidc_account.updated_at < 15.minutes.ago
end
def connection
Faraday.new(
request: { timeout: 30 },
headers: {
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{@user.oidc_account.token}",
}
) do |f|
# Configure Faraday to raise errors on status 4xx and 5xx responses.
f.response :raise_error
end
end
def only_public_connections(&)
return yield if Rails.env.development?
PrivateAddressCheck.only_public_connections(&)
end
def refresh_access_token!
strategy = OmniAuth::Strategies::OpenIDConnect.new(
Rails.application,

View File

@@ -0,0 +1,98 @@
# frozen_string_literal: true
require "private_address_check"
require "private_address_check/tcpsocket_ext"
class EnterpriseImporter
def initialize(owner, dfc_enterprise)
@owner = owner
@dfc_enterprise = dfc_enterprise
end
def import
enterprise = find || new
apply(enterprise)
enterprise
end
def find
semantic_id = @dfc_enterprise.semanticId
@owner.owned_enterprises.includes(:semantic_link)
.find_by(semantic_link: { semantic_id: })
end
def new
@owner.owned_enterprises.new(
address: Spree::Address.new,
semantic_link: SemanticLink.new(semantic_id: @dfc_enterprise.semanticId),
is_primary_producer: true,
visible: "public",
)
end
def apply(enterprise)
address = @dfc_enterprise.localizations.first
country = find_country(address)
enterprise.name = @dfc_enterprise.name
enterprise.address.assign_attributes(
address1: address.street,
city: address.city,
zipcode: address.postalCode,
state: find_state(country, address),
country:,
)
enterprise.email_address = @dfc_enterprise.emails.first
enterprise.description = @dfc_enterprise.description
enterprise.phone = @dfc_enterprise.phoneNumbers.first&.phoneNumber
enterprise.website = @dfc_enterprise.websites.first
apply_social_media(enterprise)
apply_logo(enterprise)
end
def apply_social_media(enterprise)
attributes = {}
@dfc_enterprise.socialMedias.each do |media|
attributes[media.name.downcase] = media.url
end
attributes["twitter"] = attributes.delete("x") if attributes.key?("x")
enterprise_attributes = attributes.slice(*SocialMediaBuilder::NAMES)
enterprise.assign_attributes(enterprise_attributes)
end
def apply_logo(enterprise)
link = @dfc_enterprise.logo
logo = enterprise.logo
return if link.blank?
return if logo.blob && (logo.blob.custom_metadata&.fetch("origin", nil) == link)
url = URI.parse(link)
filename = File.basename(url.path)
metadata = { custom: { origin: link } }
PrivateAddressCheck.only_public_connections do
logo.attach(io: url.open, filename:, metadata:)
end
rescue StandardError
# Any URL parsing or network error shouldn't impact the import
# at all. Maybe we'll add UX for error handling later.
nil
end
def find_country(address)
country = address.country
country = country[:path] if country.is_a?(Hash)
Spree::Country.find_by(iso3: country.to_s[-3..]) ||
Spree::Country.find_by(name: country) ||
Spree::Country.first
end
def find_state(country, address)
country.states.find_by(name: address.region) || country.states.first
end
end

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
require "private_address_check"
require "private_address_check/tcpsocket_ext"
# Request a JSON document with an OIDC token.
class OidcRequest
def initialize(token)
@token = token
end
def call(url, data = nil, method: nil)
request(url, data, method:)
rescue StandardError => e
Alert.raise(e, { dfc_request: { data: } })
raise
end
private
def request(url, data = nil, method: nil)
only_public_connections do
if method == :put
connection.put(url, data)
elsif data
connection.post(url, data)
else
connection.get(url)
end
end
end
def connection
Faraday.new(
request: { timeout: 30 },
headers: {
'Authorization' => "Bearer #{@token}",
}
) do |f|
f.request :json
f.response :json
# Configure Faraday to raise errors on status 4xx and 5xx responses.
f.response :raise_error
end
end
def only_public_connections(&)
return yield if Rails.env.development?
PrivateAddressCheck.only_public_connections(&)
end
end

View File

@@ -1,54 +1,15 @@
# frozen_string_literal: true
require "private_address_check"
require "private_address_check/tcpsocket_ext"
# Call a webhook to notify a data proxy about changes in our data.
class ProxyNotifier
def refresh(platform, enterprise_url)
PrivateAddressCheck.only_public_connections do
notify_proxy(platform, enterprise_url)
end
end
def request_token(platform)
connection = Faraday.new(
request: { timeout: 5 },
) do |f|
f.request :url_encoded
f.response :json
f.response :raise_error
end
url = ApiUser.token_endpoint(platform)
data = {
grant_type: "client_credentials",
client_id: ENV.fetch("OPENID_APP_ID", nil),
client_secret: ENV.fetch("OPENID_APP_SECRET", nil),
scope: "ReadEnterprise",
}
response = connection.post(url, data)
response.body["access_token"]
end
def notify_proxy(platform, enterprise_url)
token = request_token(platform)
endpoint = ApiUser.webhook_url(platform)
data = {
eventType: "refresh",
enterpriseUrlid: enterprise_url,
scope: "ReadEnterprise",
}
connection = Faraday.new(
request: { timeout: 10 },
headers: {
'Authorization' => "Bearer #{token}",
}
) do |f|
f.request :json
f.response :json
f.response :raise_error
end
connection.post(ApiUser.webhook_url(platform), data)
api = DfcPlatformRequest.new(platform)
api.call(endpoint, data)
end
end

View File

@@ -12,6 +12,7 @@ DfcProvider::Engine.routes.draw do
resources :enterprise_groups, only: [:index, :show] do
resources :affiliated_by, only: [:create, :destroy], module: 'enterprise_groups'
end
resources :events, only: [:create]
resources :persons, only: [:show]
resources :supplied_products, only: [:index]
resources :product_groups, only: [:show]

View File

@@ -0,0 +1,127 @@
# frozen_string_literal: true
require_relative "../swagger_helper"
RSpec.describe "Events", swagger_doc: "dfc.yaml" do
include_context "authenticated as platform" do
let(:access_token) {
file_fixture("fdc_access_token.jwt").read
}
end
path "/api/dfc/events" do
post "Create Event" do
consumes "application/json"
produces "application/json"
parameter name: :event, in: :body, schema: {
example: {
eventType: "refresh",
enterpriseUrlid: "https://api.beta.litefarm.org/dfc/enterprises/",
scope: "ReadEnterprise",
}
}
response "400", "bad request" do
describe "with missing request body" do
around do |example|
# Rswag expects all required parameters to be supplied with `let`
# but we want to send a request without the request body parameter.
parameters = example.metadata[:operation][:parameters]
example.metadata[:operation][:parameters] = []
example.run
example.metadata[:operation][:parameters] = parameters
end
run_test!
end
describe "with empty request body" do
let(:event) { nil }
run_test!
end
describe "with missing parameter" do
let(:event) { { eventType: "refresh" } }
run_test!
end
end
response "401", "unauthorised" do
describe "as normal user" do
let(:Authorization) { nil }
let(:event) { { eventType: "refresh" } }
before { login_as create(:oidc_user) }
run_test!
end
describe "as other platform" do
let(:access_token) {
file_fixture("startinblox_access_token.jwt").read
}
let(:event) { { eventType: "refresh" } }
before { login_as create(:oidc_user) }
run_test!
end
end
response "200", "success" do
let(:event) do |example|
example.metadata[:operation][:parameters].first[:schema][:example]
end
before do
stub_request(:post, %r{openid-connect/token$})
end
describe "when some records fail" do
before do
body = {
'@context': "https://www.datafoodconsortium.org",
'@graph': [
{
'@id': "http://some-id",
'@type': "dfc-b:Enterprise",
'dfc-b:hasMainContact': "http://some-person",
'dfc-b:hasAddress': "http://address",
},
{
'@id': "http://some-person",
'@type': "dfc-b:Person",
'dfc-b:email': "community@litefarm.org",
},
{
'@id': "http://address",
'@type': "dfc-b:Address",
},
]
}.to_json
stub_request(:get, "https://api.beta.litefarm.org/dfc/enterprises/")
.to_return(body:)
end
run_test! do
expect(json_response["success"]).to eq true
expect(json_response["messages"].first)
.to match "http://some-id: Validation failed: Address address1 can't be blank"
end
end
describe "importing an empty list" do
before do
stub_request(:get, "https://api.beta.litefarm.org/dfc/enterprises/")
.to_return(body: "[]")
end
run_test! do
expect(json_response["success"]).to eq true
end
end
end
end
end
end

View File

@@ -0,0 +1,45 @@
# frozen_string_literal: true
require_relative "../spec_helper"
# These tests depend on valid OpenID Connect client credentials in your
# `.env.test.local` file.
#
# OPENID_APP_ID="..."
# OPENID_APP_SECRET="..."
RSpec.describe DfcImporter do
let(:endpoint) { "https://api.beta.litefarm.org/dfc/enterprises/" }
let(:semantic_id) {
"https://api.beta.litefarm.org/dfc/enterprises/23bfd9b1-98b5-4b91-88e5-efa7cb36219d"
}
it "fetches a list of enterprises", :vcr do
expect {
subject.import_enterprise_profiles("lf-dev", endpoint)
}.to have_enqueued_mail(Spree::UserMailer, :confirmation_instructions).exactly(7)
.and have_enqueued_mail(EnterpriseMailer, :welcome).exactly(6)
# You can show the emails in your browser.
# Consider creating a test helper if you find this useful elsewhere.
# allow(ApplicationMailer).to receive(:delivery_method).and_return(:letter_opener)
# perform_enqueued_jobs(only: ActionMailer::MailDeliveryJob)
# Repeating works without creating duplicates:
expect {
subject.import_enterprise_profiles("lf-dev", endpoint)
}.not_to have_enqueued_mail
enterprise = Enterprise.joins(:semantic_link).find_by(semantic_link: { semantic_id: })
expect(enterprise.name).to eq "DFC Test Farm Beta (All Supplied Fields)"
expect(enterprise.email_address).to eq "dfcshop@example.com"
expect(enterprise.logo.blob.content_type).to eq "image/webp"
expect(enterprise.logo.blob.byte_size).to eq 8974
expect(enterprise.visible).to eq "public"
expect(subject.errors.count).to eq 2
expect(subject.errors.first.record.semantic_link.semantic_id)
.to eq "https://api.beta.litefarm.org/dfc/enterprises/13152ea2-8d19-4309-a443-c95d8879d299"
expect(subject.errors.first.message)
.to eq "Validation failed: Address zipcode can't be blank, Address is invalid"
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
require_relative "../spec_helper"
# These tests depend on valid OpenID Connect client credentials in your
# `.env.test.local` file if you want to update the VCR cassettes.
#
# OPENID_APP_ID="..."
# OPENID_APP_SECRET="..."
RSpec.describe DfcPlatformRequest do
subject { DfcPlatformRequest.new(platform) }
let(:platform) { "cqcm-dev" }
it "receives an access token", :vcr do
token = subject.request_token
expect(token).to be_a String
expect(token.length).to be > 20
end
end

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
require_relative "../spec_helper"
RSpec.describe EnterpriseImporter do
subject { EnterpriseImporter.new(owner, dfc_enterprise) }
let(:owner) { Spree::User.new }
let(:dfc_enterprise) {
DataFoodConsortium::Connector::Enterprise.new(
"litefarm.org",
name: "Test Farm",
localizations: [
DataFoodConsortium::Connector::Address.new(
nil,
region: "Victoria",
country: {
scheme: "http",
host: "publications.europa.eu",
path: "/resource/authority/country/AUS",
}
)
],
socialMedias: [
DataFoodConsortium::Connector::SocialMedia.new(
nil,
name: "Facebook",
url: "dfc_test_farm",
)
],
)
}
it "assigns data to a new enterprise object" do
enterprise = subject.import
expect(enterprise.id).to eq nil
expect(enterprise.semantic_link.semantic_id).to eq "litefarm.org"
expect(enterprise.name).to eq "Test Farm"
expect(enterprise.address.state.name).to eq "Victoria"
expect(enterprise.address.country.name).to eq "Australia"
expect(enterprise.facebook).to eq "dfc_test_farm"
end
it "understands old country names" do
dfc_enterprise.localizations[0].country = "France"
dfc_enterprise.localizations[0].region = "Aquitaine"
enterprise = subject.import
expect(enterprise.id).to eq nil
expect(enterprise.address.country.name).to eq "France"
expect(enterprise.address.state.name).to eq "Aquitaine"
end
it "ignores errors during image import" do
dfc_enterprise.logo = "invalid url"
enterprise = subject.import
expect(enterprise.name).to eq "Test Farm"
expect(enterprise.logo.attached?).to eq false
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
require_relative "../spec_helper"
RSpec.describe OidcRequest do
subject(:api) { OidcRequest.new("some-token") }
it "gets a DFC document" do
stub_request(:get, "http://example.net/api").
to_return(status: 200, body: '{"@context":"/"}')
expect(api.call("http://example.net/api").body).to eq '{"@context":"/"}'
end
it "posts a DFC document" do
json = '{"name":"new season apples"}'
stub_request(:post, "http://example.net/api").
with(body: json).
to_return(status: 201) # Created
expect(api.call("http://example.net/api", json).body).to eq ""
end
it "reports and raises server errors" do
stub_request(:get, "http://example.net/api").to_return(status: 500)
expect(Bugsnag).to receive(:notify)
expect { api.call("http://example.net/api") }
.to raise_error(Faraday::ServerError)
end
end

View File

@@ -11,12 +11,6 @@ RSpec.describe ProxyNotifier do
let(:platform) { "cqcm-dev" }
let(:enterprise_url) { "http://ofn.example.net/api/dfc/enterprises/10000" }
it "receives an access token", :vcr do
token = subject.request_token(platform)
expect(token).to be_a String
expect(token.length).to be > 20
end
it "notifies the proxy", :vcr do
# The test server is not reachable by the notified server.
# If you don't have valid credentials, you'll get an unauthorized error.

File diff suppressed because one or more lines are too long

View File

@@ -58,37 +58,40 @@ RSpec.describe Enterprise do
expect(EnterpriseRelationship.where(id: [er1, er2])).to be_empty
end
it "raises a DeleteRestrictionError on destroy if distributed_orders exist" do
it "does not destroy distributed_orders upon destroy" do
enterprise = create(:distributor_enterprise)
create_list(:order, 2, distributor: enterprise)
expect do
enterprise.destroy
end.to raise_error(ActiveRecord::DeleteRestrictionError,
/Cannot delete record because of dependent distributed_orders/)
.and change { Spree::Order.count }.by(0)
expect(enterprise.errors.full_messages).to eq(
["Cannot delete record because dependent distributed orders exist"]
)
end.to change { Spree::Order.count }.by(0)
end
it "raises an DeleteRestrictionError on destroy if distributor_payment_methods exist" do
it "does not destroy distributor_payment_methods upon destroy" do
enterprise = create(:distributor_enterprise)
create_list(:distributor_payment_method, 2, distributor: enterprise)
expect do
enterprise.destroy
end.to raise_error(ActiveRecord::DeleteRestrictionError,
/Cannot delete record because of dependent distributor_payment_methods/)
.and change { DistributorPaymentMethod.count }.by(0)
expect(enterprise.errors.full_messages).to eq(
["Cannot delete record because dependent distributor payment methods exist"]
)
end.to change { Spree::Order.count }.by(0)
end
it "raises an DeleteRestrictionError on destroy if distributor_shipping_methods exist" do
it "does not destroy distributor_shipping_methods upon destroy" do
enterprise = create(:distributor_enterprise)
create_list(:distributor_shipping_method, 2, distributor: enterprise)
expect do
enterprise.destroy
end.to raise_error(ActiveRecord::DeleteRestrictionError,
/Cannot delete record because of dependent distributor_shipping_methods/)
.and change { DistributorShippingMethod.count }.by(0)
expect(enterprise.errors.full_messages).to eq(
["Cannot delete record because dependent distributor shipping methods exist"]
)
end.to change { Spree::Order.count }.by(0)
end
it "does not destroy enterprise_fees upon destroy" do
@@ -97,9 +100,10 @@ RSpec.describe Enterprise do
expect do
enterprise.destroy
end.to raise_error(ActiveRecord::DeleteRestrictionError,
/Cannot delete record because of dependent enterprise_fees/)
.and change { EnterpriseFee.count }.by(0)
expect(enterprise.errors.full_messages).to eq(
["Cannot delete record because dependent enterprise fees exist"]
)
end.to change { Spree::Order.count }.by(0)
end
it "does not destroy vouchers upon destroy" do
@@ -110,9 +114,10 @@ RSpec.describe Enterprise do
expect do
enterprise.destroy
end.to raise_error(ActiveRecord::DeleteRestrictionError,
/Cannot delete record because of dependent vouchers/)
.and change { Voucher.count }.by(0)
expect(enterprise.errors.full_messages).to eq(
["Cannot delete record because dependent vouchers exist"]
)
end.to change { Spree::Order.count }.by(0)
end
describe "relationships to other enterprises" do

View File

@@ -7,6 +7,15 @@ VCR.configure do |config|
config.hook_into :webmock
config.configure_rspec_metadata!
# Change recording mode during development:
#
# VCR_RECORD=new_episodes ./bin/rspec spec/example_spec.rb
# VCR_RECORD=all ./bin/rspec spec/example_spec.rb
#
if ENV.fetch("VCR_RECORD", nil)
config.default_cassette_options = { record: ENV.fetch("VCR_RECORD").to_sym }
end
# Chrome calls a lot of services and they trip us up.
config.ignore_hosts(
"localhost", "127.0.0.1", "0.0.0.0",

View File

@@ -111,6 +111,7 @@ RSpec.describe 'Customers' do
end
end
expect(page).not_to have_selector "tr#c_#{customer2.id}"
expect(page).not_to have_content 'You have unsaved changes'
}.to change{ Customer.count }.by(-1)
end

View File

@@ -68,6 +68,48 @@ RSpec.describe '
expect(page).to have_checked_field "enterprise_visible_only_through_links"
end
it "deleting an existing enterprise successfully" do
enterprise = create(:enterprise)
user = create(:user)
admin = login_as_admin
visit '/admin/enterprises'
expect do
accept_alert do
within "tr.enterprise-#{enterprise.id}" do
first("a", text: 'Delete').click
end
end
expect(page).to have_content("Successfully Removed")
end.to change{ Enterprise.count }.by(-1)
end
it "deleting an existing enterprise unsuccessfully" do
enterprise = create(:enterprise)
create(:order, distributor: enterprise)
user = create(:user)
admin = login_as_admin
visit '/admin/enterprises'
expect do
accept_alert do
within "tr.enterprise-#{enterprise.id}" do
first("a", text: 'Delete').click
end
end
expect(page).to have_content("Cannot delete record because dependent distributed order")
expect(page).to have_content(enterprise.name)
end.to change{ Enterprise.count }.by(0)
end
it "editing an existing enterprise" do
@enterprise = create(:enterprise)
e2 = create(:enterprise)

View File

@@ -579,6 +579,47 @@ paths:
dfc-b:URL: https://facebook.com/user
'404':
description: not found
"/api/dfc/events":
post:
summary: Create Event
parameters: []
tags:
- Events
responses:
'400':
description: bad request
content:
application/json:
examples:
test_example:
value:
success: false
message: Missing parameter `enterpriseUrlid`
'401':
description: unauthorised
content:
application/json:
examples:
test_example:
value:
success: false
message: Your client_id is not authorised on this platform.
'200':
description: success
content:
application/json:
examples:
test_example:
value:
success: true
requestBody:
content:
application/json:
schema:
example:
eventType: refresh
enterpriseUrlid: https://api.beta.litefarm.org/dfc/enterprises/
scope: ReadEnterprise
"/api/dfc/enterprises/{enterprise_id}/offers/{id}":
parameters:
- name: enterprise_id