Compare commits

..

2 Commits

58 changed files with 350 additions and 1171 deletions

View File

@@ -167,7 +167,7 @@ GEM
zeitwerk (>= 2.4, < 3.0)
acts_as_list (1.0.4)
activerecord (>= 4.2)
addressable (2.8.9)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
aes_key_wrap (1.1.0)
afm (1.0.0)
@@ -212,7 +212,7 @@ GEM
bindex (0.8.1)
bootsnap (1.22.0)
msgpack (~> 1.2)
bugsnag (6.29.0)
bugsnag (6.28.0)
concurrent-ruby (~> 1.0)
builder (3.3.0)
bullet (8.1.0)
@@ -385,11 +385,11 @@ GEM
good_migrations (0.3.1)
activerecord (>= 3.1)
railties (>= 3.1)
haml (7.2.0)
haml (6.3.0)
temple (>= 0.8.2)
thor
tilt
haml_lint (0.72.0)
haml_lint (0.68.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@@ -402,19 +402,18 @@ GEM
highline (3.1.2)
reline
htmlentities (4.4.2)
http_parser.rb (0.8.1)
http_parser.rb (0.8.0)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
i18n-js (3.9.2)
i18n (>= 0.6.6)
i18n-tasks (1.1.2)
i18n-tasks (1.0.15)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
highline (>= 3.0.0)
highline (>= 2.0.0)
i18n
parser (>= 3.2.2.1)
prism
rails-i18n
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.8, >= 1.8.1)
@@ -515,7 +514,7 @@ GEM
money (6.16.0)
i18n (>= 0.6.4, <= 2)
msgpack (1.8.0)
multi_json (1.19.1)
multi_json (1.17.0)
multi_xml (0.6.0)
mutex_m (0.3.0)
net-http (0.9.1)
@@ -591,12 +590,12 @@ GEM
hashery (~> 2.0)
ruby-rc4
ttfunk
pg (1.6.3)
pg (1.6.2)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.9.0)
private_address_check (0.6.0)
private_address_check (0.5.0)
pry (0.16.0)
coderay (~> 1.1)
method_source (~> 1.0)
@@ -604,10 +603,9 @@ GEM
psych (5.3.1)
date
stringio
public_suffix (7.0.5)
puffing-billy (4.0.4)
public_suffix (7.0.2)
puffing-billy (4.0.2)
addressable (~> 2.5)
cgi
em-http-request (~> 1.1, >= 1.1.0)
em-synchrony
eventmachine (~> 1.2)
@@ -898,7 +896,7 @@ GEM
faraday (~> 2.0)
faraday-follow_redirects
sysexits (1.2.0)
taler (0.3.0)
taler (0.2.0)
temple (0.10.4)
terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4)
@@ -916,7 +914,7 @@ GEM
turbo-rails (>= 1.3.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
undercover (0.8.4)
undercover (0.8.3)
base64
bigdecimal
imagen (>= 0.2.0)
@@ -959,7 +957,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.26.2)
webmock (3.26.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)

View File

@@ -3,15 +3,7 @@ angular.module('admin.orderCycles').controller 'AdminOrderCycleIncomingCtrl', ($
$scope.view = 'incoming'
# NB: weirdly at this next line $scope.order_cycle.id comes out undefined so we use $scope.order_cycle_id instead
$scope.enterprise_fees = null
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true) unless EnterpriseFee.loading
# We want to make sure to load the filtered EnterpriseFee when any previous request is finished
# otherwise the enterprise_fees migh get overriden by non filtered ones.
$scope.$watch(( -> EnterpriseFee.loading), (isLoading) =>
$scope.enterprise_fees ||= EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true) unless isLoading
)
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.order_cycle_id, per_item: true)
$scope.exchangeTotalVariants = (exchange) ->
return unless $scope.enterprises? && $scope.enterprises[exchange.enterprise_id]?

View File

@@ -14,14 +14,10 @@ angular.module('admin.orderCycles').factory('EnterpriseFee', ($resource) ->
EnterpriseFee: EnterpriseFee
enterprise_fees: {}
loaded: false
loading: false
index: (params={}) ->
return if @loading == true
@loading = true
EnterpriseFee.index params, (data) =>
@enterprise_fees = data
@loading = false
@loaded = true
forEnterprise: (enterprise_id) ->

View File

@@ -6,7 +6,6 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris
'manage_products'
'edit_profile'
'create_variant_overrides'
'create_linked_variants'
]
constructor: ->
@@ -31,4 +30,3 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris
when "manage_products" then t('js.services.manage_products')
when "edit_profile" then t('js.services.edit_profile')
when "create_variant_overrides" then t('js.services.add_products_to_inventory')
when "create_linked_variants" then t('js.services.create_linked_variants')

View File

@@ -107,33 +107,6 @@ module Admin
end
end
# Clone a variant, retaining a link to the "source"
def create_linked_variant
linked_variant = Spree::Variant.find(params[:variant_id])
product_index = params[:product_index]
authorize! :create_linked_variant, linked_variant
status = :ok
begin
variant = linked_variant.create_linked_variant(spree_current_user)
flash.now[:success] = t('.success')
variant_index = "-#{variant.id}"
rescue ActiveRecord::RecordInvalid
flash.now[:error] = variant.errors.full_messages.to_sentence
status = :unprocessable_entity
variant_index = "-1" # Create a unique-enough index
end
respond_with do |format|
format.turbo_stream {
locals = { linked_variant:, variant:, product_index:, variant_index:,
producer_options:, category_options: categories, tax_category_options: }
render :create_linked_variant, status:, locals:
}
end
end
def index_url(params)
"/admin/products?#{params.to_query}" # todo: fix routing so this can be automaticly generated
end

View File

@@ -52,7 +52,7 @@ module Spree
# (we can't use respond_override because Spree no longer uses respond_with)
def fire
event = params[:e]
return unless event
return unless event && @payment.payment_source
# capture_and_complete_order will complete the order, so we want to try to redeem VINE
# voucher first and exit if it fails

View File

@@ -47,10 +47,5 @@ module Admin
def variant_tag_enabled?(user)
feature?(:variant_tag, user) || feature?(:variant_tag, *user.enterprises)
end
def allowed_source_producers
@allowed_source_producers ||= OpenFoodNetwork::Permissions.new(spree_current_user)
.enterprises_granting_linked_variants
end
end
end

View File

@@ -22,10 +22,10 @@ class PaymentMailer < ApplicationMailer
end
end
def refund_available(amount, payment, taler_order_status_url)
def refund_available(payment, taler_order_status_url)
@order = payment.order
@shop = @order.distributor.name
@amount = amount
@amount = payment.display_amount
@taler_order_status_url = taler_order_status_url
I18n.with_locale valid_locale(@order.user) do

View File

