mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-11 18:26:50 +00:00
Compare commits
48 Commits
9fd120f3a7
...
0d04dad080
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d04dad080 | ||
|
|
e98cf78b4c | ||
|
|
3173c79e8f | ||
|
|
ca14d557c1 | ||
|
|
59a3a5bd92 | ||
|
|
a226088f5c | ||
|
|
c65fcc1072 | ||
|
|
3bb68ec07e | ||
|
|
2c97638aa1 | ||
|
|
ceee9671d9 | ||
|
|
6b494be7ff | ||
|
|
a6855e6bc1 | ||
|
|
7ca43eb4a1 | ||
|
|
74b5ac559f | ||
|
|
07c236497c | ||
|
|
caf2ff9bb4 | ||
|
|
1b2a17d7e4 | ||
|
|
ce46115139 | ||
|
|
9fd2ff7620 | ||
|
|
98a25c1c7f | ||
|
|
6b78f8b855 | ||
|
|
1e2b28c559 | ||
|
|
12b86a35af | ||
|
|
4577bde692 | ||
|
|
af6be02ba4 | ||
|
|
0dabca583f | ||
|
|
d7603755bf | ||
|
|
f9d255a266 | ||
|
|
bcf4507795 | ||
|
|
9967ba2d06 | ||
|
|
f90f71cf68 | ||
|
|
fe8a0a908e | ||
|
|
bf6176c883 | ||
|
|
ffdfb7d450 | ||
|
|
3aa4c2a25f | ||
|
|
3331aaa382 | ||
|
|
b302dcfbec | ||
|
|
7dfc4d21ca | ||
|
|
f332a6934b | ||
|
|
baad0135f9 | ||
|
|
1973e36634 | ||
|
|
2e62531232 | ||
|
|
d811103a71 | ||
|
|
c526e72539 | ||
|
|
e217a6fca8 | ||
|
|
6aa7ef3c21 | ||
|
|
dc631026d4 | ||
|
|
c05532c166 |
42
Gemfile.lock
42
Gemfile.lock
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
4
app/views/admin/enterprises/destroy.turbo_stream.haml
Normal file
4
app/views/admin/enterprises/destroy.turbo_stream.haml
Normal 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 })
|
||||
@@ -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" }
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
39
engines/dfc_provider/app/services/dfc_importer.rb
Normal file
39
engines/dfc_provider/app/services/dfc_importer.rb
Normal 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
|
||||
32
engines/dfc_provider/app/services/dfc_platform_request.rb
Normal file
32
engines/dfc_provider/app/services/dfc_platform_request.rb
Normal 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
|
||||
@@ -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,
|
||||
|
||||
98
engines/dfc_provider/app/services/enterprise_importer.rb
Normal file
98
engines/dfc_provider/app/services/enterprise_importer.rb
Normal 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
|
||||
53
engines/dfc_provider/app/services/oidc_request.rb
Normal file
53
engines/dfc_provider/app/services/oidc_request.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
127
engines/dfc_provider/spec/requests/events_spec.rb
Normal file
127
engines/dfc_provider/spec/requests/events_spec.rb
Normal 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
|
||||
45
engines/dfc_provider/spec/services/dfc_importer_spec.rb
Normal file
45
engines/dfc_provider/spec/services/dfc_importer_spec.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
32
engines/dfc_provider/spec/services/oidc_request_spec.rb
Normal file
32
engines/dfc_provider/spec/services/oidc_request_spec.rb
Normal 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
|
||||
@@ -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.
|
||||
|
||||
257
spec/fixtures/vcr_cassettes/DfcImporter/fetches_a_list_of_enterprises.yml
vendored
Normal file
257
spec/fixtures/vcr_cassettes/DfcImporter/fetches_a_list_of_enterprises.yml
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user