Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
b2989db27c Bump datafoodconsortium-connector from 1.2.0 to 1.3.0
Bumps [datafoodconsortium-connector](https://github.com/datafoodconsortium/connector-ruby) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/datafoodconsortium/connector-ruby/releases)
- [Changelog](https://github.com/datafoodconsortium/connector-ruby/blob/main/CHANGELOG.md)
- [Commits](https://github.com/datafoodconsortium/connector-ruby/compare/v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: datafoodconsortium-connector
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-20 09:34:29 +00:00
299 changed files with 3363 additions and 4909 deletions

View File

@@ -27,12 +27,7 @@ assignees: ''
- [ ] Move this issue to Test Ready.
- [ ] Notify `@testers` in [#testing].
- [ ] Test build: [Deploy to Staging] with release tag.
- [ ] Map is displayed correctly. Address changes are reflected in the map.
- [ ] Stripe with no authentication card: `4242424242424242` as shopper and as Admin. Order confirmation displays order as "Paid".
- [ ] Stripe with Authentication required card: `4000002760003184` as shopper and as Admin. As admin, check authorization through customer account `/account#/transactions` and email.
- [ ] Pay with Paypal.
- [ ] Order on mobile.
- [ ] Notify a deployer to deploy it.
- [ ] Notify a deployer to deploy it
## 3. Deployment at beginning of week
@@ -62,4 +57,4 @@ The full process is described at https://github.com/openfoodfoundation/openfoodn
[Create issue]: https://github.com/openfoodfoundation/openfoodnetwork/issues/new?assignees=&labels=&projects=&template=release.md&title=Release
[#delivery-circle]: https://openfoodnetwork.slack.com/archives/C01T75H6G0Z
[Transifex Client]: https://developers.transifex.com/docs/cli
[minor or major breaking changes]: https://github.com/openfoodfoundation/openfoodnetwork/pulls?q=label%3A%22breaking+change%22%2C%22major+breaking+change%22
[minor or major breaking changes]: https://github.com/openfoodfoundation/openfoodnetwork/pulls?q=label%3A%22breaking+change%22%2C%22major+breaking+change%22

View File

@@ -203,7 +203,6 @@ group :development do
gem 'spring'
gem 'spring-commands-rspec'
gem 'spring-commands-rubocop'
gem 'spring-watcher-listen'
gem 'web-console'
gem 'rack-mini-profiler', '< 3.0.0'

View File

@@ -170,7 +170,7 @@ GEM
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
aes_key_wrap (1.1.0)
afm (1.0.0)
afm (0.2.2)
angular-rails-templates (1.4.0)
railties (>= 5.0, < 8.2)
sprockets (>= 3.0, < 5)
@@ -267,16 +267,16 @@ GEM
css_parser (1.21.1)
addressable
csv (3.3.5)
cuprite (0.17)
cuprite (0.15)
capybara (~> 3.0)
ferrum (~> 0.17.0)
ferrum (~> 0.14.0)
database_cleaner (2.1.0)
database_cleaner-active_record (>= 2, < 3)
database_cleaner-active_record (2.2.2)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0)
database_cleaner-core (2.0.1)
datafoodconsortium-connector (1.2.0)
datafoodconsortium-connector (1.3.0)
virtual_assembly-semantizer (~> 1.0, >= 1.0.5)
date (3.5.1)
debug (1.11.0)
@@ -313,9 +313,9 @@ GEM
eventmachine (>= 1.0.0.beta.1)
email_validator (2.2.4)
activemodel
erb (6.0.2)
erb (6.0.1)
erubi (1.13.1)
et-orbi (1.4.0)
et-orbi (1.3.0)
tzinfo
eventmachine (1.2.7)
eventmachine_httpserver (0.2.1)
@@ -334,22 +334,21 @@ GEM
faraday (>= 1, < 3)
faraday-net_http (3.4.2)
net-http (~> 0.5)
ferrum (0.17.1)
ferrum (0.14)
addressable (~> 2.5)
base64 (~> 0.2)
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
websocket-driver (>= 0.6, < 0.8)
ffaker (2.25.0)
ffi (1.17.3)
flipper (1.4.0)
flipper (1.3.6)
concurrent-ruby (< 2)
flipper-active_record (1.4.0)
flipper-active_record (1.3.6)
activerecord (>= 4.2, < 9)
flipper (~> 1.4.0)
flipper-ui (1.4.0)
flipper (~> 1.3.6)
flipper-ui (1.3.6)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 1.4.0)
flipper (~> 1.3.6)
rack (>= 1.4, < 4)
rack-protection (>= 1.5.3, < 5.0.0)
rack-session (>= 1.0.2, < 3.0.0)
@@ -372,8 +371,8 @@ GEM
foreman (0.90.0)
thor (~> 1.4)
formatador (0.2.5)
fugit (1.12.1)
et-orbi (~> 1.4)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
fuubar (2.5.1)
rspec-core (~> 3.0)
@@ -405,7 +404,7 @@ GEM
reline
htmlentities (4.4.2)
http_parser.rb (0.8.0)
i18n (1.14.8)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
i18n-js (3.9.2)
i18n (>= 0.6.6)
@@ -507,15 +506,14 @@ GEM
logger
mini_mime (1.1.5)
mini_portile2 (2.8.6)
minitest (6.0.2)
drb (~> 2.0)
minitest (6.0.1)
prism (~> 1.5)
monetize (1.13.0)
money (~> 6.12)
money (6.16.0)
i18n (>= 0.6.4, <= 2)
msgpack (1.8.0)
multi_json (1.17.0)
multi_json (1.19.1)
multi_xml (0.6.0)
mutex_m (0.3.0)
net-http (0.9.1)
@@ -531,7 +529,7 @@ GEM
net-protocol
newrelic_rpm (9.24.0)
nio4r (2.7.5)
nokogiri (1.19.1)
nokogiri (1.19.0)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri-html5-inference (0.3.0)
@@ -568,7 +566,7 @@ GEM
validate_url
webfinger (~> 2.0)
orm_adapter (0.5.0)
ostruct (0.6.1)
ostruct (0.6.3)
package_json (0.2.0)
pagy (9.4.0)
paper_trail (17.0.0)
@@ -585,7 +583,7 @@ GEM
xml-simple
paypal-sdk-merchant (1.117.2)
paypal-sdk-core (~> 0.3.0)
pdf-reader (2.15.1)
pdf-reader (2.15.0)
Ascii85 (>= 1.0, < 3.0, != 2.0.0)
afm (>= 0.2.1, < 2)
hashery (~> 2.0)
@@ -604,7 +602,7 @@ GEM
psych (5.3.1)
date
stringio
public_suffix (7.0.2)
public_suffix (7.0.0)
puffing-billy (4.0.2)
addressable (~> 2.5)
em-http-request (~> 1.1, >= 1.1.0)
@@ -671,8 +669,8 @@ GEM
activesupport (>= 4.2)
choice (~> 0.2.0)
ruby-graphviz (~> 1.2)
rails-html-sanitizer (1.7.0)
loofah (~> 2.25)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (7.0.10)
i18n (>= 0.7, < 2)
@@ -712,7 +710,7 @@ GEM
redcarpet (3.6.1)
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.26.4)
redis-client (0.26.1)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
@@ -817,8 +815,8 @@ GEM
ffi (~> 1.12)
logger
rubyzip (2.4.1)
rufus-scheduler (3.9.2)
fugit (~> 1.1, >= 1.11.1)
rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6)
rugged (1.9.0)
sanitize (7.0.0)
crass (~> 1.0.2)
@@ -840,9 +838,10 @@ GEM
logger
rack (>= 2.2.4, < 3.3)
redis-client (>= 0.23.0, < 1)
sidekiq-scheduler (6.0.1)
sidekiq-scheduler (5.0.3)
rufus-scheduler (~> 3.2)
sidekiq (>= 7.3, < 9)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@@ -858,9 +857,6 @@ GEM
spring (>= 0.9.1)
spring-commands-rubocop (0.4.0)
spring (>= 1.0)
spring-watcher-listen (2.1.0)
listen (>= 2.7, < 4.0)
spring (>= 4)
sprockets (3.7.5)
base64
concurrent-ruby (~> 1.0)
@@ -897,18 +893,18 @@ GEM
faraday (~> 2.0)
faraday-follow_redirects
sysexits (1.2.0)
taler (0.2.0)
taler (0.1.0)
temple (0.10.4)
terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4)
thor (1.5.0)
thread-local (1.1.0)
tilt (2.7.0)
tilt (2.6.1)
timeout (0.6.0)
tsort (0.2.0)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
turbo-rails (2.0.23)
turbo-rails (2.0.20)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
turbo_power (0.7.0)
@@ -936,7 +932,8 @@ GEM
public_suffix
validates_lengths_from_database (0.8.0)
activerecord (>= 4)
vcr (6.4.0)
vcr (6.3.1)
base64
view_component (4.1.1)
actionview (>= 7.1.0, < 8.2)
activesupport (>= 7.1.0, < 8.2)
@@ -963,7 +960,7 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.2)
websocket-driver (0.8.0)
websocket-driver (0.7.7)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
@@ -973,7 +970,7 @@ GEM
xml-simple (1.1.8)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.5)
zeitwerk (2.7.4)
PLATFORMS
ruby
@@ -1104,7 +1101,6 @@ DEPENDENCIES
spring
spring-commands-rspec
spring-commands-rubocop
spring-watcher-listen
sprockets (~> 3.7)
state_machines-activerecord
stimulus_reflex

View File

@@ -4,12 +4,16 @@ angular.module("admin.enterprises")
$scope.Enterprises = Enterprises
$scope.navClear = NavigationCheck.clear
$scope.menu = SideMenu
$scope.newManager = { id: null, email: (t('add_manager')) }
$scope.StatusMessage = StatusMessage
$scope.RequestMonitor = RequestMonitor
$scope.$watch 'enterprise_form.$dirty', (newValue) ->
StatusMessage.display 'notice', t('admin.unsaved_changes') if newValue
$scope.$watch 'newManager', (newValue) ->
$scope.addManager($scope.newManager) if newValue
$scope.setFormDirty = ->
$scope.$apply ->
$scope.enterprise_form.$setDirty()
@@ -31,6 +35,26 @@ angular.module("admin.enterprises")
# Register the NavigationCheck callback
NavigationCheck.register(enterpriseNavCallback)
$scope.removeManager = (manager) ->
if manager.id?
if manager.id == $scope.Enterprise.owner.id or manager.id == parseInt($scope.receivesNotifications)
return
for i, user of $scope.Enterprise.users when user.id == manager.id
$scope.Enterprise.users.splice i, 1
$scope.enterprise_form?.$setDirty()
$scope.addManager = (manager) ->
if manager.id? and angular.isNumber(manager.id) and manager.email?
manager =
id: manager.id
email: manager.email
confirmed: manager.confirmed
if (user for user in $scope.Enterprise.users when user.id == manager.id).length == 0
$scope.Enterprise.users.unshift(manager)
$scope.enterprise_form?.$setDirty()
else
alert ("#{manager.email}" + " " + t("is_already_manager"))
$scope.performEnterpriseAction = (enterpriseActionName, warning_message_key, success_message_key) ->
return unless confirm($scope.translation(warning_message_key))