@@ -197,16 +197,12 @@ module Spree
can [:admin, :index, :destroy], :oidc_setting
can [:admin, :create], Voucher
can [:admin, :destroy], EnterpriseRole do |enterprise_role|
enterprise_role.enterprise.owner_id == user.id
end
end
def add_product_management_abilities(user)
# Enterprise User can only access products that they are a supplier for
can [:create], Spree::Product
# An enterprise user can change a product if they are supplier of at least
# An enterperprise user can change a product if they are supplier of at least
# one of the product's associated variants
can [:admin, :read, :index, :update,
:seo, :group_buy_options,
@@ -218,16 +214,7 @@ module Spree
)
end
# An enterprise user can clone if they have been granted permission to the source variant.
# Technically I'd call this permission clone_linked_variant, but it would be less confusing to
# use the same name as everywhere else.
can [:create_linked_variant], Spree::Variant do |variant|
OpenFoodNetwork::Permissions.new(user).
enterprises_granting_linked_variants.include? variant.supplier
end
can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone,
:create_linked_variant], :products_v3
can [:admin, :index, :bulk_update, :destroy, :destroy_variant, :clone], :products_v3
can [:create], Spree::Variant
can [:admin, :index, :read, :edit,

View File

@@ -63,6 +63,35 @@ module Spree
"XXXX-XXXX-XXXX-#{last_digits}"
end
def actions
%w{capture_and_complete_order void credit resend_authorization_email}
end
def can_resend_authorization_email?(payment)
payment.requires_authorization?
end
# Indicates whether its possible to capture the payment
def can_capture_and_complete_order?(payment)
return false if payment.requires_authorization?
payment.pending? || payment.checkout?
end
# Indicates whether its possible to void the payment.
def can_void?(payment)
!payment.void?
end
# Indicates whether its possible to credit the payment. Note that most gateways require that the
# payment be settled first which generally happens within 12-24 hours of the transaction.
def can_credit?(payment)
return false unless payment.completed?
return false unless payment.order.payment_state == 'credit_owed'
payment.credit_allowed.positive?
end
# Allows us to use a gateway_payment_profile_id to store Stripe Tokens
def has_payment_profile?
gateway_customer_profile_id.present? || gateway_payment_profile_id.present?

View File

@@ -13,35 +13,6 @@ module Spree
preference :server, :string, default: 'live'
preference :test_mode, :boolean, default: false
def actions
%w{capture_and_complete_order void credit resend_authorization_email}
end
# Indicates whether its possible to capture the payment
def can_capture_and_complete_order?(payment)
return false if payment.requires_authorization?
payment.pending? || payment.checkout?
end
# Indicates whether its possible to void the payment.
def can_void?(payment)
!payment.void?
end
# Indicates whether its possible to credit the payment. Note that most gateways require that the
# payment be settled first which generally happens within 12-24 hours of the transaction.
def can_credit?(payment)
return false unless payment.completed?
return false unless payment.order.payment_state == 'credit_owed'
payment.credit_allowed.positive?
end
def can_resend_authorization_email?(payment)
payment.requires_authorization?
end
def payment_source_class
CreditCard
end

View File

@@ -152,10 +152,11 @@ module Spree
end
def actions
return [] unless payment_method.respond_to?(:actions)
return [] unless payment_source.respond_to?(:actions)
payment_method.actions.select do |action|
payment_method.__send__("can_#{action}?", self)
payment_source.actions.select do |action|
!payment_source.respond_to?("can_#{action}?") ||
payment_source.__send__("can_#{action}?", self)
end
end
@@ -165,6 +166,11 @@ module Spree
PaymentMailer.authorize_payment(self).deliver_later
end
def payment_source
res = source.is_a?(Payment) ? source.source : source
res || payment_method
end
def ensure_correct_adjustment
revoke_adjustment_eligibility if ['failed', 'invalid', 'void'].include?(state)
return if adjustment.try(:finalized?)

View File

@@ -18,27 +18,15 @@ module Spree
# - backend_url: https://backend.demo.taler.net/instances/sandbox
# - api_key: sandbox
class Taler < PaymentMethod
# Demo backend instances will use the KUDOS currency.
DEMO_PREFIX = "https://backend.demo.taler.net/instances"
preference :backend_url, :string
preference :api_key, :password
def actions
%w[credit void]
%w{void}
end
def can_void?(payment)
# The source can be another payment. Then this is an offset payment
# like a credit record. We can't void a refund.
payment.source == self && payment.state == "completed"
end
def can_credit?(payment)
return false unless payment.completed?
return false unless payment.order.payment_state == 'credit_owed'
payment.credit_allowed.positive?
payment.state == "completed"
end
# Name of the view to display during checkout
@@ -80,23 +68,6 @@ module Spree
ActiveMerchant::Billing::Response.new(success, message)
end
def credit(money, response_code, gateway_options)
amount = money / 100 # called with cents
payment = gateway_options[:payment]
taler_order = taler_order(id: response_code)
status = taler_order.fetch("order_status")
raise "Unsupported action" if status != "paid"
taler_amount = "KUDOS:#{amount}"
taler_order.refund(refund: taler_amount, reason: "credit")
spree_money = Spree::Money.new(amount, currency: payment.currency).to_s
PaymentMailer.refund_available(spree_money, payment, taler_order.status_url).deliver_later
ActiveMerchant::Billing::Response.new(true, "Refund initiated")
end
def void(response_code, gateway_options)
payment = gateway_options[:payment]
taler_order = taler_order(id: response_code)
@@ -111,8 +82,7 @@ module Spree
amount = taler_order.fetch("contract_terms")["amount"]
taler_order.refund(refund: amount, reason: "void")
spree_money = payment.money.to_s
PaymentMailer.refund_available(spree_money, payment, taler_order.status_url).deliver_later
PaymentMailer.refund_available(payment, taler_order.status_url).deliver_later
ActiveMerchant::Billing::Response.new(true, "Refund initiated")
end
@@ -126,7 +96,7 @@ module Spree
def create_taler_order(payment)
# We are ignoring currency for now so that we can test with the
# current demo backend only working with the KUDOS currency.
taler_amount = "#{currency(payment)}:#{payment.amount}"
taler_amount = "KUDOS:#{payment.amount}"
urls = Rails.application.routes.url_helpers
fulfillment_url = urls.payment_gateways_confirm_taler_url(payment_id: payment.id)
taler_order.create(
@@ -143,12 +113,6 @@ module Spree
id:,
)
end
def currency(payment)
return "KUDOS" if preferred_backend_url.starts_with?(DEMO_PREFIX)
payment.order.currency
end
end
end
end

View File

@@ -40,7 +40,6 @@ module Spree
belongs_to :shipping_category, class_name: 'Spree::ShippingCategory', optional: false
belongs_to :primary_taxon, class_name: 'Spree::Taxon', touch: true, optional: false
belongs_to :supplier, class_name: 'Enterprise', optional: false, touch: true
belongs_to :hub, class_name: 'Enterprise', optional: true
delegate :name, :name=, :description, :description=, :meta_keywords, to: :product
@@ -73,15 +72,6 @@ module Spree
has_many :semantic_links, as: :subject, dependent: :delete_all
has_many :supplier_properties, through: :supplier, source: :properties
# Linked variants: I may have one or many sources.
has_many :variant_links_as_target, class_name: 'VariantLink', foreign_key: :target_variant_id,
dependent: :delete_all, inverse_of: :target_variant
has_many :source_variants, through: :variant_links_as_target, source: :source_variant
# I may also have one more many targets.
has_many :variant_links_as_source, class_name: 'VariantLink', foreign_key: :source_variant_id,
dependent: :delete_all, inverse_of: :source_variant
has_many :target_variants, through: :variant_links_as_source, source: :target_variant
localize_number :price, :weight
validates_lengths_from_database
@@ -273,24 +263,6 @@ module Spree
@on_hand_desired = ActiveModel::Type::Integer.new.cast(val)
end
# Clone this variant, retaining a 'source' link to it
def create_linked_variant(user)
# Hub owner is my enterprise which has permission to create variant sourced from that supplier
hub_id = EnterpriseRelationship.permitted_by(supplier).permitting(user.enterprises)
.with_permission(:create_linked_variants)
.pick(:child_id)
dup.tap do |variant|
variant.price = price
variant.source_variants = [self]
variant.stock_items << Spree::StockItem.new(variant:)
variant.hub_id = hub_id
variant.on_demand = on_demand
variant.on_hand = on_hand
variant.save!
end
end
private
def check_currency

View File

@@ -1,6 +0,0 @@
# frozen_string_literal: true
class VariantLink < ApplicationRecord
belongs_to :source_variant, class_name: 'Spree::Variant'
belongs_to :target_variant, class_name: 'Spree::Variant'
end

View File

@@ -14,6 +14,7 @@ module Checkout
apply_strong_parameters
set_pickup_address
set_address_details
set_payment_amount
set_existing_card
@order_params
@@ -57,6 +58,12 @@ module Checkout
end
end
def set_payment_amount
return unless @order_params[:payments_attributes]
@order_params[:payments_attributes].first[:amount] = order.outstanding_balance.amount
end
def set_existing_card
return unless existing_card_selected?

View File

@@ -13,7 +13,7 @@ module Payments
payment: @payment.slice(:updated_at, :amount, :state),
enterprise: @enterprise.slice(:abn, :acn, :name)
.merge(address: @enterprise.address.slice(:address1, :address2, :city, :zipcode)),
order: @order.slice(:number, :total, :currency).merge(line_items: line_items)
order: @order.slice(:total, :currency).merge(line_items: line_items)
}.with_indifferent_access
end
@@ -31,7 +31,6 @@ module Payments
def self.test_order
order = Spree::Order.new(
number: "R555555555",
total: 0.00,
currency: "AUD",
)

View File

@@ -11,10 +11,9 @@
-# Filter out variant a user has not permission to update, but keep variant with no supplier
- next if variant.supplier.present? && !allowed_producers.include?(variant.supplier)
= form.fields_for("products][#{product_index}][variants_attributes", variant, index: variant_index) do |variant_form|
%tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper", 'data-new-record': variant.new_record? ? "true" : false }
= render partial: 'variant_row', locals: { variant:, f: variant_form, product_index:, category_options:, tax_category_options:, producer_options: }
= render partial: 'variant_row', locals: { variant:, f: variant_form, category_options:, tax_category_options:, producer_options: }
= form.fields_for("products][#{product_index}][variants_attributes][NEW_RECORD", prepare_new_variant(product, producer_options)) do |new_variant_form|
%template{ 'data-nested-form-target': "template" }

View File

