mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-11 03:40:20 +00:00
Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9816332601 | ||
|
|
4810b02233 | ||
|
|
f8716f8005 | ||
|
|
5cf213f22a | ||
|
|
251a1acffc | ||
|
|
d5dec05ab1 | ||
|
|
7790259c27 | ||
|
|
6a99d2a3c8 | ||
|
|
e7a2b7ea48 | ||
|
|
bd0dcd99f3 | ||
|
|
bc23423521 | ||
|
|
a4ca56c7a5 | ||
|
|
9f7655852d | ||
|
|
ec106a8f83 | ||
|
|
2e7237197a | ||
|
|
25c579c478 | ||
|
|
66b820869c | ||
|
|
ffc819ea76 | ||
|
|
ed3f928783 | ||
|
|
63a9601812 | ||
|
|
3b068b7125 | ||
|
|
597c0590ed | ||
|
|
b9b91620ef | ||
|
|
c67d47a773 | ||
|
|
85e0da8aeb | ||
|
|
34c91613f7 | ||
|
|
219e3ca9c8 | ||
|
|
628810eb33 | ||
|
|
d95aac333b | ||
|
|
3e2e7f1799 | ||
|
|
ee13a3abaf | ||
|
|
d5bd8fa086 | ||
|
|
5e4cd4d51d | ||
|
|
c0823d24c2 | ||
|
|
41e4fd79a3 | ||
|
|
a60afd10e4 | ||
|
|
b42b10fcd1 | ||
|
|
c8dbf4c6f0 | ||
|
|
f5a3093e41 | ||
|
|
5a376c9106 | ||
|
|
152fd15bd0 | ||
|
|
5bdb6e6d69 | ||
|
|
cb6b1f2dd0 | ||
|
|
130401263a | ||
|
|
29a8a6641c | ||
|
|
7ab33d86f1 | ||
|
|
28241756aa | ||
|
|
fec5516fce | ||
|
|
6aa4bf7a33 | ||
|
|
c58a65a52b | ||
|
|
e21fadd124 | ||
|
|
1b468522e6 | ||
|
|
be7be9bbc6 | ||
|
|
6915836a14 | ||
|
|
e10fd0b020 | ||
|
|
45786780a8 | ||
|
|
7d400e9860 | ||
|
|
b50f299381 | ||
|
|
d706b3a6c2 | ||
|
|
153b94f18e | ||
|
|
71daccb49a | ||
|
|
bca5ee226d | ||
|
|
fe00d1f813 | ||
|
|
57a69f7fa3 | ||
|
|
f4d972fc5e | ||
|
|
6db3b44e92 | ||
|
|
f7f7a5738a | ||
|
|
da912f21b3 | ||
|
|
e2146eb1a3 | ||
|
|
8e19bfca3c | ||
|
|
255836b4d2 | ||
|
|
fcd7b457e6 | ||
|
|
eb1daf425a | ||
|
|
06bc6c276a | ||
|
|
6854a53bd1 | ||
|
|
57186a74a8 | ||
|
|
3575952d4b | ||
|
|
11203da5ca | ||
|
|
86091a5a79 | ||
|
|
f4328f1d18 | ||
|
|
2334a695a8 | ||
|
|
c9418c52e4 | ||
|
|
ce1e38f97b | ||
|
|
e16595eacb | ||
|
|
510fd4867b | ||
|
|
53d6886f20 | ||
|
|
dad9014a60 | ||
|
|
abe9032910 | ||
|
|
272cf9ae87 | ||
|
|
1d5bc14f2f | ||
|
|
0332049551 | ||
|
|
0ffd4dcc35 | ||
|
|
e899d1b7fd | ||
|
|
ed331dc104 | ||
|
|
1bec028a09 | ||
|
|
59547ba9e4 | ||
|
|
9fb8bb15e8 | ||
|
|
8aa89c0bf7 | ||
|
|
447d80c960 | ||
|
|
67853bb976 | ||
|
|
d57274fc4c | ||
|
|
f063e2e8c6 | ||
|
|
d3eb887664 | ||
|
|
efae11e2af | ||
|
|
1554459eb9 | ||
|
|
50265780cf | ||
|
|
7433f6d183 | ||
|
|
f1071575cd | ||
|
|
7a4beb8b22 | ||
|
|
9a48ee16cc | ||
|
|
50c0e8af7d | ||
|
|
1cf2928f9f | ||
|
|
6cacde837d | ||
|
|
1d2d661675 | ||
|
|
5029c03205 | ||
|
|
2b648f3f3c | ||
|
|
b2e847b551 | ||
|
|
4873fd3275 | ||
|
|
e0ad4363a9 | ||
|
|
46de21ea2b | ||
|
|
efdbf25f86 | ||
|
|
a069e4247f | ||
|
|
7010cda9f7 | ||
|
|
498ed5a3ec | ||
|
|
c7d4c6f3c4 | ||
|
|
70b2a6d999 | ||
|
|
36e3e16ba0 | ||
|
|
0f047e2c25 | ||
|
|
ef7bd083ed | ||
|
|
c13785f2e3 | ||
|
|
28dde86960 | ||
|
|
08691f81a1 | ||
|
|
0c0304b1c1 | ||
|
|
7922bf7b65 | ||
|
|
2d46676bb4 | ||
|
|
2808a41f0d | ||
|
|
18869979db | ||
|
|
708dbb2270 | ||
|
|
de52e21ee9 | ||
|
|
9488e9b459 | ||
|
|
503429960a | ||
|
|
83ec97e720 | ||
|
|
fd178ee80b | ||
|
|
e4db20f86e | ||
|
|
58520a0c4c | ||
|
|
0bc4b1c885 | ||
|
|
b7a1754879 | ||
|
|
560348722c | ||
|
|
6d17cf50fb | ||
|
|
7b715bf6c7 | ||
|
|
ab811b2c83 | ||
|
|
85c903cb7f | ||
|
|
2cfd386ad7 | ||
|
|
ce94b394b2 | ||
|
|
98775bfdb8 | ||
|
|
47ef21deb3 | ||
|
|
e98244fe63 | ||
|
|
b348536d12 | ||
|
|
b528bb47a0 | ||
|
|
5a9aa87831 | ||
|
|
8cfab08f9e | ||
|
|
e814fdd447 | ||
|
|
4d6231105f | ||
|
|
3a75c3446e | ||
|
|
29a76fd721 | ||
|
|
593bd89095 | ||
|
|
0079ed219b | ||
|
|
5957d99812 | ||
|
|
89453ec758 | ||
|
|
0de4f2f596 | ||
|
|
183cbecef6 | ||
|
|
ab6a49e568 | ||
|
|
52ddb29dc7 | ||
|
|
5ce7905a33 | ||
|
|
8748bd76e2 | ||
|
|
30f702ea96 |
9
.github/ISSUE_TEMPLATE/release.md
vendored
9
.github/ISSUE_TEMPLATE/release.md
vendored
@@ -27,7 +27,12 @@ assignees: ''
|
||||
- [ ] Move this issue to Test Ready.
|
||||
- [ ] Notify `@testers` in [#testing].
|
||||
- [ ] Test build: [Deploy to Staging] with release tag.
|
||||
- [ ] Notify a deployer to deploy it
|
||||
- [ ] 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.
|
||||
|
||||
## 3. Deployment at beginning of week
|
||||
|
||||
@@ -57,4 +62,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
|
||||
|
||||
1
Gemfile
1
Gemfile
@@ -203,6 +203,7 @@ 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'
|
||||
|
||||
26
Gemfile.lock
26
Gemfile.lock
@@ -170,7 +170,7 @@ GEM
|
||||
addressable (2.8.8)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
afm (0.2.2)
|
||||
afm (1.0.0)
|
||||
angular-rails-templates (1.4.0)
|
||||
railties (>= 5.0, < 8.2)
|
||||
sprockets (>= 3.0, < 5)
|
||||
@@ -313,7 +313,7 @@ GEM
|
||||
eventmachine (>= 1.0.0.beta.1)
|
||||
email_validator (2.2.4)
|
||||
activemodel
|
||||
erb (6.0.1)
|
||||
erb (6.0.2)
|
||||
erubi (1.13.1)
|
||||
et-orbi (1.4.0)
|
||||
tzinfo
|
||||
@@ -405,7 +405,7 @@ GEM
|
||||
reline
|
||||
htmlentities (4.4.2)
|
||||
http_parser.rb (0.8.0)
|
||||
i18n (1.14.7)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.9.2)
|
||||
i18n (>= 0.6.6)
|
||||
@@ -507,7 +507,8 @@ GEM
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.6)
|
||||
minitest (6.0.1)
|
||||
minitest (6.0.2)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
monetize (1.13.0)
|
||||
money (~> 6.12)
|
||||
@@ -584,7 +585,7 @@ GEM
|
||||
xml-simple
|
||||
paypal-sdk-merchant (1.117.2)
|
||||
paypal-sdk-core (~> 0.3.0)
|
||||
pdf-reader (2.15.0)
|
||||
pdf-reader (2.15.1)
|
||||
Ascii85 (>= 1.0, < 3.0, != 2.0.0)
|
||||
afm (>= 0.2.1, < 2)
|
||||
hashery (~> 2.0)
|
||||
@@ -670,8 +671,8 @@ GEM
|
||||
activesupport (>= 4.2)
|
||||
choice (~> 0.2.0)
|
||||
ruby-graphviz (~> 1.2)
|
||||
rails-html-sanitizer (1.6.2)
|
||||
loofah (~> 2.21)
|
||||
rails-html-sanitizer (1.7.0)
|
||||
loofah (~> 2.25)
|
||||
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)
|
||||
@@ -857,6 +858,9 @@ 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)
|
||||
@@ -904,7 +908,7 @@ GEM
|
||||
tsort (0.2.0)
|
||||
ttfunk (1.8.0)
|
||||
bigdecimal (~> 3.1)
|
||||
turbo-rails (2.0.20)
|
||||
turbo-rails (2.0.23)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
turbo_power (0.7.0)
|
||||
@@ -932,8 +936,7 @@ GEM
|
||||
public_suffix
|
||||
validates_lengths_from_database (0.8.0)
|
||||
activerecord (>= 4)
|
||||
vcr (6.3.1)
|
||||
base64
|
||||
vcr (6.4.0)
|
||||
view_component (4.1.1)
|
||||
actionview (>= 7.1.0, < 8.2)
|
||||
activesupport (>= 7.1.0, < 8.2)
|
||||
@@ -970,7 +973,7 @@ GEM
|
||||
xml-simple (1.1.8)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.7.4)
|
||||
zeitwerk (2.7.5)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -1101,6 +1104,7 @@ DEPENDENCIES
|
||||
spring
|
||||
spring-commands-rspec
|
||||
spring-commands-rubocop
|
||||
spring-watcher-listen
|
||||
sprockets (~> 3.7)
|
||||
state_machines-activerecord
|
||||
stimulus_reflex
|
||||
|
||||
@@ -4,16 +4,12 @@ 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()
|
||||
@@ -35,26 +31,6 @@ 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))
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
angular.module('Darkswarm').controller "HomeCtrl", ($scope) ->
|
||||
$scope.brandStoryExpanded = false
|
||||
|
||||
$scope.toggleBrandStory = ->
|
||||
$scope.brandStoryExpanded = !$scope.brandStoryExpanded
|
||||
@@ -9,6 +9,11 @@
|
||||
padding: 1.2rem;
|
||||
}
|
||||
|
||||
&.big {
|
||||
max-height: 50em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# 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
|
||||
@@ -67,7 +67,6 @@ 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'
|
||||
@@ -163,9 +162,11 @@ module Admin
|
||||
end
|
||||
|
||||
def destroy
|
||||
if @object.destroy
|
||||
@object.transaction do
|
||||
@object.destroy!
|
||||
flash.now[:success] = flash_message_for(@object, :successfully_removed)
|
||||
else
|
||||
rescue StandardError
|
||||
Rails.logger.error @object.errors.full_messages.to_sentence
|
||||
flash.now[:error] = @object.errors.full_messages.to_sentence
|
||||
end
|
||||
|
||||
@@ -177,7 +178,7 @@ module Admin
|
||||
protected
|
||||
|
||||
def delete_custom_tab
|
||||
@object.custom_tab.destroy if @object.custom_tab.present?
|
||||
@object.custom_tab.presence&.destroy
|
||||
enterprise_params.delete(:custom_tab_attributes)
|
||||
end
|
||||
|
||||
@@ -240,9 +241,7 @@ module Admin
|
||||
enterprises = OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, @order_cycle)
|
||||
.visible_enterprises
|
||||
|
||||
if enterprises.present?
|
||||
enterprises.includes(supplied_products: [:variants, :image])
|
||||
end
|
||||
enterprises.presence&.includes(supplied_products: [:variants, :image])
|
||||
when :index
|
||||
if spree_current_user.admin?
|
||||
OpenFoodNetwork::Permissions.new(spree_current_user).
|
||||
@@ -314,14 +313,6 @@ 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)
|
||||
|
||||
31
app/controllers/admin/user_invitations_controller.rb
Normal file
31
app/controllers/admin/user_invitations_controller.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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
|
||||
@@ -0,0 +1,34 @@
|
||||
# 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
|
||||
@@ -31,6 +31,11 @@ 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')
|
||||
@@ -121,7 +126,9 @@ 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
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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
|
||||
@@ -15,6 +15,7 @@ module Spree
|
||||
Spree::Gateway::StripeSCA
|
||||
Spree::PaymentMethod::Check
|
||||
Spree::PaymentMethod::Taler
|
||||
Spree::PaymentMethod::CustomerCredit
|
||||
}.freeze
|
||||
|
||||
def create
|
||||
|
||||
@@ -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
|
||||
before_action :load_data, except: [:credit_customer]
|
||||
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,6 +92,18 @@ 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
|
||||
@@ -185,7 +197,7 @@ module Spree
|
||||
end
|
||||
|
||||
def allowed_events
|
||||
%w{capture void_transaction credit refund resend_authorization_email
|
||||
%w{capture void_transaction credit refund internal_void resend_authorization_email
|
||||
capture_and_complete_order}
|
||||
end
|
||||
|
||||
|
||||
@@ -29,7 +29,13 @@ module Spree
|
||||
hide_ofn_navigation(@order.distributor)
|
||||
end
|
||||
|
||||
def show; 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 empty
|
||||
if @order = current_order
|
||||
|
||||
50
app/forms/user_invitation.rb
Normal file
50
app/forms/user_invitation.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
# 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
|
||||
@@ -83,31 +83,6 @@ 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
|
||||
@@ -139,7 +114,7 @@ module CheckoutHelper
|
||||
def stripe_card_options(cards)
|
||||
cards.map do |cc|
|
||||
[
|
||||
"#{cc.brand} #{cc.last_digits} #{I18n.t(:card_expiry_abbreviation)}:" \
|
||||
"#{cc.cc_type} #{cc.last_digits} #{I18n.t(:card_expiry_abbreviation)}:" \
|
||||
"#{cc.month.to_s.rjust(2, '0')}/#{cc.year}", cc.id
|
||||
]
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ module ReportsHelper
|
||||
|
||||
next unless payment_method
|
||||
|
||||
[payment_method.name, payment_method.id]
|
||||
[payment_method.display_name, payment_method.id]
|
||||
end.compact.uniq
|
||||
end
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ module Spree
|
||||
end
|
||||
|
||||
def payment_method_name(payment)
|
||||
payment_method(payment)&.name
|
||||
payment_method(payment)&.display_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
30
app/json_schemas/customer_account_transaction_schema.rb
Normal file
30
app/json_schemas/customer_account_transaction_schema.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# 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
|
||||
@@ -6,6 +6,7 @@ class PaymentMailer < ApplicationMailer
|
||||
def authorize_payment(payment)
|
||||
@payment = payment
|
||||
@order = @payment.order
|
||||
@hide_ofn_navigation = @payment.order.distributor.hide_ofn_navigation
|
||||
I18n.with_locale valid_locale(@order.user) do
|
||||
mail(to: @order.email,
|
||||
subject: default_i18n_subject(distributor: @order.distributor.name),
|
||||
|
||||
@@ -11,6 +11,7 @@ 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),
|
||||
@@ -51,6 +52,7 @@ 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
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
|
||||
@@ -19,6 +19,7 @@ class SubscriptionMailer < ApplicationMailer
|
||||
@type = 'empty'
|
||||
@changes = changes
|
||||
@order = order
|
||||
@hide_ofn_navigation = @order.distributor.hide_ofn_navigation
|
||||
send_mail(order)
|
||||
end
|
||||
|
||||
@@ -26,11 +27,13 @@ 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
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ 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
|
||||
|
||||
38
app/models/customer_account_transaction.rb
Normal file
38
app/models/customer_account_transaction.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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
|
||||
@@ -294,7 +294,13 @@ class Enterprise < ApplicationRecord
|
||||
contact || owner
|
||||
end
|
||||
|
||||
def update_contact(user_id)
|
||||
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)
|
||||
|
||||
enterprise_roles.update_all(["receives_notifications=(user_id=?)", user_id])
|
||||
end
|
||||
|
||||
@@ -576,7 +582,7 @@ class Enterprise < ApplicationRecord
|
||||
end
|
||||
|
||||
def set_default_contact
|
||||
update_contact owner_id
|
||||
self.contact_id = owner_id
|
||||
end
|
||||
|
||||
def relate_to_owners_enterprises
|
||||
|
||||
@@ -4,7 +4,7 @@ class Invoice < ApplicationRecord
|
||||
self.belongs_to_required_by_default = false
|
||||
|
||||
belongs_to :order, class_name: 'Spree::Order'
|
||||
serialize :data, Hash, coder: YAML
|
||||
serialize :data, type: Hash, coder: YAML
|
||||
before_validation :serialize_order
|
||||
after_create :cancel_previous_invoices
|
||||
default_scope { order(created_at: :desc) }
|
||||
|
||||
@@ -18,7 +18,7 @@ class Invoice
|
||||
end
|
||||
|
||||
def payment_method_name
|
||||
payment_method&.name
|
||||
payment_method&.display_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class Invoice
|
||||
class DataPresenter
|
||||
class PaymentMethod < Invoice::DataPresenter::Base
|
||||
attributes :id, :name, :description
|
||||
attributes :id, :display_name, :display_description
|
||||
invoice_generation_attributes :id
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -76,6 +76,7 @@ class ProxyOrder < ApplicationRecord
|
||||
|
||||
def cart?
|
||||
order&.state == 'complete' &&
|
||||
order_cycle.orders_close_at.present? &&
|
||||
order_cycle.orders_close_at > Time.zone.now
|
||||
end
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ class ReportRenderingOptions < ApplicationRecord
|
||||
self.belongs_to_required_by_default = false
|
||||
|
||||
belongs_to :user, class_name: "Spree::User"
|
||||
serialize :options, Hash, coder: YAML
|
||||
serialize :options, type: Hash, coder: YAML
|
||||
end
|
||||
|
||||
@@ -61,6 +61,7 @@ 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.
|
||||
@@ -191,7 +192,7 @@ module Spree
|
||||
user.enterprises.include? stripe_account.enterprise
|
||||
end
|
||||
|
||||
can [:admin, :create], :manager_invitation
|
||||
can [:admin, :create], UserInvitation
|
||||
|
||||
can [:admin, :index, :destroy], :oidc_setting
|
||||
|
||||
@@ -457,5 +458,9 @@ 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
|
||||
|
||||
@@ -25,9 +25,6 @@ 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]}"
|
||||
|
||||
@@ -52,9 +52,9 @@ module Spree
|
||||
|
||||
def supports?(source)
|
||||
return true unless provider_class.respond_to? :supports?
|
||||
return false unless source.brand
|
||||
return false unless source.cc_type
|
||||
|
||||
provider_class.supports?(source.brand)
|
||||
provider_class.supports?(source.cc_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -730,5 +730,9 @@ module Spree
|
||||
adjustment.update_adjustment!(force: true)
|
||||
update_totals_and_states
|
||||
end
|
||||
|
||||
def apply_customer_credit
|
||||
Orders::CustomerCreditService.new(self).apply
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -78,6 +78,8 @@ 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|
|
||||
|
||||
@@ -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'
|
||||
belongs_to :payment_method, class_name: "Spree::PaymentMethod", inverse_of: :payments
|
||||
|
||||
has_many :offsets, -> { where("source_type = 'Spree::Payment' AND amount < 0").completed },
|
||||
class_name: "Spree::Payment", foreign_key: :source_id,
|
||||
@@ -115,12 +115,20 @@ module Spree
|
||||
Alert.raise(
|
||||
e,
|
||||
metadata: {
|
||||
event_tye: "ofn.payment_transition", payment_id: payment.id, event: transition.to
|
||||
event_type: "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
|
||||
|
||||
@@ -4,6 +4,8 @@ module Spree
|
||||
class Payment < ApplicationRecord
|
||||
module Processing
|
||||
def process!
|
||||
return internal_purchase! if payment_method.internal?
|
||||
|
||||
return unless validate!
|
||||
|
||||
purchase!
|
||||
@@ -20,6 +22,17 @@ 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:)
|
||||
@@ -131,6 +144,27 @@ 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
|
||||
|
||||
@@ -248,6 +282,7 @@ 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
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Spree
|
||||
class PaymentMethod < ApplicationRecord
|
||||
class PaymentMethod < ApplicationRecord # rubocop:disable Metrics/ClassLength
|
||||
include CalculatedAdjustments
|
||||
include PaymentMethodDistributors
|
||||
|
||||
@@ -11,9 +11,11 @@ module Spree
|
||||
acts_as_paranoid
|
||||
|
||||
DISPLAY = [:both, :back_end].freeze
|
||||
default_scope -> { where(deleted_at: nil) }
|
||||
INTERNAL = Spree::PaymentMethod::CustomerCredit.to_s
|
||||
default_scope -> { where(deleted_at: nil).where.not(type: INTERNAL) }
|
||||
|
||||
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
|
||||
@@ -52,6 +54,12 @@ 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
|
||||
@@ -109,9 +117,23 @@ 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
|
||||
|
||||
@@ -126,5 +148,11 @@ 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
|
||||
|
||||
110
app/models/spree/payment_method/customer_credit.rb
Normal file
110
app/models/spree/payment_method/customer_credit.rb
Normal file
@@ -0,0 +1,110 @@
|
||||
# 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
|
||||
@@ -23,6 +23,7 @@ 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
|
||||
|
||||
@@ -12,7 +12,8 @@ class CustomersWithBalanceQuery
|
||||
joins(left_join_complete_orders).
|
||||
group("customers.id").
|
||||
select("customers.*").
|
||||
select("#{outstanding_balance_sum} AS balance_value")
|
||||
select("#{outstanding_balance_sum} AS balance_value").
|
||||
select("#{available_credit} AS credit_value")
|
||||
end
|
||||
|
||||
private
|
||||
@@ -34,4 +35,21 @@ 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
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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
|
||||
@@ -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
|
||||
attributes :balance, :balance_status, :available_credit, :available_credit_url
|
||||
|
||||
delegate :balance_value, to: :object
|
||||
delegate :balance_value, :credit_value, to: :object
|
||||
|
||||
def balance
|
||||
Spree::Money.new(balance_value, currency: CurrentConfig.get(:currency)).to_s
|
||||
@@ -24,6 +24,14 @@ 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
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class CustomerAccountTransactionSerializer < Api::V1::BaseSerializer
|
||||
attributes :id, :customer_id, :amount, :currency, :description, :balance
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
class Invoice
|
||||
class PaymentMethodSerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :description
|
||||
attributes :id, :display_name, :display_description
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,7 +61,7 @@ module Checkout
|
||||
def set_payment_amount
|
||||
return unless @order_params[:payments_attributes]
|
||||
|
||||
@order_params[:payments_attributes].first[:amount] = order.total
|
||||
@order_params[:payments_attributes].first[:amount] = order.outstanding_balance.amount
|
||||
end
|
||||
|
||||
def set_existing_card
|
||||
|
||||
@@ -28,6 +28,8 @@ 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
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# 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
|
||||
@@ -19,8 +19,9 @@ module Orders
|
||||
end
|
||||
|
||||
def reset_other!(current_user, current_customer)
|
||||
reset_user_and_customer(current_user)
|
||||
reset_user(current_user)
|
||||
reset_order_cycle(current_customer)
|
||||
order.customer = current_customer
|
||||
order.save!
|
||||
end
|
||||
|
||||
@@ -28,7 +29,7 @@ module Orders
|
||||
|
||||
attr_reader :order, :distributor, :current_user
|
||||
|
||||
def reset_user_and_customer(current_user)
|
||||
def reset_user(current_user)
|
||||
return unless current_user
|
||||
|
||||
order.associate_user!(current_user) if order.user.blank? || order.email.blank?
|
||||
|
||||
115
app/services/orders/customer_credit_service.rb
Normal file
115
app/services/orders/customer_credit_service.rb
Normal file
@@ -0,0 +1,115 @@
|
||||
# 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
|
||||
@@ -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
|
||||
:remove_logo, :remove_promo_image, :remove_white_label_logo, :contact_id
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
= 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)
|
||||
@@ -48,6 +48,7 @@
|
||||
%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' }
|
||||
@@ -61,6 +62,7 @@
|
||||
%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' }
|
||||
@@ -73,10 +75,13 @@
|
||||
%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')
|
||||
@@ -98,9 +103,14 @@
|
||||
%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
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
%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')}"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
%tbody
|
||||
- @payment_methods.each do |payment_method|
|
||||
%tr
|
||||
%td= payment_method.name
|
||||
%td= payment_method.display_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
|
||||
|
||||
@@ -1,75 +1,54 @@
|
||||
- owner_email = @enterprise&.owner&.email || ""
|
||||
- full_permissions = (spree_current_user.admin? || spree_current_user == @enterprise&.owner)
|
||||
|
||||
.row
|
||||
.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
|
||||
= t '.description'
|
||||
|
||||
.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
|
||||
- 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('.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
|
||||
%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}" }
|
||||
%td
|
||||
- # Ignore this input in the submit
|
||||
= hidden_field_tag :ignored, nil, class: "select2 fullwidth", 'user-select' => 'newManager', 'ng-model' => 'newManager'
|
||||
= 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
|
||||
%td.actions
|
||||
%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}" }
|
||||
- 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" }
|
||||
|
||||
- else
|
||||
- @enterprise.users.each do |manager|
|
||||
= manager.email
|
||||
%br
|
||||
%a.button{ href: "#{new_admin_enterprise_user_invitation_path(@enterprise)}", data: { turbo_stream: true, turbo: true } }
|
||||
%i.icon-plus
|
||||
= t('.invite_manager')
|
||||
%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 }
|
||||
- else
|
||||
- @enterprise.users.each do |manager|
|
||||
= manager.email
|
||||
%br
|
||||
|
||||
@@ -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.name
|
||||
= distributor_payment_method.payment_method.display_name
|
||||
- distributor.payment_methods.inactive_or_backend.each do |payment_method|
|
||||
%label.disabled
|
||||
= check_box_tag nil, nil, false, disabled: true
|
||||
= payment_method.name
|
||||
= payment_method.display_name
|
||||
= "(#{t('.back_end')})"
|
||||
- if distributor.payment_methods.available.none?
|
||||
%p
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
- link = pagy_anchor(pagy)
|
||||
|
||||
.pagination{ "data-controller": "search" }
|
||||
%nav.pagination{ "aria-label": t('.navigation'), "data-controller": "search" }
|
||||
- if pagy.prev
|
||||
%button.page.prev{ data: { action: 'click->search#changePage', page: pagy.prev } }
|
||||
%a.page.prev{ href: "#", rel: "prev", "aria-label": t('.previous'), 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')
|
||||
|
||||
- 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
|
||||
%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
|
||||
|
||||
- elsif item.is_a?(String) # current page
|
||||
%button.page.current.active= 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 == :gap # page gap
|
||||
%span.page.gap.pagination-ellipsis!= pagy_t('pagy.gap')
|
||||
- elsif item == :gap # page gap
|
||||
%li
|
||||
%span.page.gap.pagination-ellipsis!= pagy_t('pagy.gap')
|
||||
|
||||
- if pagy.next
|
||||
%button.page.next{ data: { action: 'click->search#changePage', page: pagy.next } }
|
||||
%a.page.next{ href: "#", rel: "next", "aria-label": t('.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')
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
= 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 }
|
||||
17
app/views/admin/user_invitations/new.turbo_stream.haml
Normal file
17
app/views/admin/user_invitations/new.turbo_stream.haml
Normal file
@@ -0,0 +1,17 @@
|
||||
= 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")
|
||||
@@ -8,10 +8,17 @@
|
||||
.checkout-title
|
||||
= t("checkout.step2.payment_method.title")
|
||||
|
||||
- if @order.zero_priced_order?
|
||||
- if @order.zero_priced_order? || @order.outstanding_balance.zero?
|
||||
%h3= t(:no_payment_required)
|
||||
= hidden_field_tag "order[payments_attributes][][amount]", 0
|
||||
- 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))
|
||||
|
||||
- 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|
|
||||
@@ -23,13 +30,13 @@
|
||||
"data-action": "paymentmethod#selectPaymentMethod",
|
||||
"data-paymentmethod-id": "#{payment_method.id}",
|
||||
"data-paymentmethod-target": "input"
|
||||
= f.label :payment_method_id, "#{payment_method.name}", for: "payment_method_#{payment_method.id}"
|
||||
= f.label :payment_method_id, "#{payment_method.display_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.description))
|
||||
= simple_format(html_escape(payment_method.display_description))
|
||||
.paymentmethod-form
|
||||
= render partial: "checkout/payment/#{payment_method.method_type}", locals: { payment_method: payment_method, f: f }
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
- payment_method = last_payment_method(@order)
|
||||
%div
|
||||
- if payment_method
|
||||
= payment_method.name
|
||||
= payment_method.display_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&.description
|
||||
= payment_method&.display_description
|
||||
|
||||
|
||||
.checkout-substep
|
||||
@@ -56,7 +56,8 @@
|
||||
.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_total.to_html
|
||||
|
||||
.summary-right-line-value#order_total= @order.display_outstanding_balance.to_html
|
||||
|
||||
.summary-right-line
|
||||
.summary-right-line-label= t :order_produce
|
||||
@@ -78,6 +79,11 @@
|
||||
.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 }
|
||||
|
||||
@@ -6,21 +6,21 @@
|
||||
%p
|
||||
= t :brandstory_intro
|
||||
|
||||
#brand-story-text.hide-show.slideable
|
||||
%p
|
||||
= t :brandstory_part1
|
||||
%p
|
||||
= t :brandstory_part2
|
||||
%p
|
||||
= t :brandstory_part3
|
||||
%p
|
||||
= t :brandstory_part4
|
||||
%p
|
||||
%strong
|
||||
= t :brandstory_part5_strong
|
||||
%p
|
||||
= t :brandstory_part6
|
||||
|
||||
%a.text-vbig{"slide-toggle" => "#brand-story-text", "ng-click" => "toggleBrandStory()"}
|
||||
%i.ofn-i_005-caret-down{"ng-hide" => "brandStoryExpanded"}
|
||||
%i.ofn-i_006-caret-up{ "ng-show" => "brandStoryExpanded"}
|
||||
%details#brand-story-text
|
||||
%summary
|
||||
%i.ofn-i_005-caret-down
|
||||
%i.ofn-i_006-caret-up
|
||||
.brand-story-content
|
||||
%p
|
||||
= t :brandstory_part1
|
||||
%p
|
||||
= t :brandstory_part2
|
||||
%p
|
||||
= t :brandstory_part3
|
||||
%p
|
||||
= t :brandstory_part4
|
||||
%p
|
||||
%strong
|
||||
= t :brandstory_part5_strong
|
||||
%p
|
||||
= t :brandstory_part6
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- content_for :page_alert do
|
||||
= render "shared/menu/alert"
|
||||
|
||||
%div{"ng-controller" => "HomeCtrl"}
|
||||
%div
|
||||
= render "home/tagline"
|
||||
|
||||
#panes
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
%label{for: path}= name
|
||||
|
||||
%input.medium.input-text{attributes}
|
||||
|
||||
%small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"}
|
||||
= "{{ fieldErrors('#{path}') }}"
|
||||
@@ -1,6 +0,0 @@
|
||||
%label{for: path}= name
|
||||
|
||||
= select_tag path, options_for_select(options), attributes
|
||||
|
||||
%small.error.medium.input-text{"ng-show" => "!fieldValid('#{path}')"}
|
||||
= "{{ fieldErrors('#{path}') }}"
|
||||
@@ -17,4 +17,4 @@
|
||||
%p.callout{style: "margin-top: 40px"}
|
||||
%strong
|
||||
= t :email_payment_description
|
||||
%p{style: "margin: 5px"}= @order.last_payment_method.description
|
||||
%p{style: "margin: 5px"}= @order.last_payment_method.display_description
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- content_for :page_title do
|
||||
= t('.editing_payment_method')
|
||||
%i.icon-arrow-right
|
||||
= @payment_method.name
|
||||
= @payment_method.display_name
|
||||
- content_for :page_actions do
|
||||
%li
|
||||
= button_link_to t('.new'), spree.new_admin_payment_method_path, icon: 'icon-plus'
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
%tbody
|
||||
- @payment_methods.each do |method|
|
||||
%tr{class: "#{cycle('odd', 'even')}", id: "#{spree_dom_id method}"}
|
||||
%td.align-center= method.name
|
||||
%td.align-center= method.display_name
|
||||
%td.align-center
|
||||
- method.distributors.each do |distributor|
|
||||
= distributor.name
|
||||
|
||||
@@ -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' }
|
||||
= t(method.name, scope: :payment_methods, default: method.name)
|
||||
= method.display_name
|
||||
.payment-method-settings
|
||||
- @payment_methods.each do |method|
|
||||
.payment-methods{id: "payment_method_#{method.id}"}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
= 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'
|
||||
|
||||
@@ -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-v3', media: "screen, print"
|
||||
= stylesheet_pack_tag 'admin-style', media: "screen, print"
|
||||
= render "layouts/bugsnag_js"
|
||||
|
||||
- if content_for? :minimal_js
|
||||
|
||||
@@ -4,25 +4,21 @@
|
||||
|
||||
#new_variant
|
||||
|
||||
%table.index.sortable{"data-sortable-link" => update_positions_admin_product_variants_path(@product)}
|
||||
%table.index
|
||||
%colgroup
|
||||
%col{style: "width: 5%"}/
|
||||
%col{style: "width: 25%"}/
|
||||
%col{style: "width: 20%"}/
|
||||
%col{style: "width: 20%"}/
|
||||
%col{style: "width: 15%"}/
|
||||
%col{style: "width: 15%"}/
|
||||
%col{style: "width: 25%"}/
|
||||
%col{style: "width: 25%"}/
|
||||
%col{style: "width: 25%"}/
|
||||
%thead
|
||||
%tr
|
||||
%th{colspan: "2"}= t('.options')
|
||||
%th= t('.price')
|
||||
%th= t('.sku')
|
||||
%th= t('.options')
|
||||
%th{ style: 'text-align: center;' }= t('.price')
|
||||
%th{ style: 'text-align: center;' }= 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
|
||||
|
||||
@@ -60,10 +60,12 @@
|
||||
= 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
|
||||
|
||||
@@ -48,9 +48,17 @@
|
||||
%h5
|
||||
= t :orders_form_total
|
||||
%td.text-right
|
||||
%h5.order-total.grand-total= @order.display_total
|
||||
%h5.order-total.grand-total= @order.display_total.to_html
|
||||
%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"}
|
||||
|
||||
@@ -25,20 +25,28 @@
|
||||
= 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{id: "amount-paid"}
|
||||
%td.text-right.total#amount-paid
|
||||
%strong
|
||||
= order.display_payment_total.to_html
|
||||
= Spree::Money.new(@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
|
||||
%h5.not-paid#balance-due
|
||||
= order.display_outstanding_balance.to_html
|
||||
- if order.outstanding_balance.negative?
|
||||
%tr.total
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
- if (order_payment_method = last_payment_method(order))
|
||||
.text-big
|
||||
= t :order_payment
|
||||
%strong= order_payment_method&.name
|
||||
%strong= order_payment_method&.display_name
|
||||
%p.text-small.text-skinny.pre-line.word-wrap
|
||||
%em= order_payment_method&.description
|
||||
%em= order_payment_method&.display_description
|
||||
- else
|
||||
.text-big
|
||||
= t(:no_payment_required)
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
%p.callout{style: "margin-top: 40px"}
|
||||
%strong
|
||||
= t :email_payment_description
|
||||
%p{style: "margin: 5px"}= last_payment_method(@order).description
|
||||
%p{style: "margin: 5px"}= last_payment_method(@order).display_description
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
%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)
|
||||
@@ -19,18 +19,21 @@
|
||||
= 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-2.columns.tab{ name: "orders" }
|
||||
.small.12.medium-3.columns.tab{ name: "orders" }
|
||||
%a=t('.tabs.orders')
|
||||
- if Spree::Config.stripe_connect_enabled && Stripe.publishable_key
|
||||
.small.12.medium-2.columns.tab{ name: "cards" }
|
||||
.small.12.medium-3.columns.tab{ name: "cards" }
|
||||
%a=t('.tabs.cards')
|
||||
.small.12.medium-2.columns.tab{ name: "transactions" }
|
||||
.small.12.medium-3.columns.tab{ name: "transactions" }
|
||||
%a=t('.tabs.transactions')
|
||||
.small.12.medium-2.columns.tab{ name: "settings" }
|
||||
.small.12.medium-3.columns.tab{ name: "customer_account_transactions" }
|
||||
%a=t('.tabs.customer_account_transactions')
|
||||
.small.12.medium-3.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
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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";
|
||||
}
|
||||
}
|
||||
44
app/webpacker/controllers/select_user_controller.js
Normal file
44
app/webpacker/controllers/select_user_controller.js
Normal file
@@ -0,0 +1,44 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,13 @@
|
||||
@import "flatpickr/dist/themes/material_blue";
|
||||
@import "shortcut-buttons-flatpickr/dist/themes/light";
|
||||
|
||||
@import "../admin/globals/functions";
|
||||
@import "globals/functions";
|
||||
@import "globals/palette"; // admin_v3
|
||||
@import "globals/variables"; // admin_v3
|
||||
@import "../admin/globals/mixins";
|
||||
@import "globals/mixins";
|
||||
@import "mixins"; // admin_v3
|
||||
|
||||
@import "../admin/plugins/font-awesome";
|
||||
@import "plugins/font-awesome";
|
||||
|
||||
@import "../shared/variables/layout";
|
||||
@import "../shared/variables/variables";
|
||||
@@ -32,7 +32,7 @@
|
||||
@import "shared/icons"; // admin_v3
|
||||
@import "shared/forms"; // admin_v3
|
||||
@import "shared/layout"; // admin_v3
|
||||
@import "../admin/shared/scroll_bar";
|
||||
@import "shared/scroll_bar";
|
||||
|
||||
@import "../shared/trix";
|
||||
|
||||
@@ -40,96 +40,94 @@
|
||||
@import "plugins/powertip"; // admin_v3
|
||||
|
||||
@import "sections/orders"; // admin_v3
|
||||
@import "../admin/sections/products";
|
||||
@import "sections/products";
|
||||
|
||||
@import "../admin/hacks/mozilla";
|
||||
@import "../admin/hacks/opera";
|
||||
@import "../admin/hacks/ie";
|
||||
@import "hacks/mozilla";
|
||||
@import "hacks/opera";
|
||||
@import "hacks/ie";
|
||||
|
||||
@import "components/actions"; // admin_v3
|
||||
@import "../admin/components/alert-box";
|
||||
@import "../admin/components/alert_row";
|
||||
@import "components/alert-box";
|
||||
@import "components/alert_row";
|
||||
@import "components/buttons"; // admin_v3
|
||||
@import "components/date-picker"; // admin_v3
|
||||
@import "../admin/components/dialogs";
|
||||
@import "../admin/components/input";
|
||||
@import "../admin/components/jquery_dialog";
|
||||
@import "components/dialogs";
|
||||
@import "components/input";
|
||||
@import "components/jquery_dialog";
|
||||
@import "components/messages"; // admin_v3
|
||||
@import "components/navigation"; // admin_v3
|
||||
@import "../admin/components/ng-cloak";
|
||||
@import "../admin/components/page_actions";
|
||||
@import "components/ng-cloak";
|
||||
@import "components/page_actions";
|
||||
@import "components/pagination"; // admin_v3
|
||||
@import "../admin/components/per_page_controls";
|
||||
@import "../admin/components/product_autocomplete";
|
||||
@import "../admin/components/progress";
|
||||
@import "../admin/components/save_bar";
|
||||
@import "components/per_page_controls";
|
||||
@import "components/product_autocomplete";
|
||||
@import "components/progress";
|
||||
@import "components/save_bar";
|
||||
@import "components/sidebar"; // admin_v3
|
||||
@import "../admin/components/simple_modal";
|
||||
@import "../admin/components/states";
|
||||
@import "../admin/components/stripe_connect_button";
|
||||
@import "../admin/components/subscriptions_states";
|
||||
@import "../admin/components/table-filter";
|
||||
@import "../admin/components/table_loading";
|
||||
@import "../admin/components/timepicker";
|
||||
@import "../admin/components/todo";
|
||||
@import "../admin/components/tooltip";
|
||||
@import "../admin/components/wizard_progress";
|
||||
@import "components/simple_modal";
|
||||
@import "components/states";
|
||||
@import "components/stripe_connect_button";
|
||||
@import "components/subscriptions_states";
|
||||
@import "components/table-filter";
|
||||
@import "components/table_loading";
|
||||
@import "components/timepicker";
|
||||
@import "components/todo";
|
||||
@import "components/tooltip";
|
||||
@import "components/wizard_progress";
|
||||
|
||||
@import "../admin/pages/enterprise_form";
|
||||
@import "../admin/pages/subscription_form";
|
||||
@import "../admin/pages/subscription_line_items";
|
||||
@import "../admin/pages/subscription_review";
|
||||
@import "pages/enterprise_form";
|
||||
@import "pages/subscription_form";
|
||||
@import "pages/subscription_line_items";
|
||||
@import "pages/subscription_review";
|
||||
|
||||
@import "../admin/advanced_settings";
|
||||
@import "../admin/alert";
|
||||
@import "../admin/animations";
|
||||
@import "advanced_settings";
|
||||
@import "alert";
|
||||
@import "animations";
|
||||
@import "pages/change_type_form"; // admin_v3
|
||||
@import "../admin/connected_apps";
|
||||
@import "../admin/customers";
|
||||
@import "connected_apps";
|
||||
@import "customers";
|
||||
@import "dashboard/dashboard_item"; // admin_v3
|
||||
@import "pages/dashboard-single-ent"; // admin_v3
|
||||
@import "../admin/dialog";
|
||||
@import "../admin/disabled";
|
||||
@import "dialog";
|
||||
@import "disabled";
|
||||
@import "components/dropdown"; // admin_v3
|
||||
@import "pages/edit_variant"; // admin_v3
|
||||
@import "pages/enterprise_index_panels"; // admin_v3
|
||||
@import "../admin/enterprises";
|
||||
@import "../admin/filters_and_controls";
|
||||
@import "../admin/grid";
|
||||
@import "../admin/icons";
|
||||
@import "../admin/index_panel_buttons";
|
||||
@import "../admin/index_panels";
|
||||
@import "../admin/modals";
|
||||
@import "../admin/offsets";
|
||||
@import "../admin/openfoodnetwork";
|
||||
@import "../admin/order_cycles";
|
||||
@import "../admin/orders";
|
||||
@import "enterprises";
|
||||
@import "filters_and_controls";
|
||||
@import "grid";
|
||||
@import "icons";
|
||||
@import "index_panel_buttons";
|
||||
@import "index_panels";
|
||||
@import "modals";
|
||||
@import "offsets";
|
||||
@import "openfoodnetwork";
|
||||
@import "order_cycles";
|
||||
@import "orders";
|
||||
@import "pages/product_import"; // admin_v3
|
||||
@import "../admin/products";
|
||||
@import "../admin/products_v3";
|
||||
@import "../admin/question-mark-tooltip";
|
||||
@import "../admin/relationships";
|
||||
@import "../admin/reports";
|
||||
@import "products";
|
||||
@import "products_v3";
|
||||
@import "question-mark-tooltip";
|
||||
@import "relationships";
|
||||
@import "reports";
|
||||
@import "components/select2"; // admin_v3
|
||||
@import "components/sidebar-item"; // admin_v3
|
||||
@import "../admin/side_menu";
|
||||
@import "../admin/tables";
|
||||
@import "../admin/tag_rules";
|
||||
@import "../admin/terms_of_service_files";
|
||||
@import "../admin/validation";
|
||||
@import "../admin/variant_overrides";
|
||||
@import "../admin/welcome";
|
||||
@import "side_menu";
|
||||
@import "tables";
|
||||
@import "tag_rules";
|
||||
@import "terms_of_service_files";
|
||||
@import "validation";
|
||||
@import "variant_overrides";
|
||||
@import "welcome";
|
||||
|
||||
@import "shared/question-mark-icon";
|
||||
@import "../admin/question-mark-tooltip";
|
||||
|
||||
@import "tom-select/src/scss/tom-select.default";
|
||||
@import "~tom-select/src/scss/tom-select.default";
|
||||
@import "components/tom_select"; // admin_v3
|
||||
|
||||
@import "modal_component/modal_component";
|
||||
@import "vertical_ellipsis_menu_component/vertical_ellipsis_menu_component"; // admin_v3 and only V3
|
||||
@import "tag_list_input_component/tag_list_input_component";
|
||||
@import "admin/trix";
|
||||
@import "trix";
|
||||
|
||||
@import "terms_of_service_banner"; // admin_v3
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ table tbody tr {
|
||||
}
|
||||
|
||||
&.action-remove td,
|
||||
&.action-void td {
|
||||
&.action-void td,
|
||||
&.action-internal_void td {
|
||||
text-decoration: line-through;
|
||||
|
||||
&.actions {
|
||||
@@ -2,14 +2,17 @@
|
||||
text-align: center;
|
||||
margin: 0 0 1em;
|
||||
padding: 20px 0 28px 0;
|
||||
|
||||
background-color: $color-7;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
button.page,
|
||||
.pagelist {
|
||||
display: flex;
|
||||
gap: inherit;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: $btn-relaxed-height;
|
||||
line-height: $btn-relaxed-height;
|
||||
@@ -69,17 +72,6 @@
|
||||
|
||||
&:active {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 0.35em;
|
||||
|
||||
background-color: $white;
|
||||
color: $near-black;
|
||||
box-shadow: $box-shadow;
|
||||
|
||||
&.active {
|
||||
color: $white;
|
||||
background-color: $red;
|
||||
cursor: default;
|
||||
@@ -1,21 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user