View File

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

View File

@@ -9,11 +9,6 @@
padding: 1.2rem;
}
&.big {
max-height: 50em;
overflow: auto;
}
h1,
h2,
h3,

View File

@@ -1,26 +0,0 @@
# frozen_string_literal: true
module Admin
class CustomerAccountTransactionController < Admin::ResourceController
def index
@available_credit = @collection.first&.balance || 0.00
respond_with do |format|
format.turbo_stream {
render :index
}
end
end
# We are using an old version of CanCanCan so I could not get `accessible_by` to work properly,
# so we are doing our own authorization before calling 'accessible_by'
def collection
allowed_customers = OpenFoodNetwork::Permissions.new(spree_current_user)
.managed_enterprises.joins(:customers).select("customers.id").map(&:id)
raise CanCan::AccessDenied unless allowed_customers.include?(params[:customer_id].to_i)
CustomerAccountTransaction.accessible_by(current_ability, action)
.where(customer_id: params[:customer_id]).order(id: :desc)
end
end
end

View File

@@ -67,6 +67,7 @@ module Admin
def update
tag_rules_attributes = params[object_name].delete :tag_rules_attributes
update_tag_rules(tag_rules_attributes) if tag_rules_attributes.present?
update_enterprise_notifications
update_vouchers
delete_custom_tab if params[:custom_tab] == 'false'
@@ -162,11 +163,9 @@ module Admin
end
def destroy
@object.transaction do
@object.destroy!
if @object.destroy
flash.now[:success] = flash_message_for(@object, :successfully_removed)
rescue StandardError
Rails.logger.error @object.errors.full_messages.to_sentence
else
flash.now[:error] = @object.errors.full_messages.to_sentence
end
@@ -178,7 +177,7 @@ module Admin
protected
def delete_custom_tab
@object.custom_tab.presence&.destroy
@object.custom_tab.destroy if @object.custom_tab.present?
enterprise_params.delete(:custom_tab_attributes)
end
@@ -241,7 +240,9 @@ module Admin
enterprises = OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, @order_cycle)
.visible_enterprises
enterprises.presence&.includes(supplied_products: [:variants, :image])
if enterprises.present?
enterprises.includes(supplied_products: [:variants, :image])
end
when :index
if spree_current_user.admin?
OpenFoodNetwork::Permissions.new(spree_current_user).
@@ -313,6 +314,14 @@ module Admin
end
end
def update_enterprise_notifications
user_id = params[:receives_notifications].to_i
return unless user_id.positive? && @enterprise.user_ids.include?(user_id)
@enterprise.update_contact(user_id)
end
def update_vouchers
params_voucher_ids = params[:enterprise][:voucher_ids].to_a.map(&:to_i)
voucher_ids = @enterprise.vouchers.map(&:id)

View File

@@ -28,7 +28,7 @@ module Admin
flash[:success] = I18n.t('admin.products_v3.bulk_update.success')
redirect_to [:index,
{ page: @page, per_page: @per_page, search_term: @search_term,
producer_id: @producer_id, category_id: @category_id, tags_name_in: @tags }]
producer_id: @producer_id, category_id: @category_id }]
elsif product_set.errors.present?
@error_counts = { saved: product_set.saved_count, invalid: product_set.invalid.count }
@@ -120,7 +120,7 @@ module Admin
@search_term = params[:search_term] || params[:_search_term]
@producer_id = params[:producer_id] || params[:_producer_id]
@category_id = params[:category_id] || params[:_category_id]
@tags = params[:tags_name_in] || []
@tags = params[:tags_name_in] || params[:_tags_name_in]
end
def init_pagination_params

View File

@@ -1,31 +0,0 @@
# frozen_string_literal: true
module Admin
class UserInvitationsController < ResourceController
before_action :load_enterprise
def new; end
def create
@user_invitation.attributes = permitted_resource_params
if @user_invitation.save!
flash[:success] = I18n.t(:user_invited, email: @user_invitation.email)
else
render :new
end
end
private
def load_enterprise
@enterprise = OpenFoodNetwork::Permissions
.new(spree_current_user)
.editable_enterprises
.find_by(permalink: params[:enterprise_id])
end
def permitted_resource_params
params.require(:user_invitation).permit(:email).merge(enterprise: @enterprise)
end
end
end

View File

@@ -1,34 +0,0 @@
# frozen_string_literal: true
module Api
module V1
class CustomerAccountTransactionController < Api::V1::BaseController
def create
authorize! :create, CustomerAccountTransaction
default_params = {
currency: CurrentConfig.get(:currency), created_by: current_api_user
}
parameters = default_params.merge(customer_account_transaction_params).merge(description: )
transaction = CustomerAccountTransaction.new(parameters)
if transaction.save
render json: Api::V1::CustomerAccountTransactionSerializer.new(transaction),
status: :created
else
invalid_resource! transaction
end
end
private
def customer_account_transaction_params
params.require(:customer_account_transaction).permit(:customer_id, :amount, :description)
end
def description
I18n.t(".api_customer_credit", description: params[:description])
end
end
end
end

View File

@@ -31,11 +31,6 @@ class CheckoutController < BaseController
check_step
end
if payment_step? || summary_step?
credit_payment_method = @order.distributor.payment_methods.customer_credit
@paid_with_credit = @order.payments.find_by(payment_method: credit_payment_method)&.amount
end
return if available_shipping_methods.any?
flash[:error] = I18n.t('checkout.errors.no_shipping_methods_available')
@@ -126,9 +121,7 @@ class CheckoutController < BaseController
shipping_method_updated = @order.shipping_method&.id != params[:shipping_method_id].to_i
@order.select_shipping_method(params[:shipping_method_id])
@order.update(order_params)
# We need to update voucher to take into account:
# * when moving away from "details" step : potential change in shipping method fees
# * when moving away from "payment" step : payment fees

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
module ManagerInvitations
extend ActiveSupport::Concern
def create_new_manager(email, enterprise)
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.
new_user.reset_password_sent_at = Time.now.utc
if new_user.save
enterprise.users << new_user
EnterpriseMailer.manager_invitation(enterprise, new_user).deliver_later
end
new_user
end
end

View File

@@ -15,7 +15,6 @@ module Spree
Spree::Gateway::StripeSCA
Spree::PaymentMethod::Check
Spree::PaymentMethod::Taler
Spree::PaymentMethod::CustomerCredit
}.freeze
def create

View File