@@ -1,10 +1,7 @@
-# haml-lint:disable ViewLength (This file is big, but doesn't make sense to split up at this point)
-# locals: (variant:, f:, product_index: nil, category_options:, tax_category_options:, producer_options:)
-# locals: (variant:, f:, category_options:, tax_category_options:, producer_options:)
- method_on_demand, method_on_hand = variant.new_record? ? [:on_demand_desired, :on_hand_desired ]: [:on_demand, :on_hand]
%td.col-image
-# empty
- variant.source_variants.each do |source_variant|
= content_tag(:span, "🔗", title: t('admin.products_page.variant_row.sourced_from', source_name: source_variant.display_name, source_id: source_variant.id, hub_name: variant.hub&.name))
%td.col-name.field.naked_inputs
= f.hidden_field :id
= f.text_field :display_name, 'aria-label': t('admin.products_page.columns.name'), placeholder: variant.product.name
@@ -91,10 +88,6 @@
= render(VerticalEllipsisMenuComponent.new) do
- if variant.persisted?
= link_to t('admin.products_page.actions.edit'), edit_admin_product_variant_path(variant.product, variant)
- if allowed_source_producers.include?(variant.supplier)
= link_to t('admin.products_page.actions.create_linked_variant'), admin_create_linked_variant_path(variant_id: variant.id, product_index:), 'data-turbo-method': :post
- if variant.product.variants.size > 1
%a{ "data-controller": "modal-link", "data-action": "click->modal-link#setModalDataSetOnConfirm click->modal-link#open",
"data-modal-link-target-value": "variant-delete-modal", "class": "delete",

View File

@@ -1,16 +0,0 @@
-# locals: (variant:, linked_variant:, product_index:, variant_index:, producer_options:, category_options:, tax_category_options:)
-# Pre-render the form, because you can't do it inside turbo stream block
- variant_row = nil
- fields_for("products][#{product_index}][variants_attributes", variant, index: variant_index) do |f|
- variant_row = render(partial: 'variant_row', formats: :html,
locals: { f:,
variant:,
producer_options:,
category_options:,
tax_category_options:})
= turbo_stream.after dom_id(linked_variant) do
%tr.condensed{ id: dom_id(variant), 'data-controller': "variant", 'class': "nested-form-wrapper slide-in",'data-variant-bulk-form-outlet': "#products-form"}
= variant_row
= turbo_stream.append "flashes" do
= render(partial: 'admin/shared/flashes', locals: { flashes: flash })

View File

@@ -6,7 +6,7 @@
%p= t ".description"
%fieldset.no-border-top.no-border-bottom
.row.field
.row
= f.label :email, t(:email)
= f.email_field :email, placeholder: t('.eg_email_address'), data: { controller: "select-user" }, inputmode: "email", autocomplete: "off"
= f.error_message_on :email

View File

@@ -44,17 +44,12 @@ export default class BulkFormController extends Controller {
}
// Register any new elements (may be called by another controller after dynamically adding fields)
// May be called with array of elements to register, otherwise finds all un-registered elements.
registerElements(eventOrElements = null) {
let newElements;
if (Array.isArray(eventOrElements)) {
newElements = eventOrElements;
} else {
const registeredElements = Object.values(this.recordElements).flat();
// Select only elements that haven't been registered yet
newElements = Array.from(this.form.elements).filter((n) => !registeredElements.includes(n));
}
registerElements() {
const registeredElements = Object.values(this.recordElements).flat();
// Select only elements that haven't been registered yet
const newElements = Array.from(this.form.elements).filter(
(n) => !registeredElements.includes(n),
);
this.#registerElements(newElements);
}

View File

@@ -4,8 +4,6 @@ import OptionValueNamer from "js/services/option_value_namer";
// Dynamically update related variant fields
//
export default class VariantController extends Controller {
static outlets = ["bulk-form"];
connect() {
// idea: create a helper that includes a nice getter/setter for Rails model attr values, just pass it the attribute name.
// It could automatically find (and cache a ref to) each dom element and get/set the values.
@@ -42,12 +40,6 @@ export default class VariantController extends Controller {
// on display_as changed; update unit_to_display
// TODO: optimise to avoid unnecessary OptionValueNamer calc
this.displayAs.addEventListener("input", this.#updateUnitDisplay.bind(this), { passive: true });
// Register with bulk products form to listen for changes. Used when dynamically appending variants.
if (this.hasBulkFormOutlet) {
const formElements = this.element.querySelectorAll("input, select, textarea, button");
this.bulkFormOutlet.registerElements(formElements);
}
}
disconnect() {

View File

@@ -54,7 +54,6 @@
.button, button {
@include border-radius(0.5em);
font-family: inherit;
outline: none;
&.x-small {
@@ -66,6 +65,7 @@
}
.button.primary, button.primary {
font-family: $body-font;
background: $orange-450;
color: white;
}

View File

@@ -717,11 +717,8 @@ en:
delete: Delete
remove: Remove
preview: Preview
create_linked_variant: Create linked variant
image:
edit: Edit
variant_row:
sourced_from: "Sourced from: %{source_name} (%{source_id}); Hub: %{hub_name}"
product_preview:
product_preview: Product preview
shop_tab: Shop
@@ -1101,8 +1098,6 @@ en:
clone:
success: Successfully cloned the product
error: Unable to clone the product
create_linked_variant:
success: "Successfully created linked variant"
tag_rules:
rules_per_tag:
one: "%{tag} has 1 rule"
@@ -3906,7 +3901,6 @@ en:
manage_products: "manage products"
edit_profile: "edit profile"
add_products_to_inventory: "add products to inventory"
create_linked_variants: "create linked variants [BETA]"
resources:
could_not_delete_customer: 'Could not delete customer'
product_import:

View File

@@ -287,7 +287,7 @@ en_CA:
customer_instructions: "Customer instructions"
additional_information: "Additional information"
connect_app:
url: "https://n8n.openfoodnetwork.org/webhook-test/foodjustice/connect-enterprise"
url: "https://n8n.openfoodnetwork.org/webhook/foodjustice/connect-enterprise"
devise:
passwords:
spree_user:
@@ -668,11 +668,8 @@ en_CA:
delete: Delete
remove: Remove
preview: Preview
create_linked_variant: Create linked variant
image:
edit: Edit
variant_row:
sourced_from: "Sourced from: %{source_name} (%{source_id}); Hub: %{hub_name}"
product_preview:
product_preview: Product preview
shop_tab: Shop
@@ -814,7 +811,6 @@ en_CA:
bill_address: "Billing Address"
ship_address: "Shipping Address"
balance: "Balance"
credit: "Available Credit"
update_address_success: "Address updated successfully."
update_address_error: "Sorry! Please input all of the required fields!"
edit_bill_address: "Edit Billing Address"
@@ -829,16 +825,12 @@ en_CA:
guest_label: "Guest checkout"
credit_owed: "Credit Owed"
balance_due: "Balance Due"
id: Id
destroy:
has_associated_subscriptions: "Delete failed: This customer has active subscriptions. Cancel them first."
customer_account_transaction:
index:
available_credit: "Available credit: %{available_credit}"
transaction_date: Transaction Date
description: Description
amount: Amount
created_by: Created by
running_balance: Running balance
column_preferences:
bulk_update:
@@ -1028,8 +1020,6 @@ en_CA:
clone:
success: Successfully cloned the product
error: Unable to clone the product
create_linked_variant:
success: "Successfully created linked variant"
tag_rules:
rules_per_tag:
one: "%{tag} has 1 rule"
@@ -1799,11 +1789,6 @@ en_CA:
images: "Images"
contact: "Contact"
web: "Web Resources"
stimulus_pagination:
navigation: Pagination
page: "Page %{number}"
previous: Previous page
next: Next page
enterprise_issues:
create_new: Create New
resend_email: Resend Email
@@ -2470,7 +2455,6 @@ en_CA:
order_total: Total order
order_payment: "Paying via:"
no_payment_required: "No payment required"
credit_used: "Credit used: %{amount}"
customer_credit: Credit
order_billing_address: Billing address
order_delivery_on: Delivery on
@@ -3455,7 +3439,6 @@ en_CA:
no_orders_found: "No Orders Found"
order_information: "Order Information"
new_payment: "New Payment"
credit_customer: Credit customer
create_or_update_invoice: "Create or Update Invoice"
date_completed: "Date Completed"
amount: "Amount"
@@ -3766,7 +3749,6 @@ en_CA:
manage_products: "manage products"
edit_profile: "edit profile"
add_products_to_inventory: "add products to inventory"
create_linked_variants: "create linked variants [BETA]"
resources:
could_not_delete_customer: 'Could not delete customer'
product_import:
@@ -4047,7 +4029,6 @@ en_CA:
items_cannot_be_shipped: "Items cannot be shipped"
gateway_config_unavailable: "Gateway config unavailable"
gateway_error: "Payment failed"
internal_payment_not_voidable: Payment not voidable
more: "More"
new_adjustment: "New adjustment"
new_tax_category: "New Tax Category"
@@ -4542,7 +4523,6 @@ en_CA:
paypalexpress: "PayPal Express"
stripesca: "Stripe SCA"
taler: "Taler"
customercredit: "Customer Credit"
payments:
source_forms:
stripe:
@@ -4550,7 +4530,6 @@ en_CA:
submitting_payment: Submitting payment...
paypal:
no_payment_via_admin_backend: Paypal payments cannot be captured in the backoffice.
customer_credit_successful: Customer has been successfully credited!
products:
image_upload_error: "Please upload the image in JPG, PNG, GIF, SVG or WEBP format."
image_not_processable: "Image attachment is not a valid image."
@@ -4866,7 +4845,6 @@ en_CA:
orders: Orders
cards: Credit Cards
transactions: Transactions
customer_account_transactions: Customer Transactions
settings: Account Settings
unconfirmed_email: "Pending email confirmation for: %{unconfirmed_email}. Your email address will be updated once the new email is confirmed."
orders:
@@ -4877,9 +4855,6 @@ en_CA:
authorisation_required: Authorisation Required
authorise: Authorize
customer_account_transactions:
title: Customer Transactions
credit_available: "Credit available: %{credit}"
transaction_date: Transaction Date
description: Description
amount: Amount
running_balance: Running balance
@@ -5022,22 +4997,3 @@ en_CA:
invisible_captcha:
sentence_for_humans: "Please leave empty"
timestamp_error_message: "Please try again after 5 seconds."
api_customer_credit: "API credit: %{description}"
credit_payment_method:
name: Customer credit
description: Allow customer to pay with credit
success: Payment with credit was sucessful
void_success: Credit void was sucessful
order_payment_description: "Customer credit: Payment for order: %{order_number}"
order_void_description: "Customer credit: Refund for order: %{order_number}"
errors:
customer_not_found: Customer not found
missing_payment: Missing payment
credit_payment_method_missing: Credit payment method is missing
no_credit_available: No credit available
not_enough_credit_available: Not enough credit available
orders:
customer_credit_service:
no_credit_owed: No credit owed
credit_payment_method_missing: Customer credit payment method is missing, please check configuration
refund_sucessful: Refund successful!

View File

@@ -1432,7 +1432,7 @@ en_GB:
legend: "Users"
email_confirmation_notice_html: "Email confirmation is pending. We've sent a confirmation email to %{email}."
resend: Resend
contact: "Notifications"
contact: "Contact"
manager: "Manager"
owner: 'Owner'
contact_tip: "The manager who will receive enterprise emails for orders and notifications. Must have a confirmed email adress."

View File

@@ -669,11 +669,8 @@ fr:
delete: Supprimer
remove: Supprimer
preview: Prévisualisation
create_linked_variant: Créer une variante liée
image:
edit: Modifier
variant_row:
sourced_from: "Source : %{source_name} ( %{source_id} ) ; Hub : %{hub_name}"
product_preview:
product_preview: Prévisualisation du produit
shop_tab: Boutique
@@ -1031,8 +1028,6 @@ fr:
clone:
success: Le produit a bien été dupliqué
error: Impossible de dupliquer le produit
create_linked_variant:
success: "Variante liée créée avec succès"
tag_rules:
rules_per_tag:
one: "%{tag} comporte une règle"
@@ -3804,7 +3799,6 @@ fr:
manage_products: "modifier les produits"
edit_profile: "modifier le profil"
add_products_to_inventory: "ajouter les produits au catalogue boutique"
create_linked_variants: "créer des variantes liées [BÊTA]"
resources:
could_not_delete_customer: 'L''acheteur n''a pas pu être supprimé'
product_import:

View File

@@ -662,11 +662,8 @@ fr_BE:
delete: Supprimer
remove: Supprimer
preview: Aperçu
create_linked_variant: Créer une variante liée
image:
edit: Modifier
variant_row:
sourced_from: "Source : %{source_name} (%{source_id}); Hub : %{hub_name}"
product_preview:
product_preview: Aperçu du produit
shop_tab: Comptoir
@@ -1024,8 +1021,6 @@ fr_BE:
clone:
success: Le produit a bien été dupliqué
error: Impossible de dupliquer le produit
create_linked_variant:
success: "Variante liée créée avec succès"
tag_rules:
rules_per_tag:
one: "%{tag} comporte 1 règle"
@@ -3779,7 +3774,6 @@ fr_BE:
manage_products: "modifier les produits"
edit_profile: "modifier le profil"
add_products_to_inventory: "ajouter les produits au catalogue comptoir"
create_linked_variants: "créer des variantes liées [BÊTA]"
resources:
could_not_delete_customer: 'L''acheteur·euse n''a pas pu être supprimé'
product_import:

View File

@@ -274,10 +274,6 @@ fr_CA:
no_default_card: "Pas de carte de paiement par défaut pour cet acheteur"
shipping_method:
not_available_to_shop: "n'est pas disponible pour %{shop}"
user_invitation:
attributes:
email:
is_already_manager: est déjà gestionnaire!
card_details: "Détalis de la carte"
card_type: "Type de carte"
card_type_is: "Type de carte"
@@ -670,11 +666,8 @@ fr_CA:
delete: Supprimer
remove: Supprimer
preview: Prévisualisation
create_linked_variant: Créer une variante liée
image:
edit: Modifier
variant_row:
sourced_from: "Source : %{source_name} (%{source_id}); Hub : %{hub_name}"
product_preview:
product_preview: Prévisualisation du produit
shop_tab: Boutique
@@ -816,7 +809,6 @@ fr_CA:
bill_address: "Adresse de facturation"
ship_address: "Adresse de livraison"
balance: "Solde"
credit: "Crédit disponible"
update_address_success: "Adresse mise à jour avec succès."
update_address_error: "Oups! Veuillez remplir tous les champs obligatoires!"
edit_bill_address: "Modifier l'adresse de facturation"
@@ -831,16 +823,12 @@ fr_CA:
guest_label: "Commande en mode invite"
credit_owed: "Crédit dû"
balance_due: "Solde dû"
id: Id
destroy:
has_associated_subscriptions: "La suppression a planté : cet acheteur a des abonnements actifs. Veuillez d'abord les annuler."
customer_account_transaction:
index:
available_credit: "Crédit disponible : %{available_credit}"
transaction_date: Date de la transaction
description: Description
amount: Montant
created_by: Créé par
running_balance: Solde courant
column_preferences:
bulk_update:
@@ -1032,8 +1020,6 @@ fr_CA:
clone:
success: Le produit a bien été dupliqué
error: Impossible de dupliquer le produit
create_linked_variant:
success: "Variante liée créée avec succès"
tag_rules:
rules_per_tag:
one: "%{tag} a 1 règle"
@@ -1440,12 +1426,10 @@ fr_CA:
show_hide_payment: 'Afficher ou Montrer les méthodes de paiement lors de la finalisation de commande'
show_hide_order_cycles: 'Afficher ou Masquer les cycles de vente de ma boutique'
users:
description: Les utilisateurs autorisés à gérer cette entreprise.
legend: "Utilisateurs"
email_confirmation_notice_html: "L'email de confirmation n'a pas encore été validé. Il a été envoyé à %{email}."
resend: Renvoyer
contact: "Contact"
manager: "Gestionnaire"
owner: 'Manager principal'
contact_tip: "Le manager qui recevra les emails de confirmation de commande et autres notifications de l'entreprise. Il doit avoir confirmé son adresse email pour pouvoir être sélectionné."
owner_tip: Manager principal de cette entreprise.
@@ -1456,8 +1440,6 @@ fr_CA:
invite_manager: "Inviter un manager"
email_confirmed: "Email confirmé"
email_not_confirmed: "Email non confirmé"
set_as_contact: "Configurer %{email} comme contact"
set_as_owner: "Définir %{email} comme propriétaire"
vouchers:
legend: Bon de réduction
voucher_code: Code promo
@@ -1806,11 +1788,6 @@ fr_CA:
images: "Images"
contact: "Contact"
web: "Liens web"
stimulus_pagination:
navigation: Pagination
page: "Page %{number}"
previous: Page précédente
next: Page suivante
enterprise_issues:
create_new: Créer Nouveau
resend_email: Renvoyer l'email
@@ -2041,10 +2018,7 @@ fr_CA:
user_invitations:
new:
back: Retour
description: "Invitez un utilisateur à s'inscrire et à devenir gestionnaire de cette entreprise."
eg_email_address: 'ex : l''adresse e-mail d''un utilisateur nouveau ou existant'
email: Email
invite_new_user: Inviter un nouvel utilisateur
invite: Inviter
vouchers:
new:
@@ -2480,7 +2454,6 @@ fr_CA:
order_total: Total commande
order_payment: "Payer via:"
no_payment_required: "Pas de paiement requis"
credit_used: "Crédit utilisé : %{amount}"
customer_credit: Crédit
order_billing_address: Adresse de facturation
order_delivery_on: Livraison prévue
@@ -3468,7 +3441,6 @@ fr_CA:
no_orders_found: "Aucune commande trouvée"
order_information: "Info commande"
new_payment: "Nouveau paiement"
credit_customer: Client à crédit
create_or_update_invoice: "Créer ou mettre à jour la facture"
date_completed: "Date d'opération"
amount: "Montant"
@@ -3790,7 +3762,6 @@ fr_CA:
manage_products: "Gérer les produits"
edit_profile: "modifier le profil"
add_products_to_inventory: "ajouter les produits au catalogue boutique"
create_linked_variants: "créer des variantes liées [BÊTA]"
resources:
could_not_delete_customer: 'L''acheteur n''a pas pu être supprimé'
product_import:
@@ -4105,7 +4076,6 @@ fr_CA:
items_cannot_be_shipped: "Les produits ne peuvent pas être envoyés"
gateway_config_unavailable: "Configuration de la passerelle indisponible"
gateway_error: "Le paiement a échoué"
internal_payment_not_voidable: Paiement non annulable
more: "Plus"
new_adjustment: "Nouvel ajustement"
new_tax_category: "Nouvelle catégorie de taxe"
@@ -4600,7 +4570,6 @@ fr_CA:
paypalexpress: "PayPal Express"
stripesca: "Stripe SCA"
taler: "Taler"
customercredit: "Crédit client"
payments:
source_forms:
stripe:
@@ -4608,7 +4577,6 @@ fr_CA:
submitting_payment: Envoi du paiement...
paypal:
no_payment_via_admin_backend: 'Il n''est pas encore possible de payer avec Paypal via l''administration. '
customer_credit_successful: Le client a été crédité avec succès!
products:
image_upload_error: "Veuillez utiliser une image au format JPG, PNG, GIF, SVG ou WEBP format."
image_not_processable: "L'image n'est pas valide"
@@ -4925,7 +4893,6 @@ fr_CA:
orders: Commandes
cards: Cartes bancaires
transactions: Achats
customer_account_transactions: Transactions des clients
settings: Paramètres du Compte
unconfirmed_email: "Attente de validation pour l'email: %{unconfirmed_email}. Votre adresse email sera mise à jour quand le nouvel email aura été confirmé."
orders:
@@ -4936,9 +4903,6 @@ fr_CA:
authorisation_required: Autorisation nécessaire
authorise: Autorise
customer_account_transactions:
title: Transactions des clients
credit_available: "Crédit disponible : %{credit}"
transaction_date: Date de la transaction
description: Description
amount: Montant
running_balance: Solde courant
@@ -5094,22 +5058,3 @@ fr_CA:
invisible_captcha:
sentence_for_humans: "Merci de laisser ce champ libre"
timestamp_error_message: "S'il vous plaît réessayez après 5 secondes."
api_customer_credit: "Crédit API : %{description}"
credit_payment_method:
name: Crédit client
description: Autoriser le client à payer par crédit
success: Le paiement par crédit a été effectué avec succès.
void_success: L'annulation du crédit a réussi.
order_payment_description: "Crédit client : Paiement de la commande :%{order_number}"
order_void_description: "Crédit client : Remboursement pour la commande :%{order_number}"
errors:
customer_not_found: Client introuvable
missing_payment: Paiement manquant
credit_payment_method_missing: Le mode de paiement par crédit est manquant.
no_credit_available: Aucune carte de paiement autorisée disponible
not_enough_credit_available: Crédit disponible insuffisant
orders:
customer_credit_service:
no_credit_owed: Aucun crédit dû
credit_payment_method_missing: Le mode de paiement par crédit client est manquant. Veuillez vérifier la configuration.
refund_sucessful: Remboursement effectué avec succès !

View File

@@ -1298,10 +1298,10 @@ hu:
add_new_button: '+ Új alapértelmezett szabály hozzáadása'
no_tags_yet: Ehhez a vállalkozáshoz még nem tartozik címke
add_new_tag: '+ Új címke hozzáadása'
show_hide_variants: 'Termékváltozatok megjelenítése vagy elrejtése a kínálatomban'
show_hide_variants: 'Változatok megjelenítése vagy elrejtése a kirakatomban'
show_hide_shipping: 'Áruátadási módok megjelenítése vagy elrejtése fizetéskor'
show_hide_payment: 'Fizetési módok megjelenítése vagy elrejtése rendelés leadásakor'
show_hide_order_cycles: 'Rendelési ciklusok megjelenítése vagy elrejtése a kínálatomban'
show_hide_payment: 'Fizetési módok megjelenítése vagy elrejtése a pénztárnál'
show_hide_order_cycles: 'Rendelési ciklusok megjelenítése vagy elrejtése az online kirakatban'
users:
legend: "Felhasználók"
email_confirmation_notice_html: "Az email megerősítés függőben van. Megerősítő emailt küldtünk a következő címre: %{email}."
@@ -1878,7 +1878,6 @@ hu:
new:
back: Vissza
description: "Hívj meg egy felhasználót, hogy regisztráljon és a vállalkozás menedzsere legyen."
eg_email_address: írd be egy új vagy meglévő felhasználó e-mail címét
email: Email
invite_new_user: Új felhasználó meghívása
invite: Meghívás
@@ -4777,16 +4776,16 @@ hu:
default_placeholder: Adj hozzá egy címkét
tag_rule_form:
tag_rules:
shipping_method_tagged_top: "Ezen címkéjű áruátadási módok:"
shipping_method_tagged_bottom: "hatása:"
payment_method_tagged_top: "Ezen címkéjű fizetési módok:"
payment_method_tagged_bottom: "hatása:"
order_cycle_tagged_top: "Ezen címkéjű rendelési ciklusok:"
order_cycle_tagged_bottom: "hatása:"
inventory_tagged_top: "Ezen címkéjű termékváltozatok:"
inventory_tagged_bottom: "hatása:"
variant_tagged_top: "Ezen címkéjű termékváltozatok:"
variant_tagged_bottom: "hatása:"
shipping_method_tagged_top: "Áruátadási módok felcímkézve"
shipping_method_tagged_bottom: "vannak:"
payment_method_tagged_top: "Fizetési módok felcímkézve"
payment_method_tagged_bottom: "vannak:"
order_cycle_tagged_top: "Rendelési ciklusok felcímkézve"
order_cycle_tagged_bottom: "vannak:"
inventory_tagged_top: "Leltár változatok felcímkézve"
inventory_tagged_bottom: "vannak:"
variant_tagged_top: "Változat felcímkézve"
variant_tagged_bottom: "vannak:"
visible: LÁTHATÓ
not_visible: NEM LÁTHATÓ
tag_rule_group_form:

View File

@@ -82,7 +82,6 @@ Openfoodnetwork::Application.routes.draw do
delete 'products_v3/:id', to: 'products_v3#destroy', as: 'product_destroy'
delete 'products_v3/destroy_variant/:id', to: 'products_v3#destroy_variant', as: 'destroy_variant'
post 'clone/:id', to: 'products_v3#clone', as: 'clone_product'
post 'products/create_linked_variant', to: 'products_v3#create_linked_variant', as: 'create_linked_variant'
resources :product_preview, only: [:show]
resources :variant_overrides do

View File

@@ -1,19 +0,0 @@
# frozen_string_literal: true
class CreateVariantLinks < ActiveRecord::Migration[7.1]
def change
# Create a join table to join two variants. One is the source of the other.
# Primary key index ensures uniqueness and assists querying. target_variant_id is the most
# likely subject and so is first in the index.
# An additional index for source_variant is also included because it may be helpful
# (https://stackoverflow.com/questions/10790518/best-sql-indexes-for-join-table).
create_table :variant_links, primary_key: [:target_variant_id, :source_variant_id] do |t|
t.integer :source_variant_id, null: false, index: true
t.integer :target_variant_id, null: false
t.datetime :created_at, null: false
end
add_foreign_key :variant_links, :spree_variants, column: :source_variant_id
add_foreign_key :variant_links, :spree_variants, column: :target_variant_id
end
end

View File

@@ -1,7 +0,0 @@
# frozen_string_literal: true
class AddHubToSpreeVariants < ActiveRecord::Migration[7.1]
def change
add_reference :spree_variants, :hub, foreign_key: { to_table: :enterprises }
end
end

View File

@@ -1009,8 +1009,6 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
t.bigint "supplier_id"
t.float "variant_unit_scale"
t.string "variant_unit_name", limit: 255
t.bigint "hub_id"
t.index ["hub_id"], name: "index_spree_variants_on_hub_id"
t.index ["primary_taxon_id"], name: "index_spree_variants_on_primary_taxon_id"
t.index ["product_id"], name: "index_variants_on_product_id"
t.index ["shipping_category_id"], name: "index_spree_variants_on_shipping_category_id"
@@ -1115,13 +1113,6 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
t.datetime "updated_at", precision: nil, null: false
end
create_table "variant_links", primary_key: ["target_variant_id", "source_variant_id"], force: :cascade do |t|
t.integer "source_variant_id", null: false
t.integer "target_variant_id", null: false
t.datetime "created_at", null: false
t.index ["source_variant_id"], name: "index_variant_links_on_source_variant_id"
end
create_table "variant_overrides", id: :serial, force: :cascade do |t|
t.integer "variant_id", null: false
t.integer "hub_id", null: false
@@ -1271,7 +1262,6 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
add_foreign_key "spree_tax_rates", "spree_zones", column: "zone_id", name: "spree_tax_rates_zone_id_fk"
add_foreign_key "spree_users", "spree_addresses", column: "bill_address_id", name: "spree_users_bill_address_id_fk"
add_foreign_key "spree_users", "spree_addresses", column: "ship_address_id", name: "spree_users_ship_address_id_fk"
add_foreign_key "spree_variants", "enterprises", column: "hub_id"
add_foreign_key "spree_variants", "enterprises", column: "supplier_id"
add_foreign_key "spree_variants", "spree_products", column: "product_id", name: "spree_variants_product_id_fk"
add_foreign_key "spree_variants", "spree_shipping_categories", column: "shipping_category_id"
@@ -1288,8 +1278,6 @@ ActiveRecord::Schema[7.1].define(version: 2026_03_06_015040) do
add_foreign_key "subscriptions", "spree_payment_methods", column: "payment_method_id", name: "subscriptions_payment_method_id_fk"
add_foreign_key "subscriptions", "spree_shipping_methods", column: "shipping_method_id", name: "subscriptions_shipping_method_id_fk"
add_foreign_key "tag_rules", "enterprises"
add_foreign_key "variant_links", "spree_variants", column: "source_variant_id"
add_foreign_key "variant_links", "spree_variants", column: "target_variant_id"
add_foreign_key "variant_overrides", "enterprises", column: "hub_id", name: "variant_overrides_hub_id_fk"
add_foreign_key "variant_overrides", "spree_variants", column: "variant_id", name: "variant_overrides_variant_id_fk"
add_foreign_key "vouchers", "enterprises"

View File

@@ -86,10 +86,6 @@ module OpenFoodNetwork
managed_and_related_enterprises_granting :manage_products
end
def enterprises_granting_linked_variants
related_enterprises_granting :create_linked_variants
end
def manages_one_enterprise?
@user.enterprises.length == 1
end

View File

@@ -1,5 +1,54 @@
---
http_interactions:
- request:
method: post
uri: https://backend.demo.taler.net/instances/sandbox/private/orders
body:
encoding: UTF-8
string: '{"order":{"amount":"KUDOS:10.0","summary":"Open Food Network order","fulfillment_url":"http://test.host/payment_gateways/taler/61"},"create_token":false}'
headers:
Authorization:
- "<HIDDEN-AUTHORIZATION-HEADER>"
Accept:
- application/json
User-Agent:
- Taler Ruby
Content-Type:
- application/json
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
response:
status:
code: 200
message: OK
headers:
Server:
- nginx/1.26.3
Date:
- Thu, 22 Jan 2026 04:43:32 GMT
Content-Type:
- application/json
Content-Length:
- '42'
Connection:
- keep-alive
Access-Control-Allow-Origin:
- "*"
Access-Control-Expose-Headers:
- "*"
Cache-Control:
- no-store
Via:
- 1.1 Caddy
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
body:
encoding: UTF-8
string: |-
{
"order_id": "2026.022-0284X4GE8WKMJ"
}
recorded_at: Thu, 22 Jan 2026 04:43:33 GMT
- request:
method: get
uri: https://backend.demo.taler.net/instances/sandbox/private/orders/2026.022-0284X4GE8WKMJ
@@ -54,108 +103,4 @@ http_interactions:
}
}
recorded_at: Thu, 22 Jan 2026 04:43:34 GMT
- request:
method: post
uri: https://backend.demo.taler.net/instances/sandbox/private/token
body:
encoding: UTF-8
string: '{"scope":"write"}'
headers:
Authorization:
- "<HIDDEN-AUTHORIZATION-HEADER>"
Accept:
- application/json
User-Agent:
- Taler Ruby
Content-Type:
- application/json
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
response:
status:
code: 200
message: OK
headers:
Server:
- nginx/1.26.3
Date:
- Fri, 20 Mar 2026 04:31:47 GMT
Content-Type:
- application/json
Content-Length:
- '258'
Connection:
- keep-alive
Access-Control-Allow-Origin:
- "*"
Access-Control-Expose-Headers:
- "*"
Cache-Control:
- no-store
Via:
- 1.1 Caddy
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
body:
encoding: ASCII-8BIT
string: |-
{
"access_token": "secret-token:J38S28NEJ6T07H1WP60F3T6PPWQYNKMR251TEZEX3CXP3SH54210",
"token": "secret-token:J38S28NEJ6T07H1WP60F3T6PPWQYNKMR251TEZEX3CXP3SH54210",
"scope": "write",
"refreshable": false,
"expiration": {
"t_s": 1774067507
}
}
recorded_at: Fri, 20 Mar 2026 04:31:48 GMT
- request:
method: post
uri: https://backend.demo.taler.net/instances/sandbox/private/orders
body:
encoding: UTF-8
string: '{"order":{"amount":"KUDOS:10.0","summary":"Open Food Network order","fulfillment_url":"http://test.host/payment_gateways/taler/198"},"create_token":false}'
headers:
Authorization:
- "<HIDDEN-AUTHORIZATION-HEADER>"
Accept:
- application/json
User-Agent:
- Taler Ruby
Content-Type:
- application/json
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
response:
status:
code: 200
message: OK
headers:
Server:
- nginx/1.26.3
Date:
- Fri, 20 Mar 2026 04:31:48 GMT
Content-Type:
- application/json
Content-Length:
- '42'
Connection:
- keep-alive
Access-Control-Allow-Origin:
- "*"
Access-Control-Expose-Headers:
- "*"
Cache-Control:
- no-store
Via:
- 1.1 Caddy
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
body:
encoding: UTF-8
string: |-
{
"order_id": "2026.079-0189PJNWMX6JA"
}
recorded_at: Fri, 20 Mar 2026 04:31:48 GMT
recorded_with: VCR 6.4.0
recorded_with: VCR 6.3.1

View File

@@ -47,61 +47,6 @@ http_interactions:
"detail": "taler-order-id:12345"
}
recorded_at: Sat, 24 Jan 2026 00:51:31 GMT
- request:
method: post
uri: https://backend.demo.taler.net/instances/sandbox/private/token
body:
encoding: UTF-8
string: '{"scope":"write"}'
headers:
Authorization:
- "<HIDDEN-AUTHORIZATION-HEADER>"
Accept:
- application/json
User-Agent:
- Taler Ruby
Content-Type:
- application/json
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
response:
status:
code: 200
message: OK
headers:
Server:
- nginx/1.26.3
Date:
- Fri, 20 Mar 2026 04:52:23 GMT
Content-Type:
- application/json
Content-Length:
- '258'
Connection:
- keep-alive
Access-Control-Allow-Origin:
- "*"
Access-Control-Expose-Headers:
- "*"
Cache-Control:
- no-store
Via:
- 1.1 Caddy
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
body:
encoding: ASCII-8BIT
string: |-
{
"access_token": "secret-token:176N5XTVVSR98FE6V4QR2Y35HKS61ZW5CK1BC7YEZYHX9M41N5GG",
"token": "secret-token:176N5XTVVSR98FE6V4QR2Y35HKS61ZW5CK1BC7YEZYHX9M41N5GG",
"scope": "write",
"refreshable": false,
"expiration": {
"t_s": 1774068743
}
}
recorded_at: Fri, 20 Mar 2026 04:52:23 GMT
- request:
method: get
uri: https://backend.demo.taler.net/instances/sandbox/private/orders/2026.020-03R3ETNZZ0DVA
@@ -125,7 +70,7 @@ http_interactions:
Server:
- nginx/1.26.3
Date:
- Fri, 20 Mar 2026 04:52:24 GMT
- Sat, 24 Jan 2026 00:55:33 GMT
Content-Type:
- application/json
Content-Length:
@@ -260,5 +205,5 @@ http_interactions:
"refund_details": [],
"order_status_url": "https://backend.demo.taler.net/instances/sandbox/orders/2026.020-03R3ETNZZ0DVA"
}
recorded_at: Fri, 20 Mar 2026 04:52:24 GMT
recorded_with: VCR 6.4.0
recorded_at: Sat, 24 Jan 2026 00:55:32 GMT
recorded_with: VCR 6.3.1

View File

@@ -16,4 +16,3 @@ describe "enterprise relationships", ->
expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "manage products"
expect(EnterpriseRelationships.permission_presentation("edit_profile")).toEqual "edit profile"
expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "add products to inventory"
expect(EnterpriseRelationships.permission_presentation("create_linked_variants")).toEqual "create linked variants [BETA]"

View File

@@ -56,7 +56,7 @@ RSpec.describe PaymentMailer do
payment = build(:payment)
payment.order.distributor = build(:enterprise, name: "Carrot Castle")
link = "https://taler.example.com/order/1"
mail = PaymentMailer.refund_available(payment.money.to_s, payment, link)
mail = PaymentMailer.refund_available(payment, link)
expect(mail.subject).to eq "Refund from Carrot Castle"
expect(mail.body).to include "Your payment of $45.75 to Carrot Castle is being refunded."

View File

@@ -364,19 +364,6 @@ RSpec.describe Spree::Ability do
for: p2.variants.first)
end
describe "create_linked_variant" do
it "should not be able to create linked variant without permission" do
is_expected.not_to have_ability([:create_linked_variant], for: p_related.variants.first)
end
it "should be able to create linked variant when granted permission" do
create(:enterprise_relationship, parent: s_related, child: s1,
permissions_list: [:create_linked_variants])
is_expected.to have_ability([:create_linked_variant], for: p_related.variants.first)
end
end
it "should not be able to access admin actions on orders" do
is_expected.not_to have_ability([:admin], for: Spree::Order)
end
@@ -742,19 +729,6 @@ RSpec.describe Spree::Ability do
it "can request permitted enterprise fees for an order cycle" do
is_expected.to have_ability([:for_order_cycle], for: EnterpriseFee)
end
describe "create_linked_variant" do
it "should not be able to create linked variant without permission" do
is_expected.not_to have_ability([:create_linked_variant], for: p_related.variants.first)
end
it "should be able to create linked variant when granted permission" do
create(:enterprise_relationship, parent: s_related, child: d1,
permissions_list: [:create_linked_variants])
is_expected.to have_ability([:create_linked_variant], for: p_related.variants.first)
end
end
end
context 'Order Cycle co-ordinator, distributor enterprise manager' do
@@ -830,19 +804,6 @@ RSpec.describe Spree::Ability do
it "has the ability to manage vouchers" do
is_expected.to have_ability([:admin, :create], for: Voucher)
end
describe "create_linked_variant for own enterprise" do
it "should not be able to create own sourced variant without permission" do
is_expected.not_to have_ability([:create_linked_variant], for: p1.variants.first)
end
it "should be able to create own sourced variant when granted self permission" do
create(:enterprise_relationship, parent: s1, child: s1,
permissions_list: [:create_linked_variants])
is_expected.to have_ability([:create_linked_variant], for: p1.variants.first)
end
end
end
context 'enterprise owner' do

View File

@@ -21,6 +21,53 @@ RSpec.describe Spree::CreditCard do
let(:credit_card) { described_class.new }
context "#can_capture?" do
it "should be true if payment is pending" do
payment = build_stubbed(:payment, created_at: Time.zone.now)
allow(payment).to receive(:pending?) { true }
expect(credit_card.can_capture_and_complete_order?(payment)).to be_truthy
end
it "should be true if payment is checkout" do
payment = build_stubbed(:payment, created_at: Time.zone.now)
allow(payment).to receive_messages pending?: false,
checkout?: true
expect(credit_card.can_capture_and_complete_order?(payment)).to be_truthy
end
end
context "#can_void?" do
it "should be true if payment is not void" do
payment = build_stubbed(:payment)
allow(payment).to receive(:void?) { false }
expect(credit_card.can_void?(payment)).to be_truthy
end
end
context "#can_credit?" do
it "should be false if payment is not completed" do
payment = build_stubbed(:payment)
allow(payment).to receive(:completed?) { false }
expect(credit_card.can_credit?(payment)).to be_falsy
end
it "should be false when order payment_state is not 'credit_owed'" do
payment = build_stubbed(:payment,
order: create(:order, payment_state: 'paid'))
allow(payment).to receive(:completed?) { true }
expect(credit_card.can_credit?(payment)).to be_falsy
end
it "should be false when credit_allowed is zero" do
payment = build_stubbed(:payment,
order: create(:order, payment_state: 'credit_owed'))
allow(payment).to receive_messages completed?: true,
credit_allowed: 0
expect(credit_card.can_credit?(payment)).to be_falsy
end
end
context "#valid?" do
it "should validate presence of number" do
credit_card.attributes = valid_credit_card_attributes.except(:number)

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true
RSpec.describe Spree::Gateway do
subject(:gateway) { test_gateway.new }
let(:test_gateway) do
Class.new(Spree::Gateway) do
def provider_class
@@ -16,58 +15,13 @@ RSpec.describe Spree::Gateway do
it "passes through all arguments on a method_missing call" do
expect(Rails.env).to receive(:local?).and_return(false)
gateway = test_gateway.new
expect(gateway.provider).to receive(:imaginary_method).with('foo')
gateway.imaginary_method('foo')
end
it "raises an error in test env" do
gateway = test_gateway.new
expect { gateway.imaginary_method('foo') }.to raise_error StandardError
end
describe "#can_capture?" do
it "should be true if payment is pending" do
payment = build_stubbed(:payment, created_at: Time.zone.now)
allow(payment).to receive(:pending?) { true }
expect(gateway.can_capture_and_complete_order?(payment)).to be_truthy
end
it "should be true if payment is checkout" do
payment = build_stubbed(:payment, created_at: Time.zone.now)
allow(payment).to receive_messages pending?: false,
checkout?: true
expect(gateway.can_capture_and_complete_order?(payment)).to be_truthy
end
end
describe "#can_void?" do
it "should be true if payment is not void" do
payment = build_stubbed(:payment)
allow(payment).to receive(:void?) { false }
expect(gateway.can_void?(payment)).to be_truthy
end
end
describe "#can_credit?" do
it "should be false if payment is not completed" do
payment = build_stubbed(:payment)
allow(payment).to receive(:completed?) { false }
expect(gateway.can_credit?(payment)).to be_falsy
end
it "should be false when order payment_state is not 'credit_owed'" do
payment = build_stubbed(:payment,
order: create(:order, payment_state: 'paid'))
allow(payment).to receive(:completed?) { true }
expect(gateway.can_credit?(payment)).to be_falsy
end
it "should be false when credit_allowed is zero" do
payment = build_stubbed(:payment,
order: create(:order, payment_state: 'credit_owed'))
allow(payment).to receive_messages completed?: true,
credit_allowed: 0
expect(gateway.can_credit?(payment)).to be_falsy
end
end
end

View File

@@ -10,38 +10,16 @@ RSpec.describe Spree::PaymentMethod::Taler do
)
}
let(:backend_url) { "https://backend.demo.taler.net/instances/sandbox" }
let(:token_url) { "#{backend_url}/private/token" }
describe "#external_payment_url" do
it "creates an order reference and retrieves a URL to pay at", vcr: true do
describe "#external_payment_url", vcr: true do
it "creates an order reference and retrieves a URL to pay at" do
order = create(:order_ready_for_confirmation, payment_method: taler)
url = subject.external_payment_url(order:)
expect(url).to start_with "#{backend_url}/orders/"
expect(url).to match "orders/20...[0-9A-Z-]{17}$"
expect(url).to eq "#{backend_url}/orders/2026.022-0284X4GE8WKMJ"
payment = order.payments.last.reload
expect(payment.response_code).to match "20...[0-9A-Z-]{17}$"
end
it "creates the Taler order with the right currency" do
order = create(:order_ready_for_confirmation, payment_method: taler)
backend_url = "https://taler.example.com"
token_url = "https://taler.example.com/private/token"
order_url = "https://taler.example.com/private/orders"
taler = Spree::PaymentMethod::Taler.new(
preferred_backend_url: "https://taler.example.com",
preferred_api_key: "sandbox",
)
stub_request(:post, token_url).to_return(body: { token: "1234" }.to_json)
stub_request(:post, order_url)
.with(body: /"amount":"AUD:10.0"/)
.to_return(body: { order_id: "one" }.to_json)
url = taler.external_payment_url(order:)
expect(url).to eq "#{backend_url}/orders/one"
expect(payment.response_code).to match "2026.022-0284X4GE8WKMJ"
end
end
@@ -51,10 +29,6 @@ RSpec.describe Spree::PaymentMethod::Taler do
let(:payment) { build(:payment, response_code: "taler-order-7") }
let(:order_url) { "#{backend_url}/private/orders/taler-order-7" }
before do
stub_request(:post, token_url).to_return(body: { token: "12345" }.to_json)
end
it "returns an ActiveMerchant response" do
order_status = "paid"
stub_request(:get, order_url).to_return(body: { order_status: }.to_json)
@@ -76,50 +50,6 @@ RSpec.describe Spree::PaymentMethod::Taler do
end
end
describe "#credit" do
let(:order_endpoint) { "#{backend_url}/private/orders/taler-order-8" }
let(:refund_endpoint) { "#{order_endpoint}/refund" }
let(:taler_refund_uri) {
"taler://refund/backend.demo.taler.net/instances/sandbox/taler-order-8/"
}
before do
stub_request(:post, token_url).to_return(body: { token: "12345" }.to_json)
end
it "starts the refund process" do
order_status = { order_status: "paid" }
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
stub_request(:post, refund_endpoint).to_return(body: { taler_refund_uri: }.to_json)
order = create(:completed_order_with_totals)
order.payments.create(
amount: order.total, state: :completed,
payment_method: taler,
response_code: "taler-order-8",
)
expect {
response = taler.credit(100, "taler-order-8", { payment: order.payments[0] })
expect(response.success?).to eq true
}.to enqueue_mail(PaymentMailer, :refund_available)
end
it "raises an error if payment hasn't been taken yet" do
order_status = { order_status: "claimed" }
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
order = create(:completed_order_with_totals)
order.payments.create(
amount: order.total, state: :completed,
payment_method: taler,
response_code: "taler-order-8",
)
expect {
taler.credit(100, "taler-order-8", { payment: order.payments[0] })
}.to raise_error StandardError, "Unsupported action"
end
end
describe "#void" do
let(:order_endpoint) { "#{backend_url}/private/orders/taler-order-8" }
let(:refund_endpoint) { "#{order_endpoint}/refund" }
@@ -127,10 +57,6 @@ RSpec.describe Spree::PaymentMethod::Taler do
"taler://refund/backend.demo.taler.net/instances/sandbox/taler-order-8/"
}
before do
stub_request(:post, token_url).to_return(body: { token: "12345" }.to_json)
end
it "starts the refund process" do
order_status = {
order_status: "paid",

View File

@@ -855,8 +855,7 @@ RSpec.describe Spree::Payment do
describe "available actions" do
context "for most gateways" do
let(:payment) { build_stubbed(:payment, payment_method:) }
let(:payment_method) { Spree::Gateway::StripeSCA.new }
let(:payment) { build_stubbed(:payment, source: build_stubbed(:credit_card)) }
it "can capture and void" do
expect(payment.actions).to match_array %w(capture_and_complete_order void)

View File

@@ -8,7 +8,6 @@ RSpec.describe Spree::Variant do
it { is_expected.to have_many :semantic_links }
it { is_expected.to belong_to(:product).required }
it { is_expected.to belong_to(:supplier).required }
it { is_expected.to belong_to(:hub).optional }
it { is_expected.to have_many(:inventory_units) }
it { is_expected.to have_many(:line_items) }
it { is_expected.to have_many(:stock_items) }
@@ -21,9 +20,6 @@ RSpec.describe Spree::Variant do
it { is_expected.to have_many(:inventory_items) }
it { is_expected.to have_many(:supplier_properties).through(:supplier) }
it { is_expected.to have_many(:source_variants).through(:variant_links_as_target) }
it { is_expected.to have_many(:target_variants).through(:variant_links_as_source) }
describe "shipping category" do
it "sets a shipping category if none provided" do
variant = build(:variant, shipping_category: nil)
@@ -1005,30 +1001,4 @@ RSpec.describe Spree::Variant do
expect(variant.unit_presentation).to eq "My display"
end
end
describe "#create_linked_variant" do
let(:user) { create(:user, enterprises: [enterprise]) }
let(:supplier) { variant.supplier }
let(:enterprise) { create(:enterprise) }
context "with create_linked_variants permissions on supplier" do
let!(:enterprise_relationship) {
create(:enterprise_relationship,
parent: supplier,
child: enterprise,
permissions_list: [:create_linked_variants])
}
let(:variant) { create(:variant, price: 10.95, on_demand: false, on_hand: 5) }
it "clones the variant, retaining a link to the source" do
linked_variant = variant.create_linked_variant(user)
expect(linked_variant.source_variants).to eq [variant]
expect(linked_variant.hub).to eq enterprise
expect(linked_variant.price).to eq 10.95
expect(linked_variant.on_demand).to eq false
expect(linked_variant.on_hand).to eq 5
end
end
end
end

View File

@@ -59,71 +59,4 @@ RSpec.describe "Admin::ProductsV3" do
expect(response).to redirect_to('/unauthorized')
end
end
describe "POST /admin/products/create_linked_variant" do
let(:enterprise) { create(:supplier_enterprise) }
let(:user) { create(:user, enterprises: [enterprise]) }
let(:supplier) { create(:supplier_enterprise) }
let(:variant) { create(:variant, display_name: "Original variant", supplier: supplier) }
before do
sign_in user
end
it "checks for permission" do
params = { variant_id: variant.id, product_index: 1 }
expect {
post(admin_create_linked_variant_path, as: :turbo_stream, params:)
expect(response).to redirect_to('/unauthorized')
}.not_to change { variant.product.variants.count }
end
context "With create_linked_variants permissions on supplier" do
let!(:enterprise_relationship) {
create(:enterprise_relationship,
parent: supplier,
child: enterprise,
permissions_list: [:create_linked_variants])
}
it "clones the variant, retaining link as source" do
params = { variant_id: variant.id, product_index: 1 }
expect {
post(admin_create_linked_variant_path, as: :turbo_stream, params:)
expect(response).to have_http_status(:ok)
expect(response.body).to match "Original variant" # cloned variant name
}.to change { variant.product.variants.count }.by(1)
new_variant = variant.product.variants.order(:id).last
# The new variant is a target of the original. It is a "sourced" variant.
expect(variant.target_variants.first).to eq new_variant
# The new variant's source is the original
expect(new_variant.source_variants.first).to eq variant
end
context "and I'm also owner of another enterprise" do
let!(:enterprise2) { create(:enterprise) }
let(:user) { create(:user, enterprises: [enterprise, enterprise2]) }
it "clones the variant, owned by my enterprise that has permission" do
enterprise2.owner = user
params = { variant_id: variant.id, product_index: 1 }
expect {
post(admin_create_linked_variant_path, as: :turbo_stream, params:)
expect(response).to have_http_status(:ok)
}.to change { variant.product.variants.count }.by(1)
# The new variant is owned by my enterprise that has permission, not the other one
new_variant = variant.product.variants.order(:id).last
expect(new_variant.hub).to eq enterprise
end
end
end
end
end

View File

@@ -157,6 +157,8 @@ RSpec.describe Spree::Admin::PaymentsController do
context "with no payment source" do
it "redirect to payments page" do
allow(payment).to receive(:payment_source).and_return(nil)
put(
"/admin/orders/#{order.number}/payments/#{order.payments.first.id}/fire?e=void",
params: {},

View File

@@ -42,7 +42,6 @@ RSpec.describe Payments::WebhookPayload do
}
},
order: {
number: order.number,
total: order.total,
currency: order.currency,
line_items: line_items
@@ -73,7 +72,6 @@ RSpec.describe Payments::WebhookPayload do
}
},
order: {
number: "R555555555",
total: 0.00,
currency: "AUD",
line_items: [

View File

@@ -56,7 +56,6 @@ RSpec.describe Payments::WebhookService do
}
},
order: {
number: order.number,
total: order.total,
currency: order.currency,
line_items: line_items

View File

@@ -47,23 +47,18 @@ create(:enterprise)
uncheck 'to manage products'
check 'to edit profile'
check 'to add products to inventory'
check 'to create linked variants'
select2_select 'Two', from: 'enterprise_relationship_child_id'
click_button 'Create'
# Wait for row to appear since have_relationship doesn't wait
expect(page).to have_selector 'tr', count: 2
# Permissions appear.. in a different order for some reason.
expect_relationship_with_permissions e1, e2,
['to add to order cycle',
'to create linked variants [BETA]',
'to add products to inventory',
'to edit profile']
'to add products to inventory', 'to edit profile']
er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first
expect(er).to be_present
expect(er.permissions.map(&:name)).to match_array ['add_to_order_cycle', 'edit_profile',
'create_variant_overrides',
'create_linked_variants']
'create_variant_overrides']
end
it "attempting to create a relationship with invalid data" do

View File

@@ -885,47 +885,6 @@ RSpec.describe '
end
end
end
describe "removing enterprise managers" do
let(:existing_user) { create(:user) }
before do
distributor1.users << existing_user
login_as logged_in_user
visit edit_admin_enterprise_path(distributor1)
scroll_to(:bottom)
within ".side_menu" do
find(:link, "Users").trigger("click")
end
end
context "as the enterprise owner" do
let(:logged_in_user) { distributor1.owner }
it 'removes the manager as enterprise owner' do
expect(page).to have_content existing_user.email
within "#manager-#{existing_user.id}" do
accept_confirm do
page.find("a.icon-trash").click
end
end
expect(page).not_to have_content existing_user.email
end
end
context "as the enterprise manager" do
let(:logged_in_user) { existing_user }
it "is unable delete any other manager" do
expect(page).to have_content existing_user.email
within('.edit_enterprise') do
expect(page).not_to have_selector('a.icon-trash')
end
end
end
end
end
context "changing package" do

View File

@@ -25,17 +25,15 @@ RSpec.describe "Admin -> Order -> Payments" do
login_as distributor.owner
end
it "allows to void a Taler payment" do
it "allows to refund a Taler payment" do
order_status = {
order_status: "paid",
contract_terms: {
amount: "KUDOS:2",
}
}
token_endpoint = "https://taler.example.com/private/token"
order_endpoint = "https://taler.example.com/private/orders/taler-id-1"
refund_endpoint = "https://taler.example.com/private/orders/taler-id-1/refund"
stub_request(:post, token_endpoint).to_return(body: { token: "abc" }.to_json)
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
stub_request(:post, refund_endpoint).to_return(body: "{}")
@@ -51,36 +49,4 @@ RSpec.describe "Admin -> Order -> Payments" do
expect(page).not_to have_link "Void"
end
end
it "allows to credit a Taler payment" do
order_status = {
order_status: "paid",
contract_terms: {
amount: "KUDOS:2",
}
}
token_endpoint = "https://taler.example.com/private/token"
order_endpoint = "https://taler.example.com/private/orders/taler-id-1"
refund_endpoint = "https://taler.example.com/private/orders/taler-id-1/refund"
stub_request(:post, token_endpoint).to_return(body: { token: "abc" }.to_json)
stub_request(:get, order_endpoint).to_return(body: order_status.to_json)
stub_request(:post, refund_endpoint).to_return(body: "{}")
visit spree.admin_order_payments_path(order.number)
within row_containing("Taler") do
expect(page).to have_text "COMPLETED"
expect(page).to have_link "Credit"
click_link class: "icon-credit"
expect(page).to have_text "COMPLETED"
expect(page).not_to have_link "Credit"
end
# Our payment system creates a new payment to show the credit.
within row_containing("$-9.75") do
expect(page).not_to have_link "Void"
end
end
end