@@ -5,7 +5,7 @@ module Spree
class PaymentsController < Spree::Admin::BaseController
before_action :load_order, except: [:show]
before_action :load_payment, only: [:fire, :show]
before_action :load_data, except: [:credit_customer]
before_action :load_data
before_action :can_transition_to_payment
# We ensure that items are in stock before all screens if the order is in the Payment state.
# This way, we don't allow someone to enter credit card details for an order only to be told
@@ -92,18 +92,6 @@ module Spree
end
end
def credit_customer
response = ::Orders::CustomerCreditService.new(@order).refund
if response.success?
flash[:success] = Spree.t(:customer_credit_successful, scope: "admin.payments")
else
flash[:error] = response.message
end
redirect_to admin_order_payments_path(@order)
end
private
def load_payment_source
@@ -197,7 +185,7 @@ module Spree
end
def allowed_events
%w{capture void_transaction credit refund internal_void resend_authorization_email
%w{capture void_transaction credit refund resend_authorization_email
capture_and_complete_order}
end

View File

@@ -29,13 +29,7 @@ module Spree
hide_ofn_navigation(@order.distributor)
end
def show
credit_payment_method = @order.distributor.payment_methods.customer_credit
credit_payment = @order.payments.find_by(payment_method: credit_payment_method)
@paid_with_credit = credit_payment&.amount
@payment_total = @order.payment_total - @paid_with_credit.to_f
end
def show; end
def empty
if @order = current_order

View File

@@ -1,50 +0,0 @@
# frozen_string_literal: true
class UserInvitation
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations::Callbacks
attribute :enterprise
attribute :email
before_validation :normalize_email
validates :email, presence: true, 'valid_email_2/email': { mx: true }
validates :enterprise, presence: true
validate :not_existing_enterprise_user
def save!
return unless valid?
user = find_or_create_user!
enterprise.users << user
return unless user.previously_new_record?
EnterpriseMailer.manager_invitation(enterprise, user).deliver_later
end
private
def find_or_create_user!
Spree::User.find_or_create_by!(email: email) do |user|
user.email = email
user.password = SecureRandom.base58(64)
user.unconfirmed_email = email
user.reset_password_token = Devise.friendly_token
# Same time as used in Devise's lib/devise/models/recoverable.rb.
user.reset_password_sent_at = Time.now.utc
end
end
def normalize_email
self.email = email.strip if email.present?
end
def not_existing_enterprise_user
return unless email.present? && enterprise.users.where(email: email).exists?
errors.add(:email, :is_already_manager)
end
end

View File

@@ -83,6 +83,31 @@ module CheckoutHelper
Spree::Money.new order.total - order.total_tax, currency: order.currency
end
def validated_input(name, path, args = {})
attributes = {
:required => true,
:type => :text,
:name => path,
:id => path,
"ng-model" => path,
"ng-class" => "{error: !fieldValid('#{path}')}"
}.merge args
render "shared/validated_input", name:, path:, attributes:
end
def validated_select(name, path, options, args = {})
attributes = {
:required => true,
:id => path,
"ng-model" => path,
"ng-class" => "{error: !fieldValid('#{path}')}"
}.merge args
render "shared/validated_select", name:, path:, options:,
attributes:
end
def payment_method_price(method, order)
price = method.compute_amount(order)
if price == 0
@@ -114,7 +139,7 @@ module CheckoutHelper
def stripe_card_options(cards)
cards.map do |cc|
[
"#{cc.cc_type} #{cc.last_digits} #{I18n.t(:card_expiry_abbreviation)}:" \
"#{cc.brand} #{cc.last_digits} #{I18n.t(:card_expiry_abbreviation)}:" \
"#{cc.month.to_s.rjust(2, '0')}/#{cc.year}", cc.id
]
end

View File

@@ -17,7 +17,7 @@ module ReportsHelper
next unless payment_method
[payment_method.display_name, payment_method.id]
[payment_method.name, payment_method.id]
end.compact.uniq
end

View File

@@ -11,7 +11,7 @@ module Spree
end
def payment_method_name(payment)
payment_method(payment)&.display_name
payment_method(payment)&.name
end
end
end

View File

@@ -1,30 +0,0 @@
# frozen_string_literal: true
class CustomerAccountTransactionSchema < JsonApiSchema
def self.object_name
"customer_account_transaction"
end
def self.attributes
{
id: { type: :integer, example: 1 },
customer_id: { type: :integer, example: 10 },
amount: { type: :decimal, example: 10.50 },
currency: { type: :string, example: "AUD" },
description: { type: :string, nullable: true, example: "Payment processed by POS" },
balance: { type: :decimal, example: 10.50 },
}
end
def self.required_attributes
[:customer_id, :amount]
end
def self.writable_attributes
attributes.except(:id, :balance, :currency)
end
def self.relationships
[:customer]
end
end

View File

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

View File

@@ -11,7 +11,6 @@ module Spree
def cancel_email(order_or_order_id, resend = false)
@order = find_order(order_or_order_id)
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
I18n.with_locale valid_locale(@order.user) do
mail(to: @order.email,
subject: mail_subject(t('spree.order_mailer.cancel_email.subject'), resend),
@@ -52,7 +51,6 @@ module Spree
def invoice_email(order_or_order_id, options = {})
@order = find_order(order_or_order_id)
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
current_user = if options[:current_user_id].present?
find_user(options[:current_user_id])
end

View File

@@ -4,7 +4,6 @@ module Spree
class ShipmentMailer < ApplicationMailer
def shipped_email(shipment, delivery:)
@shipment = shipment.respond_to?(:id) ? shipment : Spree::Shipment.find(shipment)
@hide_ofn_navigation = @shipment.order.distributor.hide_ofn_navigation
@delivery = delivery
@order = @shipment.order
subject = base_subject

View File

@@ -19,7 +19,6 @@ class SubscriptionMailer < ApplicationMailer
@type = 'empty'
@changes = changes
@order = order
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
send_mail(order)
end
@@ -27,13 +26,11 @@ class SubscriptionMailer < ApplicationMailer
@type = 'placement'
@changes = changes
@order = order
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
send_mail(order)
end
def failed_payment_email(order)
@order = order
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
send_mail(order)
end

View File

@@ -19,7 +19,6 @@ class Customer < ApplicationRecord
belongs_to :enterprise
belongs_to :user, class_name: "Spree::User", optional: true
has_many :orders, class_name: "Spree::Order", dependent: :nullify
has_many :customer_account_transactions, dependent: :restrict_with_error
before_validation :downcase_email
before_validation :empty_code
before_create :associate_user

View File

@@ -1,38 +0,0 @@
# frozen_string_literal: true
require "spree/localized_number"
class CustomerAccountTransaction < ApplicationRecord
extend Spree::LocalizedNumber
localize_number :amount
belongs_to :customer
belongs_to :payment, class_name: "Spree::Payment", optional: true
belongs_to :created_by, class_name: "Spree::User", optional: true
validates :amount, presence: true
validates :currency, presence: true
before_create :update_balance
private
def readonly?
!new_record?
end
def update_balance
# Locking the customer to prevent two transactions from behing created at the same time
# resulting in a potentially wrong balance calculation.
customer.with_lock(requires_new: true) do
last_transaction = CustomerAccountTransaction.where(customer: customer).last
self.balance = if last_transaction.present?
last_transaction.balance + amount
else
amount
end
end
end
end

View File

@@ -294,13 +294,7 @@ class Enterprise < ApplicationRecord
contact || owner
end
def contact_id
contact&.id
end
def contact_id=(user_id)
return unless user_id.to_i.positive? && users.confirmed.exists?(user_id.to_i)
def update_contact(user_id)
enterprise_roles.update_all(["receives_notifications=(user_id=?)", user_id])
end
@@ -582,7 +576,7 @@ class Enterprise < ApplicationRecord
end
def set_default_contact
self.contact_id = owner_id
update_contact owner_id
end
def relate_to_owners_enterprises

View File

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

View File

@@ -18,7 +18,7 @@ class Invoice
end
def payment_method_name
payment_method&.display_name
payment_method&.name
end
end
end

View File

@@ -3,7 +3,7 @@
class Invoice
class DataPresenter
class PaymentMethod < Invoice::DataPresenter::Base
attributes :id, :display_name, :display_description
attributes :id, :name, :description
invoice_generation_attributes :id
end
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class OrderBalance
delegate :negative?, :positive?, :zero?, :abs, :to_s, :to_f, :to_d, :<, :>, :<=, :>=, to: :amount
delegate :negative?, :positive?, :zero?, :abs, :to_s, :to_f, :to_d, :<, :>, to: :amount
def initialize(order)
@order = order

View File

@@ -76,7 +76,6 @@ class ProxyOrder < ApplicationRecord
def cart?
order&.state == 'complete' &&
order_cycle.orders_close_at.present? &&
order_cycle.orders_close_at > Time.zone.now
end

View File

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

View File

@@ -61,7 +61,6 @@ module Spree
add_manage_line_items_abilities user
end
add_relationship_management_abilities user if can_manage_relationships? user
add_customer_account_transaction_abilities user if can_manage_enterprises? user
end
# New users have no enterprises.
@@ -192,7 +191,7 @@ module Spree
user.enterprises.include? stripe_account.enterprise
end
can [:admin, :create], UserInvitation
can [:admin, :create], :manager_invitation
can [:admin, :index, :destroy], :oidc_setting
@@ -458,9 +457,5 @@ module Spree
user.enterprises.include?(enterprise_relationship.child)
end
end
def add_customer_account_transaction_abilities(_user)
can [:admin, :create, :index], CustomerAccountTransaction
end
end
end

View File

@@ -25,6 +25,9 @@ module Spree
scope :with_payment_profile, -> { where.not(gateway_customer_profile_id: nil) }
# needed for some of the ActiveMerchant gateways (eg. SagePay)
alias_attribute :brand, :cc_type
def expiry=(expiry)
self[:month], self[:year] = expiry.split(" / ")
self[:year] = "20#{self[:year]}"

View File

@@ -52,9 +52,9 @@ module Spree
def supports?(source)
return true unless provider_class.respond_to? :supports?
return false unless source.cc_type
return false unless source.brand
provider_class.supports?(source.cc_type)
provider_class.supports?(source.brand)
end
end
end

View File

@@ -730,9 +730,5 @@ module Spree
adjustment.update_adjustment!(force: true)
update_totals_and_states
end
def apply_customer_credit
Orders::CustomerCreditService.new(self).apply
end
end
end

View File

@@ -78,8 +78,6 @@ module Spree
before_transition to: :delivery, do: :create_proposed_shipments
before_transition to: :delivery, do: :ensure_available_shipping_rates
before_transition to: :payment, do: :apply_customer_credit
before_transition to: :confirmation, do: :validate_payment_method!
after_transition to: :payment do |order|

View File

@@ -18,7 +18,7 @@ module Spree
belongs_to :order, class_name: 'Spree::Order'
belongs_to :source, polymorphic: true
belongs_to :payment_method, class_name: "Spree::PaymentMethod", inverse_of: :payments
belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
has_many :offsets, -> { where("source_type = 'Spree::Payment' AND amount < 0").completed },
class_name: "Spree::Payment", foreign_key: :source_id,
@@ -115,20 +115,12 @@ module Spree
Alert.raise(
e,
metadata: {
event_type: "ofn.payment_transition", payment_id: payment.id, event: transition.to
event_tye: "ofn.payment_transition", payment_id: payment.id, event: transition.to
}
)
end
end
# Allows by passing the default scope on Spree::PaymentMethod. It's needed to link payment
# to internal payment method.
# Using ->{ unscoped } on the association doesn't work presumably because the default scope
# is not a simple `where`.
def payment_method
Spree::PaymentMethod.unscoped { super }
end
def money
Spree::Money.new(amount, currency:)
end

View File

@@ -4,8 +4,6 @@ module Spree
class Payment < ApplicationRecord
module Processing
def process!
return internal_purchase! if payment_method.internal?
return unless validate!
purchase!
@@ -22,17 +20,6 @@ module Spree
end
end
def internal_purchase!
started_processing!
options = { customer_id: order.customer_id, payment_id: id, order_number: order.number }
response = payment_method.purchase(
(amount * 100).round,
nil,
options
)
handle_response(response, :complete, :failure)
end
def authorize!(return_url = nil)
started_processing!
gateway_action(source, :authorize, :pend, return_url:)
@@ -144,27 +131,6 @@ module Spree
end
end
def internal_void!
return true if void?
# We should only void complete payment, otherwise we will be refunding credit that was
# not used in the first place.
return gateway_error(Spree.t(:internal_payment_not_voidable)) if state != "completed"
options = { customer_id: order.customer_id, payment_id: id, order_number: order.number }
response = payment_method.void(
(amount * 100).round,
nil,
options
)
record_response(response)
if response.success?
void
else
gateway_error(response)
end
end
def partial_credit(amount)
return if amount > credit_allowed
@@ -282,7 +248,6 @@ module Spree
end
logger.error(Spree.t(:gateway_error))
logger.error(" #{error.to_yaml}")
# TODO why is this not captured ?
raise Core::GatewayError, text
end

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Spree
class PaymentMethod < ApplicationRecord # rubocop:disable Metrics/ClassLength
class PaymentMethod < ApplicationRecord
include CalculatedAdjustments
include PaymentMethodDistributors
@@ -11,11 +11,9 @@ module Spree
acts_as_paranoid
DISPLAY = [:both, :back_end].freeze
INTERNAL = Spree::PaymentMethod::CustomerCredit.to_s
default_scope -> { where(deleted_at: nil).where.not(type: INTERNAL) }
default_scope -> { where(deleted_at: nil) }
has_many :credit_cards, class_name: "Spree::CreditCard", dependent: :destroy
has_many :payments, class_name: "Spree::Payment", dependent: :restrict_with_error
validates :name, presence: true
validate :distributor_validation
@@ -54,12 +52,6 @@ module Spree
.where(environment: [Rails.env, "", nil])
}
# These method is used to get the internal payment method. It is accessible to all
# enterprise, but the accessibility is managed by the code, as opposed to using the database.
def self.customer_credit
unscoped.find_by(type: "Spree::PaymentMethod::CustomerCredit", deleted_at: nil)
end
def configured?
!stripe? || stripe_configured?
end
@@ -117,23 +109,9 @@ module Spree
distributors.include?(distributor)
end
def display_name
try_translating(name)
end
def display_description
try_translating(description)
end
def internal?
type == INTERNAL
end
private
def distributor_validation
return true if internal?
validates_with DistributorsValidator
end
@@ -148,11 +126,5 @@ module Spree
preferred_enterprise_id > 0 &&
stripe_account_id.present?
end
def try_translating(value)
return value if value.blank?
I18n.t(value, default: value)
end
end
end

View File

@@ -1,110 +0,0 @@
# frozen_string_literal: true
module Spree
class PaymentMethod
class CustomerCredit < Spree::PaymentMethod
# Name and description are translatable string, to allow instances to customise them
def name
"credit_payment_method.name"
end
def description
"credit_payment_method.description"
end
def actions
%w{internal_void}
end
# We should only void complete payment, otherwise we will be refunding credit that was
# not used in the first place.
def can_internal_void?(payment)
payment.state == "completed"
end
# Main method called by Spree::Payment::Processing during checkout
# - amount is in cents
# - options: {
# customer_id:, payment_id:, order_number:
# }
def purchase(amount, _source, options)
calculated_amount = amount / 100.00
customer = Customer.find_by(id: options[:customer_id])
return error_response("customer_not_found") if customer.nil?
return error_response("missing_payment") if options[:payment_id].nil?
available_credit = customer.customer_account_transactions.last&.balance
return error_response("no_credit_available") if available_credit.nil?
return error_response("not_enough_credit_available") if calculated_amount > available_credit
customer.with_lock do
description = I18n.t(
"order_payment_description",
scope: "credit_payment_method",
order_number: options[:order_number]
)
customer.customer_account_transactions.create(
amount: -calculated_amount,
currency:,
payment_id: options[:payment_id],
description:
)
end
message = I18n.t("success", scope: "credit_payment_method")
ActiveMerchant::Billing::Response.new(true, message)
end
# Main method called by Spree::Payment::Processing for void
# - amount is in cents
# - options: {
# customer_id:, payment_id:, order_number:, user_id: (optional)
# }
def void(amount, _source, options)
calculated_amount = amount / 100.00
customer = Customer.find_by(id: options[:customer_id])
return error_response("customer_not_found") if customer.nil?
return error_response("missing_payment") if options[:payment_id].nil?
customer.with_lock do
description = I18n.t(
"order_void_description",
scope: "credit_payment_method",
order_number: options[:order_number]
)
customer.customer_account_transactions.create(
amount: calculated_amount,
currency:,
payment_id: options[:payment_id],
description:,
created_by_id: options[:user_id]
)
end
message = I18n.t("void_success", scope: "credit_payment_method")
ActiveMerchant::Billing::Response.new(true, message)
end
def method_type
"check" # empty view
end
def source_required?
false
end
private
def error_response(translation_key)
message = I18n.t(translation_key, scope: "credit_payment_method.errors")
ActiveMerchant::Billing::Response.new(false, message)
end
def currency
CurrentConfig.get(:currency)
end
end
end
end

View File

@@ -21,14 +21,6 @@ module Spree
preference :backend_url, :string
preference :api_key, :password
def actions
%w{void}
end
def can_void?(payment)
payment.state == "completed"
end
# Name of the view to display during checkout
def method_type
"check" # empty view
@@ -46,9 +38,10 @@ module Spree
payment.source ||= self
payment.response_code ||= create_taler_order(payment)
payment.redirect_auth_url ||= fetch_order_url(payment)
payment.save! if payment.changed?
taler_order.status_url
payment.redirect_auth_url
end
# Main method called by Spree::Payment::Processing during checkout
@@ -60,33 +53,14 @@ module Spree
return unless payment.response_code
taler_order = taler_order(id: payment.response_code)
status = taler_order.fetch("order_status")
taler_order = client.fetch_order(payment.response_code)
status = taler_order["order_status"]
success = (status == "paid")
message = I18n.t(status, default: status, scope: "taler.order_status")
ActiveMerchant::Billing::Response.new(success, message)
end
def void(response_code, gateway_options)
payment = gateway_options[:payment]
taler_order = taler_order(id: response_code)
status = taler_order.fetch("order_status")
if status == "claimed"
return ActiveMerchant::Billing::Response.new(true, "Already expired")
end
raise "Unsupported action" if status != "paid"
amount = taler_order.fetch("contract_terms")["amount"]
taler_order.refund(refund: amount, reason: "void")
PaymentMailer.refund_available(payment, taler_order.status_url).deliver_later
ActiveMerchant::Billing::Response.new(true, "Refund initiated")
end
private
def load_payment(order)
@@ -98,20 +72,22 @@ module Spree
# current demo backend only working with the KUDOS currency.
taler_amount = "KUDOS:#{payment.amount}"
urls = Rails.application.routes.url_helpers
fulfillment_url = urls.payment_gateways_confirm_taler_url(payment_id: payment.id)
taler_order.create(
amount: taler_amount,
summary: I18n.t("payment_method_taler.order_summary"),
fulfillment_url:,
new_order = client.create_order(
taler_amount,
I18n.t("payment_method_taler.order_summary"),
urls.payment_gateways_confirm_taler_url(payment_id: payment.id),
)
new_order["order_id"]
end
def taler_order(id: nil)
@taler_order ||= ::Taler::Order.new(
backend_url: preferred_backend_url,
password: preferred_api_key,
id:,
)
def fetch_order_url(payment)
order = client.fetch_order(payment.response_code)
order["order_status_url"]
end
def client
@client ||= ::Taler::Client.new(preferred_backend_url, preferred_api_key)
end
end
end

View File

@@ -23,7 +23,6 @@ module Spree
before_destroy :check_completed_orders
scope :admin, -> { where(admin: true) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
has_many :enterprise_roles, dependent: :destroy
has_many :enterprises, through: :enterprise_roles

View File

@@ -12,8 +12,7 @@ class CustomersWithBalanceQuery
joins(left_join_complete_orders).
group("customers.id").
select("customers.*").
select("#{outstanding_balance_sum} AS balance_value").
select("#{available_credit} AS credit_value")
select("#{outstanding_balance_sum} AS balance_value")
end
private
@@ -35,21 +34,4 @@ class CustomersWithBalanceQuery
def outstanding_balance_sum
"SUM(#{OutstandingBalanceQuery.new.statement})::float"
end
def available_credit
<<~SQL.squish
CASE WHEN EXISTS (#{available_credit_subquery}) THEN (#{available_credit_subquery})#{' '}
ELSE 0.00 END
SQL
end
def available_credit_subquery
<<~SQL.squish
SELECT balance
FROM customer_account_transactions
WHERE customer_account_transactions.customer_id = customers.id
ORDER BY id desc
LIMIT 1
SQL
end
end

View File

@@ -0,0 +1,40 @@
# frozen_string_literal: true
class InviteManagerReflex < ApplicationReflex
include ManagerInvitations
def invite
email = params[:email]
enterprise = Enterprise.find(params[:enterprise_id])
authorize! :edit, enterprise
existing_user = Spree::User.find_by(email:)
locals = { error: nil, success: nil, email:, enterprise: }
if existing_user
locals[:error] = I18n.t('admin.enterprises.invite_manager.user_already_exists')
return_morph(locals)
return
end
new_user = create_new_manager(email, enterprise)
if new_user.errors.empty?
locals[:success] = true
else
locals[:error] = new_user.errors.full_messages.to_sentence
end
return_morph(locals)
end
private
def return_morph(locals)
morph "#add_manager_modal",
render(partial: "admin/enterprises/form/add_new_unregistered_manager", locals:)
end
end

View File

@@ -7,9 +7,9 @@ module Api
# columns to instance methods. This way, the `balance_value` alias on that class ends up being
# `object.balance_value` here.
class CustomerWithBalanceSerializer < CustomerSerializer
attributes :balance, :balance_status, :available_credit, :available_credit_url
attributes :balance, :balance_status
delegate :balance_value, :credit_value, to: :object
delegate :balance_value, to: :object
def balance
Spree::Money.new(balance_value, currency: CurrentConfig.get(:currency)).to_s
@@ -24,14 +24,6 @@ module Api
""
end
end
def available_credit
Spree::Money.new(object.credit_value).to_s
end
def available_credit_url
admin_customer_customer_account_transaction_index_path(object.id)
end
end
end
end

View File

@@ -1,9 +0,0 @@
# frozen_string_literal: true
module Api
module V1
class CustomerAccountTransactionSerializer < Api::V1::BaseSerializer
attributes :id, :customer_id, :amount, :currency, :description, :balance
end
end
end

View File

@@ -2,6 +2,6 @@
class Invoice
class PaymentMethodSerializer < ActiveModel::Serializer
attributes :id, :display_name, :display_description
attributes :id, :name, :description
end
end

View File

@@ -61,7 +61,7 @@ module Checkout
def set_payment_amount
return unless @order_params[:payments_attributes]
@order_params[:payments_attributes].first[:amount] = order.outstanding_balance.amount
@order_params[:payments_attributes].first[:amount] = order.total
end
def set_existing_card

View File

@@ -28,8 +28,6 @@ module Checkout
def validate_payment
return true if params.dig(:order, :payments_attributes, 0, :payment_method_id).present?
return true if order.zero_priced_order?
# No payment required, it's usually due to the order being paid by customer credit
return true if order.outstanding_balance <= 0.00
order.errors.add :payment_method, I18n.t('checkout.errors.select_a_payment_method')
end

View File

@@ -1,28 +0,0 @@
# frozen_string_literal: false
module CustomerAccountTransactions
class DataLoaderService
attr_reader :user, :enterprise
def initialize(user:, enterprise:)
@user = user
@enterprise = enterprise
end
def customer_account_transactions
return [] if user.customers.empty?
enterprise_customer = user.customers.find_by(enterprise: )
return [] if enterprise_customer.nil?
enterprise_customer.customer_account_transactions.order(id: :desc)
end
def available_credit
return 0 if customer_account_transactions.empty?
# We are ordered by newest, so the lastest transaction is the first one
customer_account_transactions.first.balance
end
end
end

View File

@@ -19,9 +19,8 @@ module Orders
end
def reset_other!(current_user, current_customer)
reset_user(current_user)
reset_user_and_customer(current_user)
reset_order_cycle(current_customer)
order.customer = current_customer
order.save!
end
@@ -29,7 +28,7 @@ module Orders
attr_reader :order, :distributor, :current_user
def reset_user(current_user)
def reset_user_and_customer(current_user)
return unless current_user
order.associate_user!(current_user) if order.user.blank? || order.email.blank?

View File

@@ -1,115 +0,0 @@
# frozen_string_literal: true
module Orders
class CustomerCreditService
def initialize(order)
@order = order
end
def apply
add_payment_with_credit if credit_available?
end
def refund(user: nil) # rubocop:disable Metrics/AbcSize
if order.payment_state != "credit_owed"
return Response.new(
success: false, message: I18n.t(:no_credit_owed, scope: translation_scope)
)
end
if credit_payment_method.nil?
error_message = I18n.t(:credit_payment_method_missing, scope: translation_scope)
log_error(error_message)
return Response.new(success: false, message: error_message)
end
amount = order.new_outstanding_balance
order.customer.with_lock do
payment = order.payments.create!( payment_method: credit_payment_method, amount: amount,
state: "completed", skip_source_validation: true)
options = { customer_id: order.customer_id, payment_id: payment.id,
order_number: order.number, user_id: user&.id }
response = credit_payment_method.void((-1 * amount * 100).round, nil, options)
raise response.message if response.failure?
Response.new(success: true, message: I18n.t(:refund_sucessful, scope: translation_scope))
end
rescue StandardError => e
# Even though the transaction rolled back, the order still have a payment in memory,
# so we reload the payments so the payment doesn't get saved later on
order.payments.reload
log_error(e)
Response.new(success: false, message: e.to_s)
end
private
attr_reader :order
def add_payment_with_credit
if credit_payment_method.nil?
error_message = I18n.t(:credit_payment_method_missing, scope: translation_scope)
log_error(error_message)
return
end
return if order.payments.where(payment_method: credit_payment_method).exists?
# we are already in a transaction because the order is locked, so we force creating a new one
# to make sure the rollback works as expected :
# https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions
ActiveRecord::Base.transaction(requires_new: true) do
amount = [available_credit, order.total].min
payment = order.payments.create!(payment_method: credit_payment_method, amount:)
payment.internal_purchase!
end
rescue StandardError => e
# Even though the transaction rolled back, the order still have a payment in memory,
# so we reload the payments so the payment doesn't get saved later on
order.payments.reload
log_error(e)
end
def credit_available?
return false if order.customer.nil?
available_credit > 0
end
def available_credit
@available_credit ||= order.customer.customer_account_transactions.last&.balance || 0.00
end
def credit_payment_method
Spree::PaymentMethod.customer_credit
end
def log_error(error)
Rails.logger.error("Orders::CustomerCreditService: #{error}")
Alert.raise(error)
end
def translation_scope
"orders.customer_credit_service"
end
class Response
attr_reader :message
def initialize(success:, message:)
@success = success
@message = message
end
def success?
@success
end
def failure?
!success?
end
end
end
end

View File

@@ -40,7 +40,7 @@ module PermittedAttributes
:hide_ofn_navigation, :white_label_logo, :white_label_logo_link,
:hide_groups_tab, :external_billing_id,
:enable_producers_to_edit_orders,
:remove_logo, :remove_promo_image, :remove_white_label_logo, :contact_id
:remove_logo, :remove_promo_image, :remove_white_label_logo
]
end
end

View File

@@ -1,31 +0,0 @@
= turbo_stream.update "customer-account-transactions-modal-container" do
= render ModalComponent.new(id: "customer-account-transactions-modal", instant: true, modal_class: "big") do
%h3
= t(".available_credit", available_credit: Spree::Money.new(@available_credit))
%table.index
%thead
%tr
%th.transaction-date
= t(".transaction_date")
%th.description
= t(".description")
%th.created-by
= t(".created_by")
%th.amount
= t(".amount")
%th.running-balance
= t(".running_balance")
%tbody
- @collection.each do |transaction|
%tr.transaction
%td.transaction-date
= transaction.updated_at.strftime("%Y-%m-%d")
%td.description
= transaction.description
%td.created-by
= transaction.created_by&.email
%td.amount
= Spree::Money.new(transaction.amount)
%td.running-balance
= Spree::Money.new(transaction.balance)

View File

@@ -48,7 +48,6 @@
%input.red{ type: "button", value: t(:save_changes), "ng-click": "submitAll(customers_form)", "ng-disabled": "!hasUnsavedChanges()" }
%table.index#customers{ 'ng-show' => '!RequestMonitor.loading && filteredCustomers.length > 0' }
%col.id{ width: "5%", 'ng-show' => 'columns.id.visible' }
%col.email{ width: "20%", 'ng-show' => 'columns.email.visible' }
%col.first_name{ width: "20%", 'ng-show' => 'columns.first_name.visible' }
%col.last_name{ width: "20%", 'ng-show' => 'columns.last_name.visible' }
@@ -62,7 +61,6 @@
%tr{ "ng-controller": "ColumnsCtrl" }
-# %th.bulk
-# %input{ :type => "checkbox", :name => 'toggle_bulk', 'ng-click' => 'toggleAllCheckboxes()', 'ng-checked' => "allBoxesChecked()" }
%th.id{ 'ng-show' => 'columns.id.visible' }=t('admin.customers.index.id')
%th.email{ 'ng-show' => 'columns.email.visible' }
%a{ :href => '', 'ng-click' => "sorting.toggle('email')" }=t('admin.email')
%th.first_name{ 'ng-show' => 'columns.first_name.visible' }
@@ -75,13 +73,10 @@
%th.bill_address{ 'ng-show' => 'columns.bill_address.visible' }=t('admin.customers.index.bill_address')
%th.ship_address{ 'ng-show' => 'columns.ship_address.visible' }=t('admin.customers.index.ship_address')
%th.balance{ 'ng-show' => 'columns.balance.visible' }=t('admin.customers.index.balance')
%th.credit{ 'ng-show' => 'columns.credit.visible' }=t('admin.customers.index.credit')
%tbody
%tr.customer{ 'ng-repeat' => "customer in filteredCustomers = ( customers | filter:quickSearch | orderBy: sorting.predicate:sorting.reverse ) | limitTo:customerLimit track by customer.id", 'ng-class-even' => "'even'", 'ng-class-odd' => "'odd'", :id => "c_{{customer.id}}" }
-# %td.bulk
-# %input{ :type => "checkbox", :name => 'bulk', 'ng-model' => 'customer.checked' }
%td.id{ 'ng-show' => 'columns.id.visible'}
%span{ 'ng-bind' => '::customer.id' }
%td.email{ 'ng-show' => 'columns.email.visible'}
%span{ 'ng-bind' => '::customer.email' }
%span.guest-label{ 'ng-show' => 'customer.user_id == null' }= t('.guest_label')
@@ -103,14 +98,9 @@
%td.balance.align-center{ 'ng-show' => 'columns.balance.visible'}
%span.state.white-space-nowrap{ 'ng-class' => 'customer.balance_status', 'ng-bind' => 'displayBalanceStatus(customer)' }
%span{ 'ng-bind' => '::customer.balance' }
%td.balance.align-center{ 'ng-show' => 'columns.credit.visible', "data-turbo": true}
%a{ "ng-href": "{{customer.available_credit_url}}", "data-turbo-stream": "" }
%span{ 'ng-bind' => '::customer.available_credit' }
%td.actions
%a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" }
.text-center{ "ng-show": "filteredCustomers.length > customerLimit" }
%input{ type: 'button', value: t(:show_more), "ng-click": 'customerLimit = customerLimit + 20' }
%input{ type: 'button', value: t(:show_all_with_more, num: '{{ filteredCustomers.length - customerLimit }}'), "ng-click": 'customerLimit = filteredCustomers.length' }
#customer-account-transactions-modal-container

View File

@@ -0,0 +1,21 @@
%form#add_manager_modal{ 'data-reflex': 'submit->InviteManager#invite', 'data-reflex-serialize-form': true }
.margin-bottom-30.text-center
.text-big
= t('js.admin.modals.invite_title')
- if success
%p.alert-box.ok= t('user_invited', email: email)
- if error
%p.alert-box.error= error
= text_field_tag :email, nil, class: 'fullwidth margin-bottom-20'
= hidden_field_tag :enterprise_id, @enterprise&.id || enterprise.id
.modal-actions
- if success
%input{ class: "button icon-plus secondary", type: 'button', value: t('js.admin.modals.close'), "data-action": "click->help-modal#close" }
- else
%input{ class: "button icon-plus secondary", type: 'button', value: t('js.admin.modals.cancel'), "data-action": "click->help-modal#close" }
= submit_tag "#{t('js.admin.modals.invite')}"

View File

@@ -11,7 +11,7 @@
%tbody
- @payment_methods.each do |payment_method|
%tr
%td= payment_method.display_name
%td= payment_method.name
%td= f.check_box :payment_method_ids, { multiple: true }, payment_method.id, nil
%td= link_to t(:edit), edit_admin_payment_method_path(payment_method)
%br

View File

@@ -1,54 +1,75 @@
- owner_email = @enterprise&.owner&.email || ""
- full_permissions = (spree_current_user.admin? || spree_current_user == @enterprise&.owner)
.row
= t '.description'
.three.columns.alpha
=f.label :owner_id, t('.owner')
- if full_permissions
%span.required *
= render partial: 'admin/shared/whats_this_tooltip', locals: {tooltip_text: t('.owner_tip')}
.eight.columns.omega
- if full_permissions
= f.hidden_field :owner_id, class: "select2 fullwidth", 'user-select' => 'Enterprise.owner', 'ng-model' => 'Enterprise.owner'
- else
= owner_email
- if full_permissions && @enterprise.users.count > 0
- enterprise_role_ids_by_user_id = @enterprise.enterprise_roles.pluck(:user_id, :id).to_h
.row
.three.columns.alpha
=f.label :user_ids, t('.notifications')
- if full_permissions
%span.required *
= render partial: 'admin/shared/whats_this_tooltip', locals: {tooltip_text: t('.contact_tip')}
.eight.columns.omega
- if full_permissions
%select.select2.fullwidth{ id: 'receives_notifications_dropdown', name: 'receives_notifications', "ng-model": 'receivesNotifications', "ng-init": "receivesNotifications = '#{@enterprise.contact.id}'" }
%option{ value: '{{user.id}}', "ng-repeat": 'user in Enterprise.users', "ng-selected": "user.id == #{@enterprise.contact.id}", "ng-hide": '!user.confirmed' }
{{user.email}}
- else
= @enterprise.contact.email
%table.managers
%thead
%tr
%th= t('.manager')
%th.center
= t('.owner')
= render AdminTooltipComponent.new(text: t('.owner_tip'), link_text: %[<i class="fa fa-question-circle"></i>].html_safe, link: nil)
%th.center
= t('.contact')
= render AdminTooltipComponent.new(text: t('.contact_tip'), link_text: %[<i class="fa fa-question-circle"></i>].html_safe, link: nil)
%tbody
- @enterprise.users.each do |user|
- contact = user.id == @enterprise.contact&.id
- owner = user.id == @enterprise.owner&.id
%tr{ id: "manager-#{user.id}" }
.row
.three.columns.alpha
=f.label :user_ids, t('.managers')
- if full_permissions
%span.required *
= render partial: 'admin/shared/whats_this_tooltip', locals: {tooltip_text: t('.managers_tip')}
.eight.columns.omega
- if full_permissions
%table.managers
%tr
%td
= user.email
- if user.confirmed?
%i.confirmation.confirmed.fa.fa-check-circle{ "ofn-with-tip": t('.email_confirmed') }
- else
%i.confirmation.unconfirmed.fa.fa-exclamation-triangle{ "ofn-with-tip": t('.email_not_confirmed') }
%td.center
- if user.confirmed?
= f.label :owner_id, t(".set_as_owner", email: user.email), class: "sr-only", value: user.id
= f.radio_button :owner_id, user.id
%td.center
- if user.confirmed?
= f.label :owner_id, t(".set_as_contact", email: user.email), class: "sr-only", value: user.id
= f.radio_button :contact_id, user.id
- # Ignore this input in the submit
= hidden_field_tag :ignored, nil, class: "select2 fullwidth", 'user-select' => 'newManager', 'ng-model' => 'newManager'
%td.actions
- if !owner && !contact
= link_to_delete user, no_text: true, url: admin_enterprise_role_path(id: enterprise_role_ids_by_user_id[user.id])
- else
%a{ class: "icon-trash no-text disabled" }
%tr.animate-repeat{ id: "manager-{{manager.id}}", "ng-repeat": 'manager in Enterprise.users' }
%td
= hidden_field_tag "enterprise[user_ids][]", nil, multiple: true, 'ng-value' => 'manager.id'
{{ manager.email }}
%i.confirmation.confirmed.fa.fa-check-circle{ "ofn-with-tip": t('.email_confirmed'), "ng-show": 'manager.confirmed' }
%i.confirmation.unconfirmed.fa.fa-exclamation-triangle{ "ofn-with-tip": t('.email_not_confirmed'), "ng-show": '!manager.confirmed' }
%i.role.contact.fa.fa-envelope-o{ "ofn-with-tip": t('.contact'), "ng-show": 'manager.id == receivesNotifications' }
%i.role.owner.fa.fa-star{ "ofn-with-tip": t('.owner'), "ng-show": 'manager.id == Enterprise.owner.id' }
%td.actions
%a{ class: "icon-trash no-text", "ng-click": 'removeManager(manager)', "ng-class": "{disabled: manager.id == Enterprise.owner.id || manager.id == receivesNotifications}" }
%a.button{ href: "#{new_admin_enterprise_user_invitation_path(@enterprise)}", data: { turbo_stream: true, turbo: true } }
%i.icon-plus
= t('.invite_manager')
%br
- else
- @enterprise.users.each do |manager|
= manager.email
%br
- else
- @enterprise.users.each do |manager|
= manager.email
%br
- if full_permissions
%form
.row
.three.columns.alpha
%label
= t('.invite_manager')
= render partial: 'admin/shared/whats_this_tooltip', locals: {tooltip_text: t('.invite_manager_tip')}
.eight.columns.omega
.row
%a.button{ "data-controller": "help-modal-link", "data-action": "click->help-modal-link#open", "data-help-modal-link-target-value": "invite-manager-modal" }
= t('.add_unregistered_user')
-# add to admin footer to avoid nesting invitation form inside enterprise form
- content_for :admin_footer do
= render HelpModalComponent.new(id: "invite-manager-modal", close_button: false) do
= render partial: 'admin/enterprises/form/add_new_unregistered_manager', locals: { error: nil, success: nil }

View File

@@ -68,11 +68,11 @@
@order_cycle.distributor_payment_methods.include?(distributor_payment_method),
id: "order_cycle_selected_distributor_payment_method_ids_#{distributor_payment_method.id}",
data: ({ "checked-target" => "checkbox" } if distributor_payment_method.payment_method.frontend?)
= distributor_payment_method.payment_method.display_name
= distributor_payment_method.payment_method.name
- distributor.payment_methods.inactive_or_backend.each do |payment_method|
%label.disabled
= check_box_tag nil, nil, false, disabled: true
= payment_method.display_name
= payment_method.name
= "(#{t('.back_end')})"
- if distributor.payment_methods.available.none?
%p

View File

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

View File

@@ -1,24 +1,24 @@
- link = pagy_anchor(pagy)
%nav.pagination{ "aria-label": t('.navigation'), "data-controller": "search" }
.pagination{ "data-controller": "search" }
- if pagy.prev
%a.page.prev{ href: "#", rel: "prev", "aria-label": t('.previous'), data: { action: 'click->search#changePage', page: pagy.prev } }
%button.page.prev{ data: { action: 'click->search#changePage', page: pagy.prev } }
%i.icon-chevron-left{ data: { action: 'click->search#changePage', page: pagy.prev } }
- else
%button.page.disabled{disabled: "disabled"}!= pagy_t('pagy.prev')
%ul.pagelist
- pagy.series.each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
- if item.is_a?(Integer) # page link
%li
%a.page{ href: "#", "aria-label": t('.page', number: item), data: { action: 'click->search#changePage', page: item } }= item
- pagy.series.each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
- if item.is_a?(Integer) # page link
%button.page{ data: { action: 'click->search#changePage', page: item } }= item
- elsif item.is_a?(String) # current page
%li
%a.page.current.active{ href: "#", "aria-label": t('.page', number: item), "aria-current": "page" }= item
- elsif item.is_a?(String) # current page
%button.page.current.active= item
- elsif item == :gap # page gap
%li
%span.page.gap.pagination-ellipsis!= pagy_t('pagy.gap')
- elsif item == :gap # page gap
%span.page.gap.pagination-ellipsis!= pagy_t('pagy.gap')
- if pagy.next
%a.page.next{ href: "#", rel: "next", "aria-label": t('.next'), data: { action: 'click->search#changePage', page: pagy.next } }
%button.page.next{ data: { action: 'click->search#changePage', page: pagy.next } }
%i.icon-chevron-right{ data: { action: 'click->search#changePage', page: pagy.next } }
- else
%button.page.disabled.pagination-next{disabled: "disabled"}!= pagy_t('pagy.next')

View File

@@ -1,5 +0,0 @@
= turbo_stream.update "remote_modal", ""
= turbo_stream.update "users_panel" do
= render partial: "admin/enterprises/form/users", locals: { f: ActionView::Helpers::FormBuilder.new(:enterprise, @enterprise, self, {}) }
= turbo_stream.append "flashes" do
= render partial: 'admin/shared/flashes', locals: { flashes: flash }

View File

@@ -1,17 +0,0 @@
= turbo_stream.update "remote_modal" do
= render ModalComponent.new id: "#modal_new_user_invitation", instant: true, close_button: false, modal_class: :fit do
= form_with model: @user_invitation, url: admin_enterprise_user_invitations_path(@enterprise), html: { name: "user_invitation", data: { turbo: true } } do |f|
%h2= t ".invite_new_user"
%p= t ".description"
%fieldset.no-border-top.no-border-bottom
.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
.modal-actions.justify-end.filter-actions
%input{ class: "secondary relaxed", type: 'button', value: t('.back'), "data-action": "click->modal#close" }
%button.button.primary.relaxed.icon-envelope{ type: "submit" }
= t(".invite")

View File

@@ -8,17 +8,10 @@
.checkout-title
= t("checkout.step2.payment_method.title")
- if @order.zero_priced_order? || @order.outstanding_balance.zero?
- if @order.zero_priced_order?
%h3= t(:no_payment_required)
- if @order.zero_priced_order?
= hidden_field_tag "order[payments_attributes][][amount]", 0
- if @paid_with_credit
= t(:credit_used, amount: Spree::Money.new(@paid_with_credit))
= hidden_field_tag "order[payments_attributes][][amount]", 0
- else
- if @paid_with_credit
= t(:credit_used, amount: Spree::Money.new(@paid_with_credit))
- selected_payment_method = @order.payments&.with_state(:checkout)&.first&.payment_method_id
- selected_payment_method ||= available_payment_methods[0].id if available_payment_methods.length == 1
- available_payment_methods.each do |payment_method|
@@ -30,13 +23,13 @@
"data-action": "paymentmethod#selectPaymentMethod",
"data-paymentmethod-id": "#{payment_method.id}",
"data-paymentmethod-target": "input"
= f.label :payment_method_id, "#{payment_method.display_name}", for: "payment_method_#{payment_method.id}"
= f.label :payment_method_id, "#{payment_method.name}", for: "payment_method_#{payment_method.id}"
%em.fees=payment_or_shipping_price(payment_method, @order)
.paymentmethod-container{"data-paymentmethod-id": "#{payment_method.id}", style: "display: #{payment_method.id == selected_payment_method ? "block" : "none"}"}
- if payment_method.description && !payment_method.description.empty?
.paymentmethod-description.panel
= simple_format(html_escape(payment_method.display_description))
= simple_format(html_escape(payment_method.description))
.paymentmethod-form
= render partial: "checkout/payment/#{payment_method.method_type}", locals: { payment_method: payment_method, f: f }

View File

@@ -30,7 +30,7 @@
- payment_method = last_payment_method(@order)
%div
- if payment_method
= payment_method.display_name
= payment_method.name
%em.fees
= payment_or_shipping_price(payment_method, @order)
- elsif @order.zero_priced_order?
@@ -41,7 +41,7 @@
.summary-subtitle
= t("checkout.step3.payment_method.instructions")
%div
= payment_method&.display_description
= payment_method&.description
.checkout-substep
@@ -56,8 +56,7 @@
.summary-right{ "data-controller": "sticky", "data-sticky-target": "container" }
.summary-right-line.total
.summary-right-line-label= t :order_total_price
.summary-right-line-value#order_total= @order.display_outstanding_balance.to_html
.summary-right-line-value#order_total= @order.display_total.to_html
.summary-right-line
.summary-right-line-label= t :order_produce
@@ -79,11 +78,6 @@
.summary-right-line-label= t :order_includes_tax
.summary-right-line-value#tax-row= display_checkout_tax_total(@order)
- if @paid_with_credit.present?
.summary-right-line
.summary-right-line-label= t :customer_credit
.summary-right-line-value#customer-credit= Spree::Money.new(-1 * @paid_with_credit).to_html
.checkout-submit
- if any_terms_required?(@order.distributor)
= render partial: "terms_and_conditions", locals: { f: f }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
%p= t(".message", shop: @shop, amount: @amount)
%p= link_to @taler_order_status_url, @taler_order_status_url

View File

@@ -0,0 +1,6 @@
%label{for: path}= name
%input.medium.input-text{attributes}
%small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"}
= "{{ fieldErrors('#{path}') }}"

View File

@@ -0,0 +1,6 @@
%label{for: path}= name
= select_tag path, options_for_select(options), attributes
%small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"}
= "{{ fieldErrors('#{path}') }}"

View File

@@ -17,4 +17,4 @@
%p.callout{style: "margin-top: 40px"}
%strong
= t :email_payment_description
%p{style: "margin: 5px"}= @order.last_payment_method.display_description
%p{style: "margin: 5px"}= @order.last_payment_method.description

View File

@@ -4,7 +4,7 @@
- content_for :page_title do
= t('.editing_payment_method')
%i.icon-arrow-right
= @payment_method.display_name
= @payment_method.name
- content_for :page_actions do
%li
= button_link_to t('.new'), spree.new_admin_payment_method_path, icon: 'icon-plus'

View File

@@ -32,7 +32,7 @@
%tbody
- @payment_methods.each do |method|
%tr{class: "#{cycle('odd', 'even')}", id: "#{spree_dom_id method}"}
%td.align-center= method.display_name
%td.align-center= method.name
%td.align-center
- method.distributors.each do |distributor|
= distributor.name

View File

@@ -14,7 +14,7 @@
%li
%label
= radio_button_tag 'payment[payment_method_id]', method.id, method == @payment_method, { class: "payment_methods_radios", "ng-model" => 'form_data.payment_method' }
= method.display_name
= t(method.name, scope: :payment_methods, default: method.name)
.payment-method-settings
- @payment_methods.each do |method|
.payment-methods{id: "payment_method_#{method.id}"}

View File

@@ -18,5 +18,5 @@
%span{class: "state #{payment.state}"}= t(payment.state, scope: "spree.payment_states", default: payment.state.capitalize)
%td.actions
- payment.actions.each do |action|
= link_to "", fire_admin_order_payment_path(@order, payment, e: action),
class: "icon_link icon-#{action} no-text", data: { method: :put, action: action, disable_with: "" }, title: Spree.t(action)
= link_to_with_icon "icon-#{action}", Spree.t(action), fire_admin_order_payment_path(@order, payment, e: action),
no_text: true, data: { method: :put, action: action, disable_with: "" }

View File

@@ -2,10 +2,6 @@
= render partial: 'spree/admin/shared/order_tabs', locals: { current: 'Payments' }
- content_for :page_actions do
- if @order.payment_state == "credit_owed"
%li#credit_customer_section
= button_link_to t(:credit_customer), admin_order_payments_credit_customer_url(@order), data: { method: :post }
- if @order.outstanding_balance?
%li#new_payment_section
= button_link_to t(:new_payment), new_admin_order_payment_url(@order), icon: 'icon-plus'

View File

@@ -29,7 +29,7 @@
.sixteen.columns.alpha
.eight.columns.alpha
= f.field_container :variant_unit do
= f.label :variant_unit_with_scale, t(".units")
= f.label :variant_unit, t(".units")
%span.required *
= f.select 'variant_unit', [],
{ include_blank: true },

View File

@@ -14,7 +14,7 @@
= " - OFN #{t(:administration)}"
%link{:href => "https://fonts.googleapis.com/css?family=Open+Sans:400italic,600italic,400,600&subset=latin,cyrillic,greek,vietnamese", :rel => "stylesheet", :type => "text/css"}
= stylesheet_pack_tag 'admin-style', media: "screen, print"
= stylesheet_pack_tag 'admin-style-v3', media: "screen, print"
= render "layouts/bugsnag_js"
- if content_for? :minimal_js

View File

@@ -4,21 +4,25 @@
#new_variant
%table.index
%table.index.sortable{"data-sortable-link" => update_positions_admin_product_variants_path(@product)}
%colgroup
%col{style: "width: 5%"}/
%col{style: "width: 25%"}/
%col{style: "width: 25%"}/
%col{style: "width: 25%"}/
%col{style: "width: 25%"}/
%col{style: "width: 20%"}/
%col{style: "width: 20%"}/
%col{style: "width: 15%"}/
%col{style: "width: 15%"}/
%thead
%tr
%th= t('.options')
%th{ style: 'text-align: center;' }= t('.price')
%th{ style: 'text-align: center;' }= t('.sku')
%th{colspan: "2"}= t('.options')
%th= t('.price')
%th= t('.sku')
%th.actions
%tbody
- @variants.each do |variant|
%tr{id: spree_dom_id(variant), class: cycle('odd', 'even'), style: "#{"color:red;" if variant.deleted? }" }
%td.no-border
%span.handle
%td= variant.full_name
%td.align-center= variant.display_price.to_html
%td.align-center= variant.sku

View File

@@ -60,12 +60,10 @@
= yield :sidebar
= render "admin/terms_of_service_banner" if tos_need_accepting?
%script
= raw "Spree.api_key = \"#{spree_current_user.try(:spree_api_key).to_s}\";"
= render "layouts/matomo_tag"
= yield :admin_footer
#remote_modal

View File

@@ -48,17 +48,9 @@
%h5
= t :orders_form_total
%td.text-right
%h5.order-total.grand-total= @order.display_total.to_html
%h5.order-total.grand-total= @order.display_total
%td
- if @paid_with_credit.present?
%tr
%td.text-right{colspan: "3"}
%strong
= t :customer_credit
%td.text-right#customer-credit
%span.order-total= Spree::Money.new(-1 * @paid_with_credit).to_html
- if @order.total_tax > 0
%tr
%td.text-right{colspan:"3"}

View File

@@ -25,28 +25,20 @@
= t :order_total_price
%td.text-right.total
%h5#order_total= order.display_total.to_html
- if @paid_with_credit.present?
%tr.total
%td.text-right{colspan: "3"}
%strong
= t :customer_credit
%td.text-right.total#customer-credit
%strong
= Spree::Money.new(-1 * @paid_with_credit).to_html
%tr.total
%td.text-right{colspan: "3"}
%strong
= t :order_amount_paid
%td.text-right.total#amount-paid
%td.text-right.total{id: "amount-paid"}
%strong
= Spree::Money.new(@payment_total).to_html
= order.display_payment_total.to_html
- if order.outstanding_balance.positive?
%tr.total
%td.text-right{colspan: "3"}
%h5.not-paid
= t :order_balance_due
%td.text-right.total.not-paid
%h5.not-paid#balance-due
%h5.not-paid
= order.display_outstanding_balance.to_html
- if order.outstanding_balance.negative?
%tr.total

View File

@@ -15,9 +15,9 @@
- if (order_payment_method = last_payment_method(order))
.text-big
= t :order_payment
%strong= order_payment_method&.display_name
%strong= order_payment_method&.name
%p.text-small.text-skinny.pre-line.word-wrap
%em= order_payment_method&.display_description
%em= order_payment_method&.description
- else
.text-big
= t(:no_payment_required)

View File

@@ -17,4 +17,4 @@
%p.callout{style: "margin-top: 40px"}
%strong
= t :email_payment_description
%p{style: "margin: 5px"}= last_payment_method(@order).display_description
%p{style: "margin: 5px"}= last_payment_method(@order).description

View File

@@ -1,41 +0,0 @@
%script{ type: "text/ng-template", id: "account/customer_account_transactions.html" }
.active_table.orders
%h3= t('.title')
- @shops.each do |shop|
- data_loader = CustomerAccountTransactions::DataLoaderService.new(user: @user, enterprise: shop)
%distributor.active_table_node.row.animate-repeat.closed.inactive{ "data-controller": "frontend-toggle-control", "data-frontend-toggle-control-selector-value": "#transaction-list-#{shop.id}", "data-frontend-toggle-control-target": "classUpdate" }
.small-12.columns
.row.active_table_row.skinny-head.margin-top.closed{ "data-action": "click->frontend-toggle-control#toggleDisplay", "data-frontend-toggle-control-target": "classUpdate" }
.columns.small-2
- if shop.logo_url(:medium)
= image_tag shop.logo_url(:medium), class: "margin-top account-logo"
- else
%i.ofn-i_059-producer
.columns.small-5
%h3.margin-top
= link_to shop.name, enterprise_url_selector(shop)
.columns.small-4.text-right
%h3.margin-top.distributor-balance
= t(".credit_available", credit: Spree::Money.new(data_loader.available_credit))
.columns.small-1.text-right
%h3.margin-top
%i.ofn-i_005-caret-down{ "data-frontend-toggle-control-target": "chevron" }
.row{ style: "display: none", id: "transaction-list-#{shop.id}" }
.columns.small-12.fat
%table
%tr
%th.order1= t(".transaction_date")
%th.order2= t(".description")
%th.order4= t(".amount")
%th.order5= t(".running_balance")
%tbody.transaction-group
- data_loader.customer_account_transactions.each do |transaction|
%tr.transaction-row.even
%td.order1
= transaction.updated_at.strftime("%Y-%m-%d")
%td.order2
= transaction.description
%td.order4
= Spree::Money.new(transaction.amount)
%td.order5
= Spree::Money.new(transaction.balance)

View File

@@ -19,21 +19,18 @@
= render 'orders'
= render 'cards'
= render 'transactions'
= render 'customer_account_transactions'
= render 'settings'
= render 'developer_settings' if @user.show_api_key_view
.row.tabset-ctrl#account-tabs{ style: 'margin-bottom: 100px', navigate: 'true', selected: 'orders', prefix: 'account' }
.small.12.medium-3.columns.tab{ name: "orders" }
.small.12.medium-2.columns.tab{ name: "orders" }
%a=t('.tabs.orders')
- if Spree::Config.stripe_connect_enabled && Stripe.publishable_key
.small.12.medium-3.columns.tab{ name: "cards" }
.small.12.medium-2.columns.tab{ name: "cards" }
%a=t('.tabs.cards')
.small.12.medium-3.columns.tab{ name: "transactions" }
.small.12.medium-2.columns.tab{ name: "transactions" }
%a=t('.tabs.transactions')
.small.12.medium-3.columns.tab{ name: "customer_account_transactions" }
%a=t('.tabs.customer_account_transactions')
.small.12.medium-3.columns.tab{ name: "settings" }
.small.12.medium-2.columns.tab{ name: "settings" }
%a=t('.tabs.settings')
// the api_keys partial is the only content for now, so we have to hide the whole tab for now
// if there is new content, we will need to handle this inside the developer_settings partial

View File

@@ -1,23 +0,0 @@
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ["chevron", "classUpdate"];
static values = { selector: String };
toggleDisplay(_event) {
if (this.hasChevronTarget) {
this.chevronTarget.classList.toggle("ofn-i_005-caret-down");
this.chevronTarget.classList.toggle("ofn-i_006-caret-up");
}
if (this.hasClassUpdateTarget) {
this.classUpdateTargets.forEach((t) => {
t.classList.toggle("closed");
t.classList.toggle("open");
});
}
const element = document.querySelector(this.selectorValue);
element.style.display = element.style.display === "none" ? "block" : "none";
}
}

View File

@@ -1,44 +0,0 @@
import { Controller } from "stimulus";
import TomSelect from "tom-select/dist/esm/tom-select.complete";
import showHttpError from "js/services/show_http_error";
export default class extends Controller {
connect() {
this.control = new TomSelect(this.element, {
create: true,
plugins: ["dropdown_input"],
labelField: "email",
load: this.#load.bind(this),
maxItems: 1,
persist: false,
searchField: ["email"],
shouldLoad: (query) => query.length > 2,
valueField: "email",
});
}
disconnect() {
if (this.control) this.control.destroy();
}
// private
#load(query, callback) {
const url = "/admin/search/known_users.json?q=" + encodeURIComponent(query);
fetch(url)
.then((response) => {
if (!response.ok) {
showHttpError(response.status);
throw response;
}
return response.json();
})
.then((json) => {
callback({ items: json });
})
.catch((error) => {
console.log(error);
callback();
});
}
}

View File

@@ -0,0 +1,110 @@
#change_type {
section {
margin: 2em 0 0 0;
&,
& * {
color: $spree-blue;
}
}
.description {
background-color: $spree-light-blue;
margin-top: -2em;
padding: 4em 2em 2em 1em;
@media all and (max-width: 786px) {
margin-bottom: 2em;
}
}
.admin-cta {
border: 1px solid $spree-blue;
@include border-radius(3px);
text-align: center;
padding: 1em;
}
.error {
display: block;
color: #f57e80;
border: 1px solid #f57e80;
background-color: #fde6e7;
@include border-radius(3px);
margin-bottom: 1em;
padding: 0.5em;
}
a.selector {
position: relative;
border: 2px solid black;
text-align: center;
width: 100%;
cursor: pointer;
&,
& * {
color: white;
}
&:after,
&:before {
top: 100%;
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&:after {
border-color: rgba(136, 183, 213, 0);
border-top-color: $spree-blue;
border-width: 12px;
margin-left: -12px;
}
&:hover {
&:after {
border-top-color: $spree-green;
}
}
&:before {
border-color: rgba(84, 152, 218, 0);
border-top-color: black;
border-width: 15px;
margin-left: -15px;
}
.bottom {
background: repeating-linear-gradient(
60deg,
rgba(84, 152, 218, 0),
rgba(84, 152, 218, 0) 5px,
rgba(255, 255, 255, 0.25) 5px,
rgba(255, 255, 255, 0.25) 10px
);
margin-top: 1em;
margin-left: -15px;
margin-right: -15px;
padding: 5px;
text-transform: uppercase;
}
&.selected {
background-color: black;
&:after,
&:hover &:after {
border-top-color: black;
}
}
}
}

View File

@@ -0,0 +1,21 @@
.dashboard_item.single-ent {
.header {
padding: 0.77778em 1.33333em 0.77778em 0.77778em;
height: auto !important;
}
.list {
.button.bottom {
width: 100%;
}
}
}
.button.big {
width: 100%;
font-size: 1rem;
@include border-radius(25px);
padding: 15px;
}

View File

@@ -0,0 +1,244 @@
div.dashboard_item {
margin-bottom: 30px;
.centered {
text-align: center;
}
.text-icon {
margin-top: 8px;
display: block;
font-size: 16px;
font-weight: bold;
color: #fff;
padding: 0px 6px;
border-radius: 10px;
&.green {
background-color: $spree-green;
}
&.warning {
background-color: $color-warning;
}
&.orange {
background-color: $color-warning;
}
}
div.header {
height: 50px;
border-radius: 6px 6px 0px 0px;
border: 1px solid $spree-blue;
position: relative;
a[ofn-with-tip] {
position: absolute;
right: 5px;
bottom: 5px;
}
&.warning {
border-color: $color-warning;
border-width: 3px;
h3 {
color: $color-warning;
}
}
&.orange {
border-color: $color-warning;
border-width: 3px;
h3 {
color: $color-warning;
}
}
h3.alpha {
height: 100%;
padding: 10px 5px 0px 3%;
}
a {
border-radius: 0px 4px 0px 0px;
height: 100%;
padding: 15px 2px 0px 2px;
}
}
.tabs {
height: 30px;
border: solid $spree-blue;
border-width: 0px 0px 1px 0px;
margin-top: 3px;
div.dashboard_tab {
cursor: pointer;
height: 30px;
color: #fff;
background-color: $spree-blue;
padding: 5px 5px 0px 5px;
text-align: center;
font-weight: bold;
border: solid $spree-blue;
border-width: 1px 1px 0px 1px;
&:hover {
background-color: $spree-green;
}
&.selected {
color: $spree-blue;
background-color: #fff;
}
}
}
.list {
max-height: 250px;
overflow-y: auto;
overflow-x: hidden;
&:focus {
outline: none;
}
}
.list-title {
border: solid $spree-blue;
border-width: 0px 1px 0px 1px;
span {
font-size: 105%;
padding: 10px 0px;
font-weight: bold;
}
span.alpha {
padding: 10px 2px 10px 5%;
}
}
.list-item {
border: solid $spree-blue;
border-width: 0px 1px 0px 1px;
height: 41px;
span.alpha {
font-weight: bold;
margin-left: -3px;
padding: 10px 2px 0px 5%;
}
span.omega {
padding-right: 13px;
margin-right: -3px;
text-align: right;
}
.icon-arrow-right {
padding-top: 6px;
font-size: 20px;
}
.icon-warning-sign {
color: $color-warning;
font-size: 30px;
}
.icon-remove-sign {
color: $color-warning;
font-size: 30px;
}
.icon-ok-sign {
color: $spree-green;
font-size: 30px;
}
&.orange {
color: $color-warning;
border: solid $color-warning;
}
&.warning {
color: $color-warning;
border: solid $color-warning;
}
&.orange,
&.warning {
border-width: 0px 3px 0px 3px;
}
&.even {
background-color: #fff;
}
&.odd {
background-color: $spree-light-blue;
}
&.even,
&.odd {
&:hover {
color: #ffffff;
background-color: $spree-green;
.icon-arrow-right {
color: #fff;
}
.icon-remove-sign {
color: #fff;
}
.icon-warning-sign {
color: #fff;
}
.icon-ok-sign {
color: #fff;
}
.text-icon {
&.green {
color: $spree-green;
background-color: #fff;
}
}
}
}
}
a.button {
color: #fff;
font-size: 110%;
font-weight: bold;
text-align: center;
&.orange {
background-color: $color-warning;
}
&.blue {
background-color: $spree-blue;
}
&.warning {
background-color: $color-warning;
}
&:hover {
background-color: $spree-green;
}
&.bottom {
border-radius: 0px 0px 6px 6px;
padding: 15px 15px;
}
}
}

View File

@@ -0,0 +1,271 @@
#content-header .ofn-drop-down {
border: none;
background-color: $spree-blue;
color: #fff;
float: none;
margin-left: 3px;
}
.ofn-drop-down {
.dropdown-content {
display: none;
}
.toggle-off {
display: none;
}
&:active:not(.disabled),
&:focus:not(.disabled) {
.dropdown-content {
display: inline-block;
}
}
}
.ofn-drop-down:hover,
.ofn-drop-down.expanded {
border: 1px solid #adadad;
color: #575757;
}
@mixin ofn-drop-down-style {
padding: 7px 15px;
border-radius: 3px;
border: 1px solid #d4d4d4;
background-color: #f5f5f5;
display: block;
color: #828282;
cursor: pointer;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
text-align: center;
margin-right: 10px;
&.disabled {
opacity: 0.5;
pointer-events: none;
&:hover {
cursor: default;
border-color: #d4d4d4;
color: #828282;
}
}
}
.ofn-drop-down-with-prepend {
display: flex;
&.right {
float: right;
}
.ofn-drop-down {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.ofn-drop-down-prepend {
@include ofn-drop-down-style;
border-right: none;
margin-left: 0;
margin-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
cursor: default;
}
}
.ofn-drop-down {
@include ofn-drop-down-style;
position: relative;
float: left;
&.right {
float: right;
margin-right: 0px;
margin-left: 10px;
}
&:hover,
&.expanded {
border: 1px solid #adadad;
color: #575757;
}
> span {
width: auto;
text-transform: uppercase;
font-size: 85%;
font-weight: 600;
}
.menu {
margin-top: 1px;
position: absolute;
float: none;
top: 100%;
left: 0px;
padding: 5px 0px;
border: 1px solid #adadad;
background-color: #ffffff;
box-shadow: 1px 3px 10px #888888;
z-index: 100;
white-space: nowrap;
.filter {
padding-left: 5px;
padding-right: 5px;
position: relative;
> input[type="text"] {
border: 1px solid rgba(18, 18, 18, 0.1);
width: 100%;
padding-left: 30px;
padding-top: 10px;
padding-bottom: 10px;
font-size: 13px;
color: #454545;
}
&:after {
content: "\f002";
font-family: FontAwesome;
position: absolute;
left: 15px;
top: 13px;
color: #454545;
}
}
.menu_item {
margin: 0px;
padding: 2px 10px;
color: #454545;
text-align: left;
display: block;
.check {
display: inline-block;
text-align: center;
width: 40px;
&:before {
content: "\00a0";
}
}
.name {
display: inline-block;
padding: 0px 15px 0px 0px;
}
&.selected {
.check:before {
content: "\2713";
}
}
&.hidden {
display: none;
}
}
.menu_item:hover {
background-color: #ededed;
}
}
> details {
// Override padding on ofn-drop-down-style
margin: -7px -15px;
padding: 7px 15px;
}
> details > summary {
display: inline-block;
list-style: none;
width: auto;
text-transform: uppercase;
font-size: 85%;
font-weight: 600;
// Override padding on ofn-drop-down-style to increase clickable area
margin: -8px -15px;
padding: 8px 15px;
}
> details > summary:after {
content: "\f0d7";
font-family: FontAwesome;
}
> details[open] > summary:after {
content: "\f0d8";
font-family: FontAwesome;
}
}
.ofn-drop-down-v2 {
border: 1px solid $pale-blue;
background-color: white;
padding: 0px;
&:hover {
border-color: $spree-blue;
}
.ofn-drop-down-label {
color: $color-3;
padding: 10px;
width: 235px;
display: flex;
justify-content: space-between;
&:hover {
color: $color-3;
}
.label {
padding-right: 10px;
}
.icon-caret-down,
.icon-caret-up {
padding-right: 0px;
}
}
.menu {
width: 100%;
}
.menu_items {
max-height: 200px;
overflow-y: scroll;
.menu_item {
margin-bottom: 5px;
color: #454545;
font-weight: 400;
cursor: pointer;
padding-top: 4px;
padding-bottom: 5px;
text-transform: uppercase;
font-size: 85%;
}
}
}
.ofn-drop-down.ofn-drop-down-v2 {
// Add very specific styling here for components that are in transition:
// ie. the ones using the two classes above
.ofn-drop-down-label {
padding-top: 7px;
padding-bottom: 7px;
}
}

View File

@@ -0,0 +1,120 @@
.enterprise_package_panel,
.enterprise_producer_panel {
.info {
p {
font-size: 1rem;
margin: 10px 0px;
}
}
a.selector {
display: block;
position: relative;
margin-bottom: 20px;
border: 2px solid black;
text-align: center;
// width: 100%;
cursor: pointer;
&,
& * {
color: white;
}
&:hover {
&:after {
border-top-color: $spree-green;
}
}
&.disabled {
background-color: #c1c1c1;
}
.bottom {
background: repeating-linear-gradient(
60deg,
rgba(84, 152, 218, 0),
rgba(84, 152, 218, 0) 5px,
rgba(255, 255, 255, 0.25) 5px,
rgba(255, 255, 255, 0.25) 10px
);
margin-top: 1em;
margin-left: -15px;
margin-right: -15px;
padding: 5px;
text-transform: uppercase;
}
&.selected {
background-color: #000000;
&:after {
top: 50%;
left: 0;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-top: 20px solid transparent;
border-bottom: 20px solid transparent;
border-right: 20px solid #000000;
margin-top: -20px;
margin-left: -20px;
}
}
}
}
.enterprise_status_panel {
.status-ok {
margin: 30px 0px;
i.icon-ok-sign {
color: $spree-green;
font-size: 1.5rem;
}
}
td.description {
font-size: 0.9rem;
}
td.severity {
text-align: center;
i {
font-size: 1.5rem;
&.issue {
color: $color-warning;
}
&.warning {
color: #ff9848;
}
}
}
}
tags-input .tags li.tag-item {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
margin: 2px 0 2px 3px;
background-image: none;
background-color: #5fa5e8;
border: none;
box-shadow: none;
color: white !important;
font-size: 85%;
height: 25px;
}
tags-input .tags .tag-item .remove-button {
color: white;
}
table th.actions .no-text[class*="icon-"],
table td.actions .no-text[class*="icon-"] {
cursor: pointer;
}

View File

@@ -4,17 +4,8 @@ form[name="enterprise_form"] {
}
table.managers {
th {
div[data-controller="tooltip"] {
display: inline-block;
}
}
.center {
text-align: center;
}
i.role {
float: right;
margin-left: 0.5em;
font-size: 1.5em;
cursor: pointer;

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