View File

@@ -2,7 +2,7 @@
require "system_helper"
RSpec.describe 'As an enterprise user, I can perform actions on the products screen' do
RSpec.describe 'As an enterprise user, I can manage my products' do
include AdminHelper
include WebHelper
include AuthenticationHelper
@@ -19,6 +19,18 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
let(:categories_search_selector) { 'input[placeholder="Select category"]' }
let(:tax_categories_search_selector) { 'input[placeholder="Search for tax categories"]' }
describe "with no products" do
before { visit admin_products_url }
it "can see the new product page" do
expect(page).to have_content "Bulk Edit Products"
expect(page).to have_text "No products found"
# displays buttons to add products with the correct links
expect(page).to have_link(class: "button", text: "New Product", href: "/admin/products/new")
expect(page).to have_link(class: "button", text: "Import multiple products",
href: admin_product_import_path)
end
end
describe "column selector" do
let!(:product) { create(:simple_product) }
@@ -93,6 +105,8 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
end
end
describe "columns"
describe "Changing producers, category and tax category" do
let!(:variant_a1) {
product_a.variants.first.tap{ |v|
@@ -246,24 +260,24 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
describe "Cloning product" do
it "shows the cloned product on page when clicked on the cloned option" do
# TODO, variant supplier missing, needs to be copied from variant and not product
within "table.products" do
# Gather input values, because page.content doesn't include them.
input_content = page.find_all('input[type=text]').map(&:value).join
# Products does not include the cloned product.
expect(input_content).not_to match /COPY OF Apples/
end
click_product_clone "Apples"
expect(page).to have_content "Successfully cloned the product"
within "table.products" do
# Product list includes the cloned product.
expect(all_input_values).to match /COPY OF Apples/
# Gather input values, because page.content doesn't include them.
input_content = page.find_all('input[type=text]').map(&:value).join
# And I can perform actions on the new product
within row_containing_name "COPY OF Apples" do
page.find(".vertical-ellipsis-menu").click
expect(page).to have_link "Edit"
expect(page).to have_link "Clone"
# expect(page).to have_link "Delete" # it's not a proper link :/
fill_in "Name", with: "My copy of Apples"
end
click_button "Save changes"
# Products include the cloned product.
expect(input_content).to match /COPY OF Apples/
end
end
end
@@ -284,88 +298,6 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
end
end
describe "Create linked variant" do
let!(:variant) {
create(:variant, display_name: "My box", supplier: producer)
}
let!(:other_producer) { create(:supplier_enterprise) }
let!(:other_variant) {
create(:variant, display_name: "My friends box", supplier: other_producer)
}
let!(:enterprise_relationship) {
# Other producer grants me access to manage their variant
create(:enterprise_relationship, parent: other_producer, child: producer,
permissions_list: [:manage_products])
}
context "with create_linked_variants permission for my, and other's variants" do
it "creates a linked variant" do
create(:enterprise_relationship, parent: producer, child: producer,
permissions_list: [:create_linked_variants])
enterprise_relationship.permissions.create! name: :create_linked_variants
visit admin_products_url
# Check my own variant
within row_containing_name("My box") do
page.find(".vertical-ellipsis-menu").click
expect(page).to have_link "Create linked variant"
end
# Create linked variant sourced from my friend
within row_containing_name("My friends box") do
page.find(".vertical-ellipsis-menu").click
click_link "Create linked variant"
end
expect(page).to have_content "Successfully created linked variant"
within "table.products" do
# There are now two copies
expect(all_input_values).to match /My friends box.*My friends box/
# One of them is designated as a linked variant
expect(page).to have_content "🔗"
last_box = page.all(row_containing_name("My friends box")).last
# Close action menu (shouldn't need this, it should close itself)
last_box.click
# And I can perform actions on the new product
within last_box do
page.find(".vertical-ellipsis-menu").click
expect(page).to have_link "Edit"
# expect(page).to have_link "Clone" # tofix: menu is partially obscured
# expect(page).to have_link "Delete" # it's not a proper link
fill_in "Name", with: "My copy of Apples"
end
click_button "Save changes"
# initially obscured by the previous message, then disappears before capybara sees it.
# expect(page).to have_content "Changes saved"
end
end
end
context "without create_linked_variants permission" do
it "does not show the option in the menu" do
visit admin_products_url
within row_containing_name("My box") do
page.find(".vertical-ellipsis-menu").click
expect(page).not_to have_link "Create linked variant"
end
within row_containing_name("My friends box") do
page.find(".vertical-ellipsis-menu").click
expect(page).not_to have_link "Create linked variant"
end
end
end
end
describe "delete" do
let!(:product_a) { create(:simple_product, name: "Apples", sku: "APL-00") }
let(:delete_option_selector) { "a[data-controller='modal-link'].delete" }
@@ -595,6 +527,90 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
end
end
context "as an enterprise manager" do
let(:supplier_managed1) { create(:supplier_enterprise, name: 'Supplier Managed 1') }
let(:supplier_managed2) { create(:supplier_enterprise, name: 'Supplier Managed 2') }
let(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Supplier Unmanaged') }
let(:supplier_permitted) { create(:supplier_enterprise, name: 'Supplier Permitted') }
let(:distributor_managed) { create(:distributor_enterprise, name: 'Distributor Managed') }
let(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Distributor Unmanaged') }
let!(:product_supplied) { create(:product, supplier_id: supplier_managed1.id, price: 10.0) }
let!(:product_not_supplied) { create(:product, supplier_id: supplier_unmanaged.id) }
let!(:product_supplied_permitted) {
create(:product, name: 'Product Permitted', supplier_id: supplier_permitted.id, price: 10.0)
}
let(:product_supplied_inactive) {
create(:product, supplier_id: supplier_managed1.id, price: 10.0)
}
let!(:supplier_permitted_relationship) do
create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed1,
permissions_list: [:manage_products])
end
before do
enterprise_user = create(:user)
enterprise_user.enterprise_roles.build(enterprise: supplier_managed1).save
enterprise_user.enterprise_roles.build(enterprise: supplier_managed2).save
enterprise_user.enterprise_roles.build(enterprise: distributor_managed).save
login_as enterprise_user
end
it "shows only products that I supply" do
visit spree.admin_products_path
# displays permitted product list only
expect(page).to have_selector row_containing_name(product_supplied.name)
expect(page).to have_selector row_containing_name(product_supplied_permitted.name)
expect(page).not_to have_selector row_containing_name(product_not_supplied.name)
end
it "shows only suppliers that I manage or have permission to" do
visit spree.admin_products_path
within row_containing_placeholder(product_supplied.name) do
expect(page).to have_select(
'_products_0_variants_attributes_0_supplier_id',
options: [
'Select producer',
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
], selected: supplier_managed1.name
)
end
within row_containing_placeholder(product_supplied_permitted.name) do
expect(page).to have_select(
'_products_1_variants_attributes_0_supplier_id',
options: [
'Select producer',
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
], selected: supplier_permitted.name
)
end
end
it "shows inactive products that I supply" do
product_supplied_inactive
visit spree.admin_products_path
expect(page).to have_selector row_containing_name(product_supplied_inactive.name)
end
it "allows me to update a product" do
visit spree.admin_products_path
within row_containing_name(product_supplied.name) do
fill_in "Name", with: "Pommes"
end
click_button "Save changes"
expect(page).to have_content "Changes saved"
expect(page).to have_selector row_containing_name("Pommes")
end
end
def open_action_menu
page.find(".vertical-ellipsis-menu").click
end

View File

@@ -2,13 +2,13 @@
require "system_helper"
RSpec.describe 'As an enterprise user, I can browse my products' do
RSpec.describe 'As an enterprise user, I can manage my products' do
include AdminHelper
include WebHelper
include AuthenticationHelper
include FileHelper
let(:producer) { create(:supplier_enterprise, name: "My Enterprise") }
let(:producer) { create(:supplier_enterprise) }
let(:user) { create(:user, enterprises: [producer]) }
before do
@@ -19,25 +19,15 @@ RSpec.describe 'As an enterprise user, I can browse my products' do
let(:categories_search_selector) { 'input[placeholder="Search for categories"]' }
let(:tax_categories_search_selector) { 'input[placeholder="Search for tax categories"]' }
describe "with no products" do
before { visit admin_products_url }
it "can see the new product page" do
expect(page).to have_content "Bulk Edit Products"
expect(page).to have_text "No products found"
# displays buttons to add products with the correct links
expect(page).to have_link(class: "button", text: "New Product", href: "/admin/products/new")
expect(page).to have_link(class: "button", text: "Import multiple products",
href: admin_product_import_path)
end
end
describe "listing" do
let!(:p1) { create(:product, name: "Product1") }
let!(:p2) { create(:product, name: "Product2") }
it "displays a list of products" do
visit admin_products_path
before do
visit admin_products_url
end
it "displays a list of products" do
within ".products" do
# displays table header
expect(page).to have_selector "th", text: "Name"
@@ -139,34 +129,6 @@ RSpec.describe 'As an enterprise user, I can browse my products' do
expect(page).to have_select "variant_unit_with_scale", selected: "Items"
expect(page).to have_field "variant_unit_name", with: "packet"
end
context "with sourced variant" do
let(:source_producer) { create(:supplier_enterprise) }
let(:p3) { create(:product, name: "Product3", supplier_id: source_producer.id) }
let!(:v3_source) { p3.variants.first }
let!(:v3_sourced) {
create(:variant, display_name: "Variant3-sourced", product: p3, supplier: source_producer,
hub: producer)
}
let!(:enterprise_relationship) {
# Other producer grants me access to manage their variant
create(:enterprise_relationship, parent: source_producer, child: producer,
permissions_list: [:manage_products])
}
before do
v3_sourced.source_variants << v3_source
visit admin_products_url
end
it "shows sourced variant with indicator" do
within row_containing_name("Variant3-sourced") do
expect(page).to have_selector 'span[title*="Sourced from: "]'
expect(page).to have_selector 'span[title*="Hub: My Enterprise"]'
end
end
end
end
describe "sorting" do
@@ -501,88 +463,4 @@ RSpec.describe 'As an enterprise user, I can browse my products' do
end
end
end
context "as an enterprise manager" do
let(:supplier_managed1) { create(:supplier_enterprise, name: 'Supplier Managed 1') }
let(:supplier_managed2) { create(:supplier_enterprise, name: 'Supplier Managed 2') }
let(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Supplier Unmanaged') }
let(:supplier_permitted) { create(:supplier_enterprise, name: 'Supplier Permitted') }
let(:distributor_managed) { create(:distributor_enterprise, name: 'Distributor Managed') }
let(:distributor_unmanaged) { create(:distributor_enterprise, name: 'Distributor Unmanaged') }
let!(:product_supplied) { create(:product, supplier_id: supplier_managed1.id, price: 10.0) }
let!(:product_not_supplied) { create(:product, supplier_id: supplier_unmanaged.id) }
let!(:product_supplied_permitted) {
create(:product, name: 'Product Permitted', supplier_id: supplier_permitted.id, price: 10.0)
}
let(:product_supplied_inactive) {
create(:product, supplier_id: supplier_managed1.id, price: 10.0)
}
let!(:supplier_permitted_relationship) do
create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed1,
permissions_list: [:manage_products])
end
before do
enterprise_user = create(:user)
enterprise_user.enterprise_roles.build(enterprise: supplier_managed1).save
enterprise_user.enterprise_roles.build(enterprise: supplier_managed2).save
enterprise_user.enterprise_roles.build(enterprise: distributor_managed).save
login_as enterprise_user
end
it "shows only products that I supply" do
visit spree.admin_products_path
# displays permitted product list only
expect(page).to have_selector row_containing_name(product_supplied.name)
expect(page).to have_selector row_containing_name(product_supplied_permitted.name)
expect(page).not_to have_selector row_containing_name(product_not_supplied.name)
end
it "shows only suppliers that I manage or have permission to" do
visit spree.admin_products_path
within row_containing_placeholder(product_supplied.name) do
expect(page).to have_select(
'_products_0_variants_attributes_0_supplier_id',
options: [
'Select producer',
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
], selected: supplier_managed1.name
)
end
within row_containing_placeholder(product_supplied_permitted.name) do
expect(page).to have_select(
'_products_1_variants_attributes_0_supplier_id',
options: [
'Select producer',
supplier_managed1.name, supplier_managed2.name, supplier_permitted.name
], selected: supplier_permitted.name
)
end
end
it "shows inactive products that I supply" do
product_supplied_inactive
visit spree.admin_products_path
expect(page).to have_selector row_containing_name(product_supplied_inactive.name)
end
it "allows me to update a product" do
visit spree.admin_products_path
within row_containing_name(product_supplied.name) do
fill_in "Name", with: "Pommes"
end
click_button "Save changes"
expect(page).to have_content "Changes saved"
expect(page).to have_selector row_containing_name("Pommes")
end
end
end

View File

@@ -370,7 +370,6 @@ RSpec.describe "As a consumer, I want to checkout my order" do
Spree::PaymentMethod::Taler.create!(
name: "Taler",
environment: "test",
preferred_backend_url: "https://taler.example.com/",
distributors: [distributor]
)
end

View File

@@ -6,13 +6,13 @@ RSpec.describe "registration/steps/_details.html.haml" do
it "uses Google Maps when it is enabled" do
allow(view).to receive_messages(using_google_maps?: true)
is_expected.to match /<ui-gmap-google-map center="map.center" zoom="map.zoom">/
is_expected.to match /<ui-gmap-google-map center='map.center' zoom='map.zoom'>/
end
it "uses OpenStreetMap when it is enabled" do
ContentConfig.open_street_map_enabled = true
allow(view).to receive_messages(using_google_maps?: false)
is_expected.to match /<div class="map-container--registration" id="open-street-map">/
is_expected.to match /<div class='map-container--registration' id='open-street-map'>/
end
end

View File

@@ -816,9 +816,9 @@
"@babel/helper-plugin-utils" "^7.28.6"
"@babel/preset-env@^7.28.5":
version "7.29.2"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.2.tgz#5a173f22c7d8df362af1c9fe31facd320de4a86c"
integrity sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.0.tgz#c55db400c515a303662faaefd2d87e796efa08d0"
integrity sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==
dependencies:
"@babel/compat-data" "^7.29.0"
"@babel/helper-compilation-targets" "^7.28.6"
@@ -900,10 +900,15 @@
"@babel/types" "^7.4.4"
esutils "^2.0.2"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.28.4", "@babel/runtime@^7.8.4":
version "7.29.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e"
integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==
"@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
"@babel/runtime@^7.28.4":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b"
integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==
"@babel/template@^7.28.6":
version "7.28.6"
@@ -5270,9 +5275,9 @@ node-addon-api@^7.0.0:
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
node-forge@^1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2"
integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==
version "1.3.3"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
node-int64@^0.4.0:
version "0.4.0"
@@ -5477,14 +5482,14 @@ picocolors@1.1.1, picocolors@^1.1.1:
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601"
integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.2, picomatch@^4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
pify@^2.3.0:
version "2.3.0"