diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index cec6bd232e..77b598ad3c 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -386,7 +386,6 @@ Metrics/AbcSize:
- app/helpers/spree/admin/base_helper.rb
- app/helpers/spree/admin/zones_helper.rb
- app/helpers/spree/orders_helper.rb
- - app/jobs/subscription_placement_job.rb
- app/mailers/producer_mailer.rb
- app/models/calculator/flat_percent_per_item.rb
- app/models/column_preference.rb
@@ -413,6 +412,8 @@ Metrics/AbcSize:
- app/services/create_order_cycle.rb
- app/services/order_syncer.rb
- app/services/subscription_validator.rb
+ - lib/active_merchant/billing/gateways/stripe_decorator.rb
+ - lib/active_merchant/billing/gateways/stripe_payment_intents.rb
- lib/discourse/single_sign_on.rb
- lib/open_food_network/bulk_coop_report.rb
- lib/open_food_network/customers_report.rb
@@ -506,6 +507,7 @@ Metrics/CyclomaticComplexity:
- app/models/spree/product_decorator.rb
- app/models/variant_override_set.rb
- app/services/cart_service.rb
+ - lib/active_merchant/billing/gateways/stripe_payment_intents.rb
- lib/discourse/single_sign_on.rb
- lib/open_food_network/bulk_coop_report.rb
- lib/open_food_network/enterprise_issue_validator.rb
@@ -531,6 +533,7 @@ Metrics/PerceivedComplexity:
- app/models/spree/ability_decorator.rb
- app/models/spree/order_decorator.rb
- app/models/spree/product_decorator.rb
+ - lib/active_merchant/billing/gateways/stripe_payment_intents.rb
- lib/discourse/single_sign_on.rb
- lib/open_food_network/bulk_coop_report.rb
- lib/open_food_network/enterprise_issue_validator.rb
@@ -600,6 +603,7 @@ Metrics/MethodLength:
- app/serializers/api/cached_enterprise_serializer.rb
- app/services/order_cycle_form.rb
- engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb
+ - lib/active_merchant/billing/gateways/stripe_payment_intents.rb
- lib/discourse/single_sign_on.rb
- lib/open_food_network/bulk_coop_report.rb
- lib/open_food_network/column_preference_defaults.rb
@@ -663,6 +667,7 @@ Metrics/ClassLength:
- app/serializers/api/enterprise_shopfront_serializer.rb
- app/services/cart_service.rb
- engines/order_management/app/services/order_management/reports/enterprise_fee_summary/scope.rb
+ - lib/active_merchant/billing/gateways/stripe_payment_intents.rb
- lib/open_food_network/bulk_coop_report.rb
- lib/open_food_network/enterprise_fee_calculator.rb
- lib/open_food_network/order_cycle_form_applicator.rb
diff --git a/.rubocop_styleguide.yml b/.rubocop_styleguide.yml
index 1af15bd7f4..eb73f8a511 100644
--- a/.rubocop_styleguide.yml
+++ b/.rubocop_styleguide.yml
@@ -96,6 +96,15 @@ Style/FormatString:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styleformatstring
+Style/HashEachMethods:
+ Enabled: false
+
+Style/HashTransformKeys:
+ Enabled: false
+
+Style/HashTransformValues:
+ Enabled: false
+
Style/IfUnlessModifier:
Enabled: false
StyleGuide: http://relaxed.ruby.style/#styleifunlessmodifier
diff --git a/Gemfile b/Gemfile
index 434fd083f4..39b27966b7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -166,5 +166,5 @@ group :development do
# greater than 1.0.9, so we just required the latest available version here.
gem 'eventmachine', '>= 1.2.3'
- gem 'rack-mini-profiler', '< 2.0.0'
+ gem 'rack-mini-profiler', '< 3.0.0'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 88570026b9..47b51fa614 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -205,7 +205,7 @@ GEM
activerecord (>= 3.2.0, < 5.0)
fog (~> 1.0)
rails (>= 3.2.0, < 5.0)
- ddtrace (0.32.0)
+ ddtrace (0.33.1)
msgpack
debugger-linecache (1.2.0)
deface (1.0.2)
@@ -465,7 +465,7 @@ GEM
railties (>= 3.1)
money (5.1.1)
i18n (~> 0.6.0)
- msgpack (1.3.1)
+ msgpack (1.3.3)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
@@ -477,7 +477,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
- oj (3.10.2)
+ oj (3.10.5)
optimist (3.0.0)
orm_adapter (0.5.0)
paper_trail (5.2.3)
@@ -492,7 +492,7 @@ GEM
parallel (1.19.1)
paranoia (2.4.2)
activerecord (>= 4.0, < 6.1)
- parser (2.7.0.2)
+ parser (2.7.0.4)
ast (~> 2.4.0)
paypal-sdk-core (0.2.10)
multi_json (~> 1.0)
@@ -513,7 +513,7 @@ GEM
rabl (0.8.4)
activesupport (>= 2.3.14)
rack (1.5.5)
- rack-mini-profiler (1.1.6)
+ rack-mini-profiler (2.0.0)
rack (>= 1.2.0)
rack-protection (1.5.5)
rack
@@ -571,15 +571,15 @@ GEM
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
- rspec-core (3.9.0)
- rspec-support (~> 3.9.0)
+ rspec-core (3.9.1)
+ rspec-support (~> 3.9.1)
rspec-expectations (3.9.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
- rspec-mocks (3.9.0)
+ rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
- rspec-rails (3.9.0)
+ rspec-rails (3.9.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
@@ -589,8 +589,8 @@ GEM
rspec-support (~> 3.9.0)
rspec-retry (0.6.2)
rspec-core (> 3.3)
- rspec-support (3.9.0)
- rubocop (0.80.0)
+ rspec-support (3.9.2)
+ rubocop (0.80.1)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.7.0.1)
@@ -752,7 +752,7 @@ DEPENDENCIES
pg (~> 0.21.0)
pry-byebug (>= 3.4.3)
rabl
- rack-mini-profiler (< 2.0.0)
+ rack-mini-profiler (< 3.0.0)
rack-rewrite
rack-ssl
rails (~> 4.0.0)
diff --git a/app/assets/javascripts/admin/payments/services/payment.js.coffee b/app/assets/javascripts/admin/payments/services/payment.js.coffee
index f079137818..a87497226d 100644
--- a/app/assets/javascripts/admin/payments/services/payment.js.coffee
+++ b/app/assets/javascripts/admin/payments/services/payment.js.coffee
@@ -21,7 +21,7 @@ angular.module('admin.payments').factory 'Payment', (AdminStripeElements, curren
year: @form_data.card_year
verification_value: @form_data.card_verification_value
}
- when 'stripe'
+ when 'stripe', 'stripe_sca'
angular.extend munged_payment.payment, {
source_attributes:
gateway_payment_profile_id: @form_data.token
@@ -35,6 +35,8 @@ angular.module('admin.payments').factory 'Payment', (AdminStripeElements, curren
purchase: ->
if @paymentMethodType() == 'stripe'
AdminStripeElements.requestToken(@form_data, @submit)
+ else if @paymentMethodType() == 'stripe_sca'
+ AdminStripeElements.createPaymentMethod(@form_data, @submit)
else
@submit()
diff --git a/app/assets/javascripts/admin/payments/services/stripe_elements.js.coffee b/app/assets/javascripts/admin/payments/services/stripe_elements.js.coffee
index 03971be228..9ecf2b1db1 100644
--- a/app/assets/javascripts/admin/payments/services/stripe_elements.js.coffee
+++ b/app/assets/javascripts/admin/payments/services/stripe_elements.js.coffee
@@ -5,7 +5,7 @@ angular.module("admin.payments").factory 'AdminStripeElements', ($rootScope, Sta
stripe: null
card: null
- # New Stripe Elements method
+ # Create Token to be used with the Stripe Charges API
requestToken: (secrets, submit) ->
return unless @stripe? && @card?
@@ -20,6 +20,21 @@ angular.module("admin.payments").factory 'AdminStripeElements', ($rootScope, Sta
secrets.card = response.token.card
submit()
+ # Create Payment Method to be used with the Stripe Payment Intents API
+ createPaymentMethod: (secrets, submit) ->
+ return unless @stripe? && @card?
+
+ cardData = @makeCardData(secrets)
+
+ @stripe.createPaymentMethod({ type: 'card', card: @card }, @card, cardData).then (response) =>
+ if(response.error)
+ StatusMessage.display 'error', response.error.message
+ else
+ secrets.token = response.paymentMethod.id
+ secrets.cc_type = response.paymentMethod.card.brand
+ secrets.card = response.paymentMethod.card
+ submit()
+
# Maps the brand returned by Stripe to that required by activemerchant
mapCC: (ccType) ->
switch ccType
diff --git a/app/assets/javascripts/admin/subscriptions/controllers/details_controller.js.coffee b/app/assets/javascripts/admin/subscriptions/controllers/details_controller.js.coffee
index 55d2a46b42..cb331a62f3 100644
--- a/app/assets/javascripts/admin/subscriptions/controllers/details_controller.js.coffee
+++ b/app/assets/javascripts/admin/subscriptions/controllers/details_controller.js.coffee
@@ -16,7 +16,7 @@ angular.module("admin.subscriptions").controller "DetailsController", ($scope, $
return if !newValue?
paymentMethod = ($scope.paymentMethods.filter (pm) -> pm.id == newValue)[0]
return unless paymentMethod?
- $scope.cardRequired = (paymentMethod.type == "Spree::Gateway::StripeConnect")
+ $scope.cardRequired = (paymentMethod.type == "Spree::Gateway::StripeConnect" || paymentMethod.type == "Spree::Gateway::StripeSCA")
$scope.loadCustomer() if $scope.cardRequired && !$scope.customer
$scope.loadCustomer = ->
diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee
index 204c35ac66..83cf018365 100644
--- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee
+++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee
@@ -7,6 +7,8 @@ Darkswarm.factory 'Checkout', ($injector, CurrentOrder, ShippingMethods, StripeE
purchase: ->
if @paymentMethod()?.method_type == 'stripe' && !@secrets.selected_card
StripeElements.requestToken(@secrets, @submit)
+ else if @paymentMethod()?.method_type == 'stripe_sca' && !@secrets.selected_card
+ StripeElements.createPaymentMethod(@secrets, @submit)
else
@submit()
@@ -59,7 +61,7 @@ Darkswarm.factory 'Checkout', ($injector, CurrentOrder, ShippingMethods, StripeE
last_name: @order.bill_address.lastname
}
- if @paymentMethod()?.method_type == 'stripe'
+ if @paymentMethod()?.method_type == 'stripe' || @paymentMethod()?.method_type == 'stripe_sca'
if @secrets.selected_card
angular.extend munged_order, {
existing_card_id: @secrets.selected_card
diff --git a/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee b/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee
index 32b0535251..acd220f092 100644
--- a/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee
+++ b/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee
@@ -1,12 +1,10 @@
Darkswarm.factory 'StripeElements', ($rootScope, Loading, RailsFlashLoader) ->
new class StripeElements
- # TODO: add locale here for translations of error messages etc. from Stripe
-
# These are both set from the StripeElements directive
stripe: null
card: null
- # New Stripe Elements method
+ # Create Token to be used with the Stripe Charges API
requestToken: (secrets, submit, loading_message = t("processing_payment")) ->
return unless @stripe? && @card?
@@ -23,6 +21,23 @@ Darkswarm.factory 'StripeElements', ($rootScope, Loading, RailsFlashLoader) ->
secrets.card = response.token.card
submit()
+ # Create Payment Method to be used with the Stripe Payment Intents API
+ createPaymentMethod: (secrets, submit, loading_message = t("processing_payment")) ->
+ return unless @stripe? && @card?
+
+ Loading.message = loading_message
+ cardData = @makeCardData(secrets)
+
+ @stripe.createPaymentMethod({ type: 'card', card: @card }, @card, cardData).then (response) =>
+ if(response.error)
+ Loading.clear()
+ RailsFlashLoader.loadFlash({error: t("error") + ": #{response.error.message}"})
+ else
+ secrets.token = response.paymentMethod.id
+ secrets.cc_type = response.paymentMethod.card.brand
+ secrets.card = response.paymentMethod.card
+ submit()
+
# Maps the brand returned by Stripe to that required by activemerchant
mapCC: (ccType) ->
switch ccType
diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb
index 105a3ee52d..cf95180818 100644
--- a/app/controllers/admin/enterprises_controller.rb
+++ b/app/controllers/admin/enterprises_controller.rb
@@ -23,7 +23,6 @@ module Admin
before_filter :setup_property, only: [:edit]
helper 'spree/products'
- include ActionView::Helpers::TextHelper
include OrderCyclesHelper
def index
@@ -77,19 +76,12 @@ module Admin
def bulk_update
@enterprise_set = EnterpriseSet.new(collection, params[:enterprise_set])
- touched_enterprises = @enterprise_set.collection.select(&:changed?)
if @enterprise_set.save
flash[:success] = I18n.t(:enterprise_bulk_update_success_notice)
- # 18-3-2015: It seems that the form for this action sometimes loads bogus values for
- # the 'sells' field, and submitting that form results in a bunch of enterprises with
- # values that have mysteriously changed. This statement is here to help debug that
- # issue, and should be removed (along with its display in index.html.haml) when the
- # issue has been resolved.
- flash[:action] = "#{I18n.t(:updated)} #{pluralize(touched_enterprises.count, 'enterprise')}: #{touched_enterprises.map(&:name).join(', ')}"
-
redirect_to main_app.admin_enterprises_path
else
+ touched_enterprises = @enterprise_set.collection.select(&:changed?)
@enterprise_set.collection.select! { |e| touched_enterprises.include? e }
flash[:error] = I18n.t(:enterprise_bulk_update_error)
render :index
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index fad20ce7aa..5e2777feac 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,5 +1,5 @@
require 'open_food_network/referer_parser'
-require 'spree/authentication_helpers'
+require_dependency 'spree/authentication_helpers'
class ApplicationController < ActionController::Base
protect_from_forgery
diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb
index d8dfc4b0c7..1a6de50f66 100644
--- a/app/controllers/enterprises_controller.rb
+++ b/app/controllers/enterprises_controller.rb
@@ -63,8 +63,6 @@ class EnterprisesController < BaseController
end
def reset_order
- distributor = Enterprise.is_distributor.find_by(permalink: params[:id]) ||
- Enterprise.is_distributor.find(params[:id])
order = current_order(true)
reset_distributor(order, distributor)
@@ -74,6 +72,14 @@ class EnterprisesController < BaseController
reset_order_cycle(order, distributor)
order.save!
+ rescue ActiveRecord::RecordNotFound
+ flash[:error] = I18n.t(:enterprise_shop_show_error)
+ redirect_to shops_path
+ end
+
+ def distributor
+ @distributor ||= Enterprise.is_distributor.find_by(permalink: params[:id]) ||
+ Enterprise.is_distributor.find(params[:id])
end
def reset_distributor(order, distributor)
diff --git a/app/controllers/spree/admin/payment_methods_controller.rb b/app/controllers/spree/admin/payment_methods_controller.rb
index 9d0cf5e817..4665417bcf 100644
--- a/app/controllers/spree/admin/payment_methods_controller.rb
+++ b/app/controllers/spree/admin/payment_methods_controller.rb
@@ -110,7 +110,7 @@ module Spree
else
Gateway.providers.reject{ |p| p.name.include? "Bogus" }.sort_by(&:name)
end
- @providers.reject!{ |p| p.name.ends_with? "StripeConnect" } unless show_stripe?
+ @providers.reject!{ |provider| stripe_provider?(provider) } unless show_stripe?
@calculators = PaymentMethod.calculators.sort_by(&:name)
end
@@ -134,12 +134,12 @@ module Spree
# current payment_method is already a Stripe method
def show_stripe?
Spree::Config.stripe_connect_enabled ||
- @payment_method.try(:type) == "Spree::Gateway::StripeConnect"
+ stripe_payment_method?
end
def restrict_stripe_account_change
return unless @payment_method
- return unless @payment_method.type == "Spree::Gateway::StripeConnect"
+ return unless stripe_payment_method?
return unless @payment_method.preferred_enterprise_id.andand > 0
@stripe_account_holder = Enterprise.find(@payment_method.preferred_enterprise_id)
@@ -147,6 +147,15 @@ module Spree
params[:payment_method][:preferred_enterprise_id] = @stripe_account_holder.id
end
+
+ def stripe_payment_method?
+ ["Spree::Gateway::StripeConnect",
+ "Spree::Gateway::StripeSCA"].include? @payment_method.try(:type)
+ end
+
+ def stripe_provider?(provider)
+ provider.name.ends_with?("StripeConnect", "StripeSCA")
+ end
end
end
end
diff --git a/app/controllers/spree/admin/reports_controller.rb b/app/controllers/spree/admin/reports_controller.rb
index ec08bca29c..cdc97fbf39 100644
--- a/app/controllers/spree/admin/reports_controller.rb
+++ b/app/controllers/spree/admin/reports_controller.rb
@@ -217,7 +217,11 @@ module Spree
end
def render_report(header, table, create_csv, csv_file_name)
- send_data csv_report(header, table), filename: csv_file_name if create_csv
+ if create_csv
+ @csv_report = csv_report(header, table)
+ send_data @csv_report, filename: csv_file_name
+ end
+
@header = header
@table = table
# Rendering HTML is the default.
diff --git a/app/controllers/spree/credit_cards_controller.rb b/app/controllers/spree/credit_cards_controller.rb
index 71a365e784..3381a240e8 100644
--- a/app/controllers/spree/credit_cards_controller.rb
+++ b/app/controllers/spree/credit_cards_controller.rb
@@ -10,10 +10,14 @@ module Spree
render json: @credit_card, serializer: ::Api::CreditCardSerializer, status: :ok
else
message = t(:card_could_not_be_saved)
- render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout, error: message) } }, status: :bad_request
+ render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout,
+ error: message) } },
+ status: :bad_request
end
rescue Stripe::CardError => e
- render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout, error: e.message) } }, status: :bad_request
+ render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout,
+ error: e.message) } },
+ status: :bad_request
end
def update
@@ -52,12 +56,19 @@ module Spree
private
- # Currently can only destroy the whole customer object
+ # It destroys the whole customer object
def destroy_at_stripe
- stripe_customer = Stripe::Customer.retrieve(@credit_card.gateway_customer_profile_id)
+ stripe_customer = Stripe::Customer.retrieve(@credit_card.gateway_customer_profile_id, {})
stripe_customer.delete if stripe_customer
end
+ def stripe_account_id
+ StripeAccount.
+ find_by_enterprise_id(@credit_card.payment_method.preferred_enterprise_id).
+ andand.
+ stripe_user_id
+ end
+
def create_customer(token)
Stripe::Customer.create(email: spree_current_user.email, source: token)
end
diff --git a/app/jobs/subscription_confirm_job.rb b/app/jobs/subscription_confirm_job.rb
index 2e0b05e4fe..be7f589cfc 100644
--- a/app/jobs/subscription_confirm_job.rb
+++ b/app/jobs/subscription_confirm_job.rb
@@ -6,6 +6,7 @@ class SubscriptionConfirmJob
ids = proxy_orders.pluck(:id)
proxy_orders.update_all(confirmed_at: Time.zone.now)
ProxyOrder.where(id: ids).each do |proxy_order|
+ Rails.logger.info "Confirming Order for Proxy Order #{proxy_order.id}"
@order = proxy_order.order
process!
end
diff --git a/app/jobs/subscription_placement_job.rb b/app/jobs/subscription_placement_job.rb
index 26c67997ba..ecaa9208ef 100644
--- a/app/jobs/subscription_placement_job.rb
+++ b/app/jobs/subscription_placement_job.rb
@@ -5,8 +5,7 @@ class SubscriptionPlacementJob
ids = proxy_orders.pluck(:id)
proxy_orders.update_all(placed_at: Time.zone.now)
ProxyOrder.where(id: ids).each do |proxy_order|
- proxy_order.initialise_order!
- process(proxy_order.order)
+ place_order_for(proxy_order)
end
send_placement_summary_emails
@@ -28,16 +27,18 @@ class SubscriptionPlacementJob
.joins(:subscription).merge(Subscription.not_canceled.not_paused)
end
- def process(order)
+ def place_order_for(proxy_order)
+ Rails.logger.info "Placing Order for Proxy Order #{proxy_order.id}"
+ proxy_order.initialise_order!
+ place_order(proxy_order.order)
+ end
+
+ def place_order(order)
record_order(order)
return record_issue(:complete, order) if order.completed?
changes = cap_quantity_and_store_changes(order)
- if order.line_items.where('quantity > 0').empty?
- order.reload.adjustments.destroy_all
- order.update!
- return send_empty_email(order, changes)
- end
+ return handle_empty_order(order, changes) if order.line_items.where('quantity > 0').empty?
move_to_completion(order)
send_placement_email(order, changes)
@@ -58,12 +59,18 @@ class SubscriptionPlacementJob
changes
end
+ def handle_empty_order(order, changes)
+ order.reload.adjustments.destroy_all
+ order.update!
+ send_empty_email(order, changes)
+ end
+
def move_to_completion(order)
AdvanceOrderService.new(order).call!
end
def unavailable_stock_lines_for(order)
- order.line_items.where('variant_id NOT IN (?)', available_variants_for(order))
+ order.line_items.where('variant_id NOT IN (?)', available_variants_for(order).select(&:id))
end
def available_variants_for(order)
diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb
index 887b1deccd..4c90dae789 100644
--- a/app/models/enterprise.rb
+++ b/app/models/enterprise.rb
@@ -319,7 +319,7 @@ class Enterprise < ActiveRecord::Base
def distributed_taxons
Spree::Taxon.
joins(:products).
- where('spree_products.id IN (?)', Spree::Product.in_distributor(self)).
+ where('spree_products.id IN (?)', Spree::Product.in_distributor(self).select(&:id)).
select('DISTINCT spree_taxons.*')
end
@@ -335,7 +335,7 @@ class Enterprise < ActiveRecord::Base
def supplied_taxons
Spree::Taxon.
joins(:products).
- where('spree_products.id IN (?)', Spree::Product.in_supplier(self)).
+ where('spree_products.id IN (?)', Spree::Product.in_supplier(self).select(&:id)).
select('DISTINCT spree_taxons.*')
end
diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb
index 5520acff1c..bbcafa01dc 100644
--- a/app/models/enterprise_fee.rb
+++ b/app/models/enterprise_fee.rb
@@ -25,7 +25,7 @@ class EnterpriseFee < ActiveRecord::Base
if user.has_spree_role?('admin')
where(nil)
else
- where('enterprise_id IN (?)', user.enterprises)
+ where('enterprise_id IN (?)', user.enterprises.select(&:id))
end
}
diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb
index 3088b05a49..f87eed6214 100644
--- a/app/models/enterprise_relationship.rb
+++ b/app/models/enterprise_relationship.rb
@@ -22,7 +22,7 @@ class EnterpriseRelationship < ActiveRecord::Base
}
scope :involving_enterprises, ->(enterprises) {
- where('parent_id IN (?) OR child_id IN (?)', enterprises, enterprises)
+ where('parent_id IN (?) OR child_id IN (?)', enterprises.select(&:id), enterprises.select(&:id))
}
scope :permitting, ->(enterprise_ids) { where('child_id IN (?)', enterprise_ids) }
diff --git a/app/models/exchange.rb b/app/models/exchange.rb
index 175d641a8a..01e23405a5 100644
--- a/app/models/exchange.rb
+++ b/app/models/exchange.rb
@@ -49,7 +49,7 @@ class Exchange < ActiveRecord::Base
}
scope :with_product, lambda { |product|
joins(:exchange_variants).
- where('exchange_variants.variant_id IN (?)', product.variants_including_master)
+ where('exchange_variants.variant_id IN (?)', product.variants_including_master.select(&:id))
}
scope :by_enterprise_name, -> {
joins('INNER JOIN enterprises AS sender ON (sender.id = exchanges.sender_id)').
diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb
index ba080e0cda..57f7ebeabc 100644
--- a/app/models/order_cycle.rb
+++ b/app/models/order_cycle.rb
@@ -17,6 +17,7 @@ class OrderCycle < ActiveRecord::Base
has_many :distributors, -> { uniq }, source: :receiver, through: :cached_outgoing_exchanges
has_and_belongs_to_many :schedules, join_table: 'order_cycle_schedules'
+ has_paper_trail meta: { custom_data: :schedule_ids }
attr_accessor :incoming_exchanges, :outgoing_exchanges
diff --git a/app/models/product_import/entry_processor.rb b/app/models/product_import/entry_processor.rb
index 64d444aaed..132ee257a8 100644
--- a/app/models/product_import/entry_processor.rb
+++ b/app/models/product_import/entry_processor.rb
@@ -122,7 +122,6 @@ module ProductImport
def save_new_inventory_item(entry)
new_item = entry.product_object
- assign_defaults(new_item, entry)
new_item.import_date = @import_time
if new_item.valid? && new_item.save
@@ -136,7 +135,6 @@ module ProductImport
def save_existing_inventory_item(entry)
existing_item = entry.product_object
- assign_defaults(existing_item, entry)
existing_item.import_date = @import_time
if existing_item.valid? && existing_item.save
@@ -164,7 +162,6 @@ module ProductImport
product = Spree::Product.new
product.assign_attributes(entry.assignable_attributes.except('id', 'on_hand', 'on_demand', 'display_name'))
product.supplier_id = entry.producer_id
- assign_defaults(product, entry)
if product.save
ensure_variant_updated(product, entry)
@@ -179,7 +176,6 @@ module ProductImport
def save_variant(entry)
variant = entry.product_object
- assign_defaults(variant, entry)
variant.import_date = @import_time
if variant.valid? && variant.save
@@ -199,37 +195,6 @@ module ProductImport
)
end
- def assign_defaults(object, entry)
- # Assigns a default value for a specified field e.g. category='Vegetables', setting this value
- # either for all entries (overwrite_all), or only for those entries where the field was blank
- # in the spreadsheet (overwrite_empty), depending on selected import settings
- return unless settings.defaults(entry)
-
- settings.defaults(entry).each do |attribute, setting|
- next unless setting['active']
-
- case setting['mode']
- when 'overwrite_all'
- object.assign_attributes(attribute => setting['value'])
- # In case of new products, some attributes are saved on the variant.
- # We write them to the entry here to be copied to the variant later.
- if entry.respond_to? "#{attribute}="
- entry.public_send("#{attribute}=", setting['value'])
- end
- when 'overwrite_empty'
- if object.public_send(attribute).blank? ||
- ((attribute == 'on_hand') &&
- entry.on_hand_nil)
-
- object.assign_attributes(attribute => setting['value'])
- if entry.respond_to? "#{attribute}="
- entry.public_send("#{attribute}=", setting['value'])
- end
- end
- end
- end
- end
-
def display_in_inventory(variant_override, is_new = false)
unless is_new
existing_item = InventoryItem.where(
diff --git a/app/models/schedule.rb b/app/models/schedule.rb
index 143db0642e..d4957f935e 100644
--- a/app/models/schedule.rb
+++ b/app/models/schedule.rb
@@ -1,4 +1,6 @@
class Schedule < ActiveRecord::Base
+ has_paper_trail meta: { custom_data: :order_cycle_ids }
+
has_and_belongs_to_many :order_cycles, join_table: 'order_cycle_schedules'
has_many :coordinators, -> { uniq }, through: :order_cycles
diff --git a/app/models/spree/gateway/stripe_connect.rb b/app/models/spree/gateway/stripe_connect.rb
index ba71049c92..3484dc3e4f 100644
--- a/app/models/spree/gateway/stripe_connect.rb
+++ b/app/models/spree/gateway/stripe_connect.rb
@@ -7,12 +7,6 @@ module Spree
validate :ensure_enterprise_selected
- CARD_TYPE_MAPPING = {
- 'American Express' => 'american_express',
- 'Diners Club' => 'diners_club',
- 'Visa' => 'visa'
- }.freeze
-
def method_type
'stripe'
end
@@ -75,11 +69,6 @@ module Spree
[money, creditcard, options]
end
- def update_source!(source)
- source.cc_type = CARD_TYPE_MAPPING[source.cc_type] if CARD_TYPE_MAPPING.include?(source.cc_type)
- source
- end
-
def token_from_card_profile_ids(creditcard)
token_or_card_id = creditcard.gateway_payment_profile_id
customer = creditcard.gateway_customer_profile_id
diff --git a/app/models/spree/gateway/stripe_sca.rb b/app/models/spree/gateway/stripe_sca.rb
new file mode 100644
index 0000000000..d5ac331b89
--- /dev/null
+++ b/app/models/spree/gateway/stripe_sca.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'stripe/profile_storer'
+require 'stripe/credit_card_cloner'
+require 'active_merchant/billing/gateways/stripe_payment_intents'
+require 'active_merchant/billing/gateways/stripe_decorator'
+
+module Spree
+ class Gateway
+ class StripeSCA < Gateway
+ preference :enterprise_id, :integer
+
+ validate :ensure_enterprise_selected
+
+ attr_accessible :preferred_enterprise_id
+
+ def method_type
+ 'stripe_sca'
+ end
+
+ def provider_class
+ ActiveMerchant::Billing::StripePaymentIntentsGateway
+ end
+
+ def payment_profiles_supported?
+ true
+ end
+
+ def stripe_account_id
+ StripeAccount.find_by_enterprise_id(preferred_enterprise_id).andand.stripe_user_id
+ end
+
+ # NOTE: the name of this method is determined by Spree::Payment::Processing
+ def purchase(money, creditcard, gateway_options)
+ provider.purchase(*options_for_purchase_or_auth(money, creditcard, gateway_options))
+ rescue Stripe::StripeError => e
+ # This will be an error caused by generating a stripe token
+ failed_activemerchant_billing_response(e.message)
+ end
+
+ # NOTE: the name of this method is determined by Spree::Payment::Processing
+ def void(response_code, _creditcard, gateway_options)
+ gateway_options[:stripe_account] = stripe_account_id
+ provider.void(response_code, gateway_options)
+ end
+
+ # NOTE: the name of this method is determined by Spree::Payment::Processing
+ def credit(money, _creditcard, response_code, gateway_options)
+ gateway_options[:stripe_account] = stripe_account_id
+ provider.refund(money, response_code, gateway_options)
+ end
+
+ def create_profile(payment)
+ return unless payment.source.gateway_customer_profile_id.nil?
+
+ profile_storer = Stripe::ProfileStorer.new(payment, provider)
+ profile_storer.create_customer_from_token
+ end
+
+ private
+
+ # In this gateway, what we call 'secret_key' is the 'login'
+ def options
+ options = super
+ options.merge(login: Stripe.api_key)
+ end
+
+ def options_for_purchase_or_auth(money, creditcard, gateway_options)
+ options = {}
+ options[:description] = "Spree Order ID: #{gateway_options[:order_id]}"
+ options[:currency] = gateway_options[:currency]
+ options[:stripe_account] = stripe_account_id
+
+ customer_id, payment_method_id = Stripe::CreditCardCloner.new.clone(creditcard,
+ stripe_account_id)
+ options[:customer] = customer_id
+ [money, payment_method_id, options]
+ end
+
+ def failed_activemerchant_billing_response(error_message)
+ ActiveMerchant::Billing::Response.new(false, error_message)
+ end
+
+ def ensure_enterprise_selected
+ return if preferred_enterprise_id.andand.positive?
+
+ errors.add(:stripe_account_owner, I18n.t(:error_required))
+ end
+ end
+ end
+end
diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb
index 88515cdd36..ab97f98249 100644
--- a/app/models/spree/order_decorator.rb
+++ b/app/models/spree/order_decorator.rb
@@ -62,7 +62,9 @@ Spree::Order.class_eval do
# Find orders that are distributed by the user or have products supplied by the user
# WARNING: This only filters orders, you'll need to filter line items separately using LineItem.managed_by
with_line_items_variants_and_products_outer.
- where('spree_orders.distributor_id IN (?) OR spree_products.supplier_id IN (?)', user.enterprises, user.enterprises).
+ where('spree_orders.distributor_id IN (?) OR spree_products.supplier_id IN (?)',
+ user.enterprises.select(&:id),
+ user.enterprises.select(&:id)).
select('DISTINCT spree_orders.*')
end
}
@@ -71,7 +73,7 @@ Spree::Order.class_eval do
if user.has_spree_role?('admin')
where(nil)
else
- where('spree_orders.distributor_id IN (?)', user.enterprises)
+ where('spree_orders.distributor_id IN (?)', user.enterprises.select(&:id))
end
}
@@ -81,6 +83,10 @@ Spree::Order.class_eval do
joins('LEFT OUTER JOIN spree_products ON (spree_products.id = spree_variants.product_id)')
}
+ scope :with_line_items_variants_and_products, lambda {
+ joins(line_items: { variant: :product })
+ }
+
scope :not_state, lambda { |state|
where("state != ?", state)
}
diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb
index ebe5a68ad2..3da3919bf0 100644
--- a/app/models/spree/payment_method_decorator.rb
+++ b/app/models/spree/payment_method_decorator.rb
@@ -18,7 +18,7 @@ Spree::PaymentMethod.class_eval do
where(nil)
else
joins(:distributors).
- where('distributors_payment_methods.distributor_id IN (?)', user.enterprises).
+ where('distributors_payment_methods.distributor_id IN (?)', user.enterprises.select(&:id)).
select('DISTINCT spree_payment_methods.*')
end
}
@@ -66,6 +66,8 @@ Spree::PaymentMethod.class_eval do
"Pin Payments"
when "Spree::Gateway::StripeConnect"
"Stripe"
+ when "Spree::Gateway::StripeSCA"
+ "Stripe SCA"
when "Spree::Gateway::PayPalExpress"
"PayPal Express"
else
diff --git a/app/models/spree/shipping_method_decorator.rb b/app/models/spree/shipping_method_decorator.rb
index f8e75a6aac..935cfe796b 100644
--- a/app/models/spree/shipping_method_decorator.rb
+++ b/app/models/spree/shipping_method_decorator.rb
@@ -13,7 +13,7 @@ Spree::ShippingMethod.class_eval do
where(nil)
else
joins(:distributors).
- where('distributors_shipping_methods.distributor_id IN (?)', user.enterprises).
+ where('distributors_shipping_methods.distributor_id IN (?)', user.enterprises.select(&:id)).
select('DISTINCT spree_shipping_methods.*')
end
}
diff --git a/app/models/spree/tax_rate_decorator.rb b/app/models/spree/tax_rate_decorator.rb
index 4e256a2cce..f15ded6de8 100644
--- a/app/models/spree/tax_rate_decorator.rb
+++ b/app/models/spree/tax_rate_decorator.rb
@@ -1,12 +1,14 @@
module Spree
TaxRate.class_eval do
class << self
- def match_with_sales_tax_registration(order)
+ def match(order)
return [] if order.distributor && !order.distributor.charges_sales_tax
+ return [] unless order.tax_zone
- match_without_sales_tax_registration(order)
+ all.select do |rate|
+ rate.zone == order.tax_zone || rate.zone.contains?(order.tax_zone) || rate.zone.default_tax
+ end
end
- alias_method_chain :match, :sales_tax_registration
end
def adjust_with_included_tax(order)
diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb
index bc7228646c..4e1e8e1a21 100644
--- a/app/models/spree/variant_decorator.rb
+++ b/app/models/spree/variant_decorator.rb
@@ -48,7 +48,7 @@ Spree::Variant.class_eval do
}
scope :for_distribution, lambda { |order_cycle, distributor|
- where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor))
+ where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor).select(&:id))
}
scope :visible_for, lambda { |enterprise|
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index 443a6ef7a4..ca1fd676e8 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -1,5 +1,7 @@
class Subscription < ActiveRecord::Base
- ALLOWED_PAYMENT_METHOD_TYPES = ["Spree::PaymentMethod::Check", "Spree::Gateway::StripeConnect"].freeze
+ ALLOWED_PAYMENT_METHOD_TYPES = ["Spree::PaymentMethod::Check",
+ "Spree::Gateway::StripeConnect",
+ "Spree::Gateway::StripeSCA"].freeze
belongs_to :shop, class_name: 'Enterprise'
belongs_to :customer
diff --git a/app/serializers/api/admin/payment_method_serializer.rb b/app/serializers/api/admin/payment_method_serializer.rb
index 3d66ddbc03..9862b81dcd 100644
--- a/app/serializers/api/admin/payment_method_serializer.rb
+++ b/app/serializers/api/admin/payment_method_serializer.rb
@@ -4,7 +4,8 @@ module Api
delegate :serializable_hash, to: :method_serializer
def method_serializer
- if object.type == 'Spree::Gateway::StripeConnect'
+ if object.type == 'Spree::Gateway::StripeConnect' ||
+ object.type == 'Spree::Gateway::StripeSCA'
Api::Admin::PaymentMethod::StripeSerializer.new(object)
else
Api::Admin::PaymentMethod::BaseSerializer.new(object)
diff --git a/app/services/order_payment_finder.rb b/app/services/order_payment_finder.rb
new file mode 100644
index 0000000000..c28d0e139a
--- /dev/null
+++ b/app/services/order_payment_finder.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module OrderPaymentFinder
+ def self.last_payment_method(order)
+ # `max_by` avoids additional database queries when payments are loaded
+ # already. There is usually only one payment and this shouldn't cause
+ # any overhead compared to `order(:created_at).last`. Using `last`
+ # without order is not deterministic.
+ #
+ # We are not using `updated_at` because all payments are touched when the
+ # order is updated and then all payments have the same `updated_at` value.
+ order.payments.max_by(&:created_at)&.payment_method
+ end
+end
diff --git a/app/services/permissions/order.rb b/app/services/permissions/order.rb
index 5e95ba92c8..cf44b1e0ae 100644
--- a/app/services/permissions/order.rb
+++ b/app/services/permissions/order.rb
@@ -10,7 +10,7 @@ module Permissions
# Find orders that the user can see
def visible_orders
Spree::Order.
- with_line_items_variants_and_products_outer.
+ with_line_items_variants_and_products.
where(visible_orders_where_values)
end
diff --git a/app/services/subscription_validator.rb b/app/services/subscription_validator.rb
index 8d9a678f9a..4cce8a3af3 100644
--- a/app/services/subscription_validator.rb
+++ b/app/services/subscription_validator.rb
@@ -82,13 +82,18 @@ class SubscriptionValidator
def credit_card_ok?
return unless customer && payment_method
- return unless payment_method.type == "Spree::Gateway::StripeConnect"
+ return unless stripe_payment_method?(payment_method)
return errors.add(:payment_method, :charges_not_allowed) unless customer.allow_charges
return if customer.user.andand.default_card.present?
errors.add(:payment_method, :no_default_card)
end
+ def stripe_payment_method?(payment_method)
+ payment_method.type == "Spree::Gateway::StripeConnect" ||
+ payment_method.type == "Spree::Gateway::StripeSCA"
+ end
+
def subscription_line_items_present?
return if subscription_line_items.reject(&:marked_for_destruction?).any?
diff --git a/app/views/admin/enterprises/_admin_index.html.haml b/app/views/admin/enterprises/_admin_index.html.haml
index 3376601aaa..cb38f57722 100644
--- a/app/views/admin/enterprises/_admin_index.html.haml
+++ b/app/views/admin/enterprises/_admin_index.html.haml
@@ -1,7 +1,3 @@
--# For purposes of debugging bulk_update. See Admin/Enterprises#bulk_update.
-- if flash[:action]
- %p= flash[:action]
-
= form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path do |f|
%table#listing_enterprises.index
%colgroup
diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml
index a9a7ba0685..4636e07e5a 100644
--- a/app/views/admin/order_cycles/_row.html.haml
+++ b/app/views/admin/order_cycles/_row.html.haml
@@ -19,8 +19,7 @@
{{ orderCycle.producers.length }}
= t('.suppliers')
%span{ ng: { hide: 'orderCycle.producers.length > 3', bind: 'orderCycle.producerNames' } }
- %td.coordinator{ ng: { show: 'columns.coordinator.visible' } }
- {{ orderCycle.coordinator.name }}
+ %td.coordinator{ ng: { show: 'columns.coordinator.visible', bind: { html: 'orderCycle.coordinator.name'} } }
%td.shops{ ng: { show: 'columns.shops.visible' } }
%span{'ofn-with-tip' => '{{ orderCycle.shopNames }}', ng: { show: 'orderCycle.shops.length > 3' } }
{{ orderCycle.shops.length }}
diff --git a/app/views/admin/variant_overrides/_new_products.html.haml b/app/views/admin/variant_overrides/_new_products.html.haml
index cf23c22039..77eda7ae37 100644
--- a/app/views/admin/variant_overrides/_new_products.html.haml
+++ b/app/views/admin/variant_overrides/_new_products.html.haml
@@ -13,7 +13,7 @@
%th.hide=t('admin.variant_overrides.index.hide')
%tbody{ ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } }
%tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub_id:views' } }
- %td.producer{ ng: { bind: '::producersByID[product.producer_id].name'} }
+ %td.producer{ ng: { bind: { html: '::producersByID[product.producer_id].name'} } }
%td.product{ ng: { bind: '::product.name'} }
%td.variant
%span{ ng: { bind: '::variant.display_name || ""'} }
diff --git a/app/views/admin/variant_overrides/_products_product.html.haml b/app/views/admin/variant_overrides/_products_product.html.haml
index 5f101e91a6..312273d4d2 100644
--- a/app/views/admin/variant_overrides/_products_product.html.haml
+++ b/app/views/admin/variant_overrides/_products_product.html.haml
@@ -1,5 +1,5 @@
%tr.product.even
- %td.producer{ ng: { show: 'columns.producer.visible', bind: '::producersByID[product.producer_id].name'} }
+ %td.producer{ ng: { show: 'columns.producer.visible', bind: { html: '::producersByID[product.producer_id].name'} } }
%td.product{ ng: { show: 'columns.product.visible', bind: '::product.name'} }
%td.sku{ ng: { show: 'columns.sku.visible' } }
%td.price{ ng: { show: 'columns.price.visible' } }
diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml
index fdbd103203..928578158f 100644
--- a/app/views/checkout/_payment.html.haml
+++ b/app/views/checkout/_payment.html.haml
@@ -1,8 +1,3 @@
-- content_for :injection_data do
- - if Stripe.publishable_key
- :javascript
- angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}"))
-
%fieldset#payment
%ng-form{"ng-controller" => "PaymentCtrl", name: "payment"}
diff --git a/app/views/spree/admin/payment_methods/_provider_settings.html.haml b/app/views/spree/admin/payment_methods/_provider_settings.html.haml
index c64ad9f1d2..38f2ff06e9 100644
--- a/app/views/spree/admin/payment_methods/_provider_settings.html.haml
+++ b/app/views/spree/admin/payment_methods/_provider_settings.html.haml
@@ -1,6 +1,9 @@
+= @payment_method
- case @payment_method
- when Spree::Gateway::StripeConnect
= render 'stripe_connect'
+- when Spree::Gateway::StripeSCA
+ = render 'stripe_connect'
- else
- if @payment_method.preferences.present?
%fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields
diff --git a/app/views/spree/admin/payments/source_forms/_stripe_sca.html.haml b/app/views/spree/admin/payments/source_forms/_stripe_sca.html.haml
new file mode 100644
index 0000000000..fbdba84ac3
--- /dev/null
+++ b/app/views/spree/admin/payments/source_forms/_stripe_sca.html.haml
@@ -0,0 +1,16 @@
+.stripe
+ %script{:src => "https://js.stripe.com/v3/", :type => "text/javascript"}
+ - if Stripe.publishable_key
+ :javascript
+ angular.module('admin.payments').value("stripeObject", Stripe("#{Stripe.publishable_key}"))
+
+ .row
+ .three.columns
+ = label_tag :cardholder_name, t(:cardholder_name)
+ .six.columns
+ = text_field_tag :cardholder_name, nil, {size: 40, "ng-model" => 'form_data.name'}
+ .row
+ .three.columns
+ = label_tag :card_details, t(:card_details)
+ .six.columns
+ %stripe-elements
diff --git a/app/views/spree/admin/payments/source_views/_gateway.html.haml b/app/views/spree/admin/payments/source_views/_gateway.html.haml
index bd366492ba..ebd63febc0 100644
--- a/app/views/spree/admin/payments/source_views/_gateway.html.haml
+++ b/app/views/spree/admin/payments/source_views/_gateway.html.haml
@@ -6,28 +6,28 @@
%dt
= Spree.t(:card_number)
\:
- %dd= payment.source.display_number
+ %dd= payment.source&.display_number
%dt
= Spree.t(:expiration)
\:
%dd
- = payment.source.month
+ = payment.source&.month
\/
- = payment.source.year
+ = payment.source&.year
%dt
= Spree.t(:card_code)
\:
- %dd= payment.source.verification_value
+ %dd= payment.source&.verification_value
.omega.six.columns
%dl
%dt
= t(:maestro_or_solo_cards)
\:
- %dd= payment.source.issue_number
+ %dd= payment.source&.issue_number
%dt
= Spree.t(:start_date)
\:
%dd
- = payment.source.start_month
+ = payment.source&.start_month
\/
- = payment.source.start_year
+ = payment.source&.start_year
diff --git a/app/views/spree/admin/payments/source_views/_stripe_sca.html.haml b/app/views/spree/admin/payments/source_views/_stripe_sca.html.haml
new file mode 100644
index 0000000000..64af07bd3b
--- /dev/null
+++ b/app/views/spree/admin/payments/source_views/_stripe_sca.html.haml
@@ -0,0 +1 @@
+= render "spree/admin/payments/source_views/gateway", payment: payment
diff --git a/app/views/spree/checkout/payment/_stripe.html.haml b/app/views/spree/checkout/payment/_stripe.html.haml
index eace9f00a7..2929b90d3e 100644
--- a/app/views/spree/checkout/payment/_stripe.html.haml
+++ b/app/views/spree/checkout/payment/_stripe.html.haml
@@ -1,3 +1,8 @@
+- content_for :injection_data do
+ - if Stripe.publishable_key
+ :javascript
+ angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}"))
+
.row{ "ng-show" => "savedCreditCards.length > 0" }
.small-12.columns
%h6= t('.used_saved_card')
diff --git a/app/views/spree/checkout/payment/_stripe_sca.html.haml b/app/views/spree/checkout/payment/_stripe_sca.html.haml
new file mode 100644
index 0000000000..2929b90d3e
--- /dev/null
+++ b/app/views/spree/checkout/payment/_stripe_sca.html.haml
@@ -0,0 +1,22 @@
+- content_for :injection_data do
+ - if Stripe.publishable_key
+ :javascript
+ angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}"))
+
+.row{ "ng-show" => "savedCreditCards.length > 0" }
+ .small-12.columns
+ %h6= t('.used_saved_card')
+ %select{ name: "selected_card", required: false, ng: { model: "secrets.selected_card", options: "card.id as card.formatted for card in savedCreditCards" } }
+ %option{ value: "" }= "{{ secrets.selected_card ? '#{t('.enter_new_card')}' : '#{t('.choose_one')}' }}"
+
+ %h6{ ng: { if: '!secrets.selected_card' } }
+ = t('.or_enter_new_card')
+
+%div{ ng: { if: '!secrets.selected_card' } }
+ %stripe-elements
+
+ - if spree_current_user
+ .row
+ .small-12.columns.text-right
+ = check_box_tag 'secrets.save_requested_by_customer', '1', false, 'ng-model' => 'secrets.save_requested_by_customer'
+ = label_tag 'secrets.save_requested_by_customer', t('.remember_this_card')
diff --git a/app/views/spree/order_mailer/_payment.html.haml b/app/views/spree/order_mailer/_payment.html.haml
index 9e7a3a22c8..60fc1056f4 100644
--- a/app/views/spree/order_mailer/_payment.html.haml
+++ b/app/views/spree/order_mailer/_payment.html.haml
@@ -8,7 +8,7 @@
= t :email_payment_summary
%h4
= t :email_payment_method
- %strong= @order.payments.first.andand.payment_method.andand.name.andand.html_safe
+ %strong= OrderPaymentFinder.last_payment_method(@order)&.name
%p
- %em= @order.payments.first.andand.payment_method.andand.description.andand.html_safe
+ %em= OrderPaymentFinder.last_payment_method(@order)&.description
%p
diff --git a/app/views/spree/shared/_order_details.html.haml b/app/views/spree/shared/_order_details.html.haml
index 667bf8b306..22c8ccdf8f 100644
--- a/app/views/spree/shared/_order_details.html.haml
+++ b/app/views/spree/shared/_order_details.html.haml
@@ -13,9 +13,9 @@
.pad
.text-big
= t :order_payment
- %strong= order.payments.first.andand.payment_method.andand.name.andand.html_safe
+ %strong= OrderPaymentFinder.last_payment_method(order)&.name
%p.text-small.text-skinny.pre-line
- %em= order.payments.first.andand.payment_method.andand.description.andand.html_safe
+ %em= OrderPaymentFinder.last_payment_method(order)&.description
.order-summary.text-small
%strong
diff --git a/config/application.rb b/config/application.rb
index 973a1d942a..9f7e817999 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -92,6 +92,7 @@ module Openfoodnetwork
app.config.spree.payment_methods << Spree::Gateway::Migs
app.config.spree.payment_methods << Spree::Gateway::Pin
app.config.spree.payment_methods << Spree::Gateway::StripeConnect
+ app.config.spree.payment_methods << Spree::Gateway::StripeSCA
end
# Settings in config/environments/* take precedence over those specified here.
diff --git a/config/initializers/paper_trail.rb b/config/initializers/paper_trail.rb
index 39a6679176..e955bac9be 100644
--- a/config/initializers/paper_trail.rb
+++ b/config/initializers/paper_trail.rb
@@ -1 +1,7 @@
PaperTrail.config.track_associations = false
+
+module PaperTrail
+ class Version < ActiveRecord::Base
+ attr_accessible :custom_data
+ end
+end
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index b03afe72e0..42dcbf52ba 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -3155,6 +3155,12 @@ ar:
used_saved_card: "استخدم بطاقة المحفوظة:"
or_enter_new_card: "أو أدخل تفاصيل البطاقة الجديدة:"
remember_this_card: تذكر هذه البطاقة؟
+ stripe_sca:
+ choose_one: اختيار واحد
+ enter_new_card: أدخل تفاصيل البطاقة الجديدة
+ used_saved_card: "استخدم بطاقة المحفوظة:"
+ or_enter_new_card: "أو أدخل تفاصيل البطاقة الجديدة:"
+ remember_this_card: تذكر هذه البطاقة؟
date_picker:
format: '٪ س-٪ م-%d'
js_format: 'يوم-شهر-سنة'
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index b834f84a2f..7ea493176c 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -1285,6 +1285,7 @@ ca:
saving_credit_card: Desant la targeta de crèdit...
card_has_been_removed: "S'ha eliminat la teva targeta (número: %{number})"
card_could_not_be_removed: Ho sentim, no s'ha pogut eliminar la targeta
+ invalid_credit_card: "Targeta de crèdit no vàlida"
ie_warning_headline: "El vostre navegador no està actualitzat :-("
ie_warning_text: "Per obtenir la millor experiència a Open Food Network et recomanem que actualitzis el teu navegador:"
ie_warning_chrome: Descarrega Chrome
@@ -2276,6 +2277,7 @@ ca:
enterprise_register_success_notice: "Enhorabona! El registre de %{enterprise} s'ha completat!"
enterprise_bulk_update_success_notice: "Les organitzacions s'han actualitzat correctament"
enterprise_bulk_update_error: 'No s''ha pogut actualitzar'
+ enterprise_shop_show_error: "La botiga que busqueu no existeix o està inactiva a OFN. Consulteu altres botigues."
order_cycles_create_notice: 'S''ha creat el cicle de comanda.'
order_cycles_update_notice: 'S''ha actualitzat el cicle de comanda.'
order_cycles_bulk_update_notice: 'S''han actualitzat els cicles de comanda.'
@@ -2430,6 +2432,12 @@ ca:
severity: Severitat
description: Descripció
resolve: Resoldre
+ exchange_products:
+ load_more_variants: "Carregueu més variants"
+ load_all_variants: "Carregueu totes les variants"
+ select_all_variants: "Seleccioneu totes les %{total_number_of_variants} variants"
+ variants_loaded: "%{num_of_variants_loaded} de %{total_number_of_variants} variants carregades"
+ loading_variants: "Carregant variants"
tag_rules:
shipping_method_tagged_top: "Els mètodes d'enviament etiquetats"
shipping_method_tagged_bottom: "son:"
@@ -2588,6 +2596,73 @@ ca:
signup_or_login: "Comenceu registrant-vos (o iniciant sessió)"
have_an_account: "Ja tens un compte?"
action_login: "Inicia la sessió ara."
+ inflections:
+ each:
+ one: "cadascun"
+ other: "cadascun"
+ bunch:
+ one: "munt"
+ other: "grapats"
+ pack:
+ one: "paquet"
+ other: "paquets"
+ box:
+ one: "Caixa"
+ other: "caixes"
+ bottle:
+ one: "ampolla"
+ other: "ampolles"
+ jar:
+ one: "gerro"
+ other: "pots"
+ head:
+ one: "cap"
+ other: "caps"
+ bag:
+ one: "bossa"
+ other: "bosses"
+ loaf:
+ one: "pa"
+ other: "barres"
+ single:
+ one: "solter"
+ other: "únics"
+ tub:
+ one: "tina"
+ other: "cubells"
+ item:
+ one: "article"
+ other: "articles"
+ dozen:
+ one: "dotzena"
+ other: "dotzenes"
+ unit:
+ one: "unitat"
+ other: "unitats"
+ serve:
+ one: "servir"
+ other: "porcions"
+ tray:
+ one: "safata"
+ other: "safates"
+ piece:
+ one: "peça"
+ other: "peces"
+ pot:
+ one: "pot"
+ other: "pots"
+ bundle:
+ one: "paquet"
+ other: "paquets"
+ flask:
+ one: "matràs"
+ other: "ampolleta"
+ basket:
+ one: "cistella"
+ other: "cistelles"
+ sack:
+ one: "sac"
+ other: "sacs"
producers:
signup:
start_free_profile: "Comença amb un perfil gratuït i amplia'l quan estiguis preparada."
@@ -3139,6 +3214,12 @@ ca:
used_saved_card: "Utilitza una targeta desada:"
or_enter_new_card: "O bé introdueix els detalls d'una nova targeta:"
remember_this_card: Recordar aquesta targeta?
+ stripe_sca:
+ choose_one: Escull-ne un
+ enter_new_card: Introdueix els detalls d'una targeta nova
+ used_saved_card: "Utilitza una targeta desada:"
+ or_enter_new_card: "O bé introdueix els detalls d'una nova targeta:"
+ remember_this_card: Recordar aquesta targeta?
date_picker:
format: '%d-% m-% Y'
js_format: 'dd-mm-yy'
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 2393ee26d7..ed6ae414dd 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -2412,6 +2412,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
enterprise_register_success_notice: "Congratulations! Registration for %{enterprise} is complete!"
enterprise_bulk_update_success_notice: "Enterprises updated successfully"
enterprise_bulk_update_error: 'Update failed'
+ enterprise_shop_show_error: "The shop you are looking for doesn't exist or is inactive on OFN. Please check other shops."
order_cycles_create_notice: 'Your order cycle has been created.'
order_cycles_update_notice: 'Your order cycle has been updated.'
order_cycles_bulk_update_notice: 'Order cycles have been updated.'
@@ -3383,6 +3384,12 @@ See the %{link} to find out more about %{sitename}'s features and to start using
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: ! '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/en_AU.yml b/config/locales/en_AU.yml
index 16d8f4d18c..cbfc05a738 100644
--- a/config/locales/en_AU.yml
+++ b/config/locales/en_AU.yml
@@ -119,7 +119,7 @@ en_AU:
userguide: "Open Food Network User Guide"
email_admin_html: "You can manage your account by logging into the %{link} or by clicking on the cog in the top right hand side of the homepage, and selecting Administration."
admin_panel: "Admin Panel"
- email_community_html: "We also have an online forum for community discussion related to OFN software and the unique challenges of running a food enterprise. You are encouraged to join in. We are constantly evolving and your input into this forum will shape what happens next. %{link}"
+ email_community_html: "Open Food Network is community supported software. If you have less than $500 per month turnover on OFN it's free to use. Read more about our membership options and make your selection: https://about.openfoodnetwork.org.au/software-pricing/. If you haven't yet chosen your membership option, you will be assigned to our basic community membership level, which is 1% of turnover through the platform. We'll invoice you monthly once you reach that level. "
join_community: "Join the community"
invite_manager:
subject: "%{enterprise} has invited you to be a manager"
@@ -1737,46 +1737,46 @@ en_AU:
registration:
steps:
introduction:
- registration_greeting: "Hi there!"
- registration_intro: "You can now create a profile for your Producer or Hub"
- registration_checklist: "What do I need?"
- registration_time: "5-10 minutes"
- registration_enterprise_address: "Enterprise address"
- registration_contact_details: "Primary contact details"
- registration_logo: "Your logo image"
- registration_promo_image: "Landscape image for your profile"
- registration_about_us: "'About Us' text"
- registration_outcome_headline: "What do I get?"
- registration_outcome1_html: "Your profile helps people find and contact you on the Open Food Network."
- registration_outcome2: "Use this space to tell the story of your enterprise, to help drive connections to your social and online presence."
- registration_outcome3: "It's also the first step towards trading on the Open Food Network, or opening an online store."
+ registration_greeting: "Let’s get you set up"
+ registration_intro: "Share your story to connect with the community (and eaters!)"
+ registration_checklist: "Set up your profile"
+ registration_time: "Tell us about yourself "
+ registration_enterprise_address: "And all about your enterprise"
+ registration_contact_details: "Then share a bit about what you do..."
+ registration_logo: "...or even what you sell"
+ registration_promo_image: "Add pictures, and stories, and ways to get in touch"
+ registration_about_us: "Or manage your profile whenever you need"
+ registration_outcome_headline: "Connect with the community and eaters"
+ registration_outcome1_html: "Your profile helps people find and connect with you directly on the Open Food Network."
+ registration_outcome2: "Use this space to tell your story, and to help drive connections with your enterprise. "
+ registration_outcome3: "It’s also the first step to opening your own store on the Open Food Network. \nIt’s totally free to get set up. If your shop starts to earn more than $500 a month, a 1% membership fee will apply."
registration_action: "Let's get started!"
details:
title: "Details"
- headline: "Let's Get Started"
- enterprise: "Woot! First need to know a little bit about your enterprise:"
- producer: "Woot! First we need to know a little bit about your farm:"
- enterprise_name_field: "Enterprise Name:"
- producer_name_field: "Farm Name:"
+ headline: "Tell us about your enterprise"
+ enterprise: "This will put you on our map, and be shared on your profile."
+ producer: "This will put you on our map, and be shared on your profile."
+ enterprise_name_field: "Enterprise Name"
+ producer_name_field: "Farm Name"
producer_name_field_placeholder: "e.g. Charlie's Awesome Farm"
producer_name_field_error: "Please choose a unique name for your enterprise"
- address1_field: "Address line 1:"
+ address1_field: "Address line 1"
address1_field_placeholder: "e.g. 123 Cranberry Drive"
address1_field_error: "Please enter an address"
- address2_field: "Address line 2:"
- suburb_field: "Suburb:"
+ address2_field: "Address line 2"
+ suburb_field: "Suburb"
suburb_field_placeholder: "e.g. Northcote"
suburb_field_error: "Please enter a suburb"
- postcode_field: "Postcode:"
+ postcode_field: "Postcode"
postcode_field_placeholder: "e.g. 3070"
postcode_field_error: "Postcode required"
- state_field: "State:"
+ state_field: "State"
state_field_error: "State required"
- country_field: "Country:"
+ country_field: "Country"
country_field_error: "Please select a country"
contact:
title: "Contact"
- who_is_managing_enterprise: "Who is responsible for managing %{enterprise}?"
+ who_is_managing_enterprise: "Who is the main contact for %{enterprise}? We’ll add these details to your profile so people can get in touch."
contact_field: "Primary Contact"
contact_field_placeholder: "Contact Name"
contact_field_required: "You need to enter a primary contact."
@@ -1784,24 +1784,24 @@ en_AU:
phone_field_placeholder: "eg. (03) 1234 5678"
type:
title: "Type"
- headline: "Last step to add %{enterprise}!"
- question: "Are you a producer?"
- yes_producer: "Yes, I'm a producer"
- no_producer: "No, I'm not a producer"
+ headline: "How will you use the Open Food Network?"
+ question: "This helps us work out what sort of package would best suit you."
+ yes_producer: "I’ll share my own produce"
+ no_producer: "I’ll share other people’s produce (and mine!)"
producer_field_error: "Please choose one. Are you are producer?"
- yes_producer_help: "Producers make yummy things to eat and/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it."
- no_producer_help: "If you’re not a producer, you’re probably someone who sells and distributes food. You might be a hub, coop, buying group, retailer, wholesaler or other."
+ yes_producer_help: "What sort of produce? All the yummy things you can eat and drink. If you grow it, raise it, brew it, bake it, ferment it, milk it or mould it – you’re a producer. "
+ no_producer_help: "Maybe you’re someone who sells or distributes food that you or others have produced. You might be a hub, co-op, buying group, retailer, wholesaler, or someone else."
create_profile: "Create Profile"
about:
title: "About"
- headline: "Nice one!"
- message: "Now let's flesh out the details about"
- success: "Success! %{enterprise} added to the Open Food Network"
- registration_exit_message: "If you exit this wizard at any stage, you can continue to create your profile by going to the admin interface."
- enterprise_description: "Short Description"
+ headline: "Add to your profile"
+ message: "Your profile is set up! You can complete the details later, or start adding details for"
+ success: "Welcome to the Open Food Network, %{enterprise}!"
+ registration_exit_message: "If you need a bit more time, you can upload an image and add to your story in the Admin section of your profile another time."
+ enterprise_description: "What do you do?"
enterprise_description_placeholder: "A short sentence describing your enterprise"
- enterprise_long_desc: "Long Description"
- enterprise_long_desc_placeholder: "This is your opportunity to tell the story of your enterprise - what makes you different and wonderful? We'd suggest keeping your description to under 600 characters or 150 words."
+ enterprise_long_desc: "Tell us more"
+ enterprise_long_desc_placeholder: "This is your opportunity to tell your story – what makes your enterprise different and wonderful? Try to keep it around 150 words so it doesn’t get cut off."
enterprise_long_desc_length: "%{num} characters / up to 600 recommended"
enterprise_abn: "ABN"
enterprise_abn_placeholder: "eg. 99 123 456 789"
@@ -1809,31 +1809,31 @@ en_AU:
enterprise_acn_placeholder: "eg. 123 456 789"
enterprise_tax_required: "You need to make a selection."
images:
- title: "Images"
- headline: "Thanks!"
- description: "Let's upload some pretty pictures so your profile looks great! :)"
+ title: "Logo"
+ headline: "Upload your logo"
+ description: "Help people recognise you (and make your profile stand out!)"
uploading: "Uploading..."
continue: "Continue"
back: "Back"
logo:
- select_logo: "Step 1. Select Logo Image"
- logo_tip: "Tip: Square images will work best, preferably at least 300×300px"
- logo_label: "Choose a logo image"
- logo_drag: "Drag and drop your logo here"
- review_logo: "Step 2. Review Your Logo"
- review_logo_tip: "Tip: for best results, your logo should fill the available space"
- logo_placeholder: "Your logo will appear here for review once uploaded"
+ select_logo: "Select file to upload"
+ logo_tip: "Try using a square image, around 300x300px."
+ logo_label: "Select a file"
+ logo_drag: "Drag and drop here"
+ review_logo: "How does it look?"
+ review_logo_tip: "For best results, the image should fill the available space"
+ logo_placeholder: "Your logo will appear here once it's uploaded"
promo:
- select_promo_image: "Step 3. Select Promo Image"
- promo_image_tip: "Tip: Shown as a banner, preferred size is 1200×260px"
- promo_image_label: "Choose a promo image"
- promo_image_drag: "Drag and drop your promo here"
- review_promo_image: "Step 4. Review Your Promo Banner"
- review_promo_image_tip: "Tip: for best results, your promo image should fill the available space"
- promo_image_placeholder: "Your logo will appear here for review once uploaded"
+ select_promo_image: "Choose a shop header image"
+ promo_image_tip: "This displays as a banner. For best results try an image that's around 1200×260px"
+ promo_image_label: "Choose a header image"
+ promo_image_drag: "Drag and drop your header image here"
+ review_promo_image: "How does it look?"
+ review_promo_image_tip: "For best results, your banner image should fill the available space"
+ promo_image_placeholder: "Your logo will appear here once it's uploaded"
social:
title: "Social"
- enterprise_final_step: "Final step!"
+ enterprise_final_step: "One last thing..."
enterprise_social_text: "How can people find %{enterprise} online?"
website: "Website"
website_placeholder: "eg. openfoodnetwork.org.au"
@@ -1851,9 +1851,9 @@ en_AU:
text: "You have reached the limit for the number of enterprises you are allowed to own on the"
action: "Return to the homepage"
finished:
- headline: "Finished!"
+ headline: "You’re all set up! "
thanks: "Thanks for filling out the details for %{enterprise}."
- login: "To manage your new Enterprise, go to openfoodnetwork.org.au/admin"
+ login: "To manage your new Enterprise, go to openfoodnetwork.org.au/admin\n\nYou can also get to your Admin page in the top righthand corner of the Open Food Network homepage, just to the left of the shopping cart symbol."
action: "Open Food Network home"
back: "Back"
continue: "Continue"
@@ -3126,6 +3126,12 @@ en_AU:
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/en_BE.yml b/config/locales/en_BE.yml
index 7757ac6641..eebd4d0c53 100644
--- a/config/locales/en_BE.yml
+++ b/config/locales/en_BE.yml
@@ -3044,6 +3044,12 @@ en_BE:
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/en_CA.yml b/config/locales/en_CA.yml
index 53dd86f2cd..2981d7a4f8 100644
--- a/config/locales/en_CA.yml
+++ b/config/locales/en_CA.yml
@@ -3212,6 +3212,12 @@ en_CA:
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/en_DE.yml b/config/locales/en_DE.yml
index 4ade7e753b..b4045f6b91 100644
--- a/config/locales/en_DE.yml
+++ b/config/locales/en_DE.yml
@@ -3060,6 +3060,12 @@ en_DE:
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/en_FR.yml b/config/locales/en_FR.yml
index c498d5da68..b37822f390 100644
--- a/config/locales/en_FR.yml
+++ b/config/locales/en_FR.yml
@@ -2274,6 +2274,7 @@ en_FR:
enterprise_register_success_notice: "Congratulations! Registration for %{enterprise} is complete!"
enterprise_bulk_update_success_notice: "Enterprises updated successfully"
enterprise_bulk_update_error: 'Update failed'
+ enterprise_shop_show_error: "The shop you are looking for doesn't exist or is inactive on OFN. Please check other shops."
order_cycles_create_notice: 'Your order cycle has been created.'
order_cycles_update_notice: 'Your order cycle has been updated.'
order_cycles_bulk_update_notice: 'Order cycles have been updated.'
@@ -3213,6 +3214,12 @@ en_FR:
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml
index 81654ea6be..fe89f73505 100644
--- a/config/locales/en_GB.yml
+++ b/config/locales/en_GB.yml
@@ -57,7 +57,7 @@ en_GB:
attributes:
subscription_line_items:
at_least_one_product: "^Please add at least one product"
- not_available: "^%{name} is not available from the selected schedule"
+ not_available: "^%{name} is not available from the selected schedule. Your changes have not been saved."
ends_at:
after_begins_at: "must be after begins at"
customer:
@@ -294,7 +294,7 @@ en_GB:
shipping_method: Shipping Method
shop: Shop
sku: SKU
- status_state: County
+ status_state: State
tags: Tags
variant: Variant
weight: Weight
@@ -2039,7 +2039,7 @@ en_GB:
admin_share_city: "City"
admin_share_zipcode: "Postcode"
admin_share_country: "Country"
- admin_share_state: "County"
+ admin_share_state: "State"
hub_sidebar_hubs: "Hubs"
hub_sidebar_none_available: "None Available"
hub_sidebar_manage: "Manage"
@@ -2549,7 +2549,7 @@ en_GB:
no_changes_to_save: No changes to save.'
no_authorisation: "I couldn't get authorisation to save those changes, so they remain unsaved."
some_trouble: "I had some trouble saving: %{errors}"
- changing_on_hand_stock: Changing on hand stock levels...
+ changing_on_hand_stock: Changing 'in stock' stock levels...
stock_reset: Stocks reset to defaults.
tag_rules:
show_hide_variants: 'Show or Hide variants in my shopfront'
@@ -2748,7 +2748,7 @@ en_GB:
cannot_create_returns: Cannot create returns as this order has no shipped units.
select_stock: "Select stock"
location: "Location"
- count_on_hand: "Count On Hand"
+ count_on_hand: "Count In Stock"
quantity: "Quantity"
package_from: "package from"
item_description: "Item Description"
@@ -3219,6 +3219,12 @@ en_GB:
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/en_NZ.yml b/config/locales/en_NZ.yml
index 1c0d06883c..3a051e71c6 100644
--- a/config/locales/en_NZ.yml
+++ b/config/locales/en_NZ.yml
@@ -3213,6 +3213,12 @@ en_NZ:
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml
index 9375fa03b4..8b955e8a3b 100644
--- a/config/locales/en_US.yml
+++ b/config/locales/en_US.yml
@@ -3057,6 +3057,12 @@ en_US:
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/en_ZA.yml b/config/locales/en_ZA.yml
index 0516828725..f60d79d734 100644
--- a/config/locales/en_ZA.yml
+++ b/config/locales/en_ZA.yml
@@ -3058,6 +3058,12 @@ en_ZA:
used_saved_card: "Use a saved card:"
or_enter_new_card: "Or, enter details for a new card:"
remember_this_card: Remember this card?
+ stripe_sca:
+ choose_one: Choose one
+ enter_new_card: Enter details for a new card
+ used_saved_card: "Use a saved card:"
+ or_enter_new_card: "Or, enter details for a new card:"
+ remember_this_card: Remember this card?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 0520566763..9acc65d841 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -3065,6 +3065,12 @@ es:
used_saved_card: "Usa una tarjeta guardada:"
or_enter_new_card: "O introduce los detalles de una nueva tarjeta:"
remember_this_card: ¿Recordar esta tarjeta?
+ stripe_sca:
+ choose_one: Elige uno
+ enter_new_card: Introduce los detalles para una nueva tarjeta
+ used_saved_card: "Usa una tarjeta guardada:"
+ or_enter_new_card: "O introduce los detalles de una nueva tarjeta:"
+ remember_this_card: ¿Recordar esta tarjeta?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 434aa1bc4b..afb17379ed 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -701,7 +701,7 @@ fr:
enable_subscriptions_tip: "Activer la fonction abonnements?"
enable_subscriptions_false: "Désactivé"
enable_subscriptions_true: "Activé"
- shopfront_message: "Message d'accueil boutique ouverte"
+ shopfront_message: "Message d'accueil"
shopfront_message_placeholder: >
Vous pouvez indiquer ici un message de bienvenue ou un message expliquant
les particularités de votre boutique. Ce message s'affiche dans l'onglet
@@ -2221,7 +2221,7 @@ fr:
no_change_to_save: "Pas de changement à sauvegarder"
user_invited: "%{email}a été invité à gérer cette entreprise"
add_manager: "Ajouter un utilisateur existant"
- users: "Utilisateurs"
+ users: "Gestionnaires"
about: "A propos"
images: "Images"
web: "Web"
@@ -2278,6 +2278,7 @@ fr:
enterprise_register_success_notice: "Bravo ! L'entreprise %{enterprise} est maintenant inscrite sur Open Food France :-)"
enterprise_bulk_update_success_notice: "Entreprises mises à jour avec succès"
enterprise_bulk_update_error: 'Echec dans la mise à jour'
+ enterprise_shop_show_error: "La boutique que vous recherchez n'existe pas ou est inactive. Veuillez sélectionner une boutique depuis la liste ci-dessous."
order_cycles_create_notice: 'Votre cycle de vente a été créé.'
order_cycles_update_notice: 'Votre cycle de vente a été mis à jour.'
order_cycles_bulk_update_notice: 'Des cycles de vente ont été mis à jour.'
@@ -3243,6 +3244,12 @@ fr:
used_saved_card: "Utiliser une carte sauvegardée :"
or_enter_new_card: "Ou entrez les informations pour utiliser une nouvelle carte :"
remember_this_card: Se souvenir de cette carte ?
+ stripe_sca:
+ choose_one: En choisir un
+ enter_new_card: Entrer les informations pour la nouvelle carte
+ used_saved_card: "Utiliser une carte sauvegardée :"
+ or_enter_new_card: "Ou entrez les informations pour utiliser une nouvelle carte :"
+ remember_this_card: Se souvenir de cette carte ?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/fr_BE.yml b/config/locales/fr_BE.yml
index 5635ffe8f3..2bf0ba3f0e 100644
--- a/config/locales/fr_BE.yml
+++ b/config/locales/fr_BE.yml
@@ -3148,6 +3148,12 @@ fr_BE:
used_saved_card: "Utiliser une carte sauvegardée :"
or_enter_new_card: "Ou entrez les informations pour utiliser une nouvelle carte :"
remember_this_card: Se souvenir de cette carte ?
+ stripe_sca:
+ choose_one: En choisir un
+ enter_new_card: Entrer les informations pour la nouvelle carte
+ used_saved_card: "Utiliser une carte sauvegardée :"
+ or_enter_new_card: "Ou entrez les informations pour utiliser une nouvelle carte :"
+ remember_this_card: Se souvenir de cette carte ?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/fr_CA.yml b/config/locales/fr_CA.yml
index bf3cda687e..d8d045d2d9 100644
--- a/config/locales/fr_CA.yml
+++ b/config/locales/fr_CA.yml
@@ -3226,6 +3226,12 @@ fr_CA:
used_saved_card: "Utiliser une carte sauvegardée :"
or_enter_new_card: "Ou entrez les informations pour utiliser une nouvelle carte :"
remember_this_card: Se souvenir de cette carte ?
+ stripe_sca:
+ choose_one: En choisir un
+ enter_new_card: Entrer les informations pour la nouvelle carte
+ used_saved_card: "Utiliser une carte sauvegardée :"
+ or_enter_new_card: "Ou entrez les informations pour utiliser une nouvelle carte :"
+ remember_this_card: Se souvenir de cette carte ?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 06aa5e78dc..937e299de2 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -3123,6 +3123,12 @@ it:
used_saved_card: "usare la carta salvata"
or_enter_new_card: "Oppure, inserire dettagli di una nuova carta"
remember_this_card: Ricordare questa Carta?
+ stripe_sca:
+ choose_one: Scegli uno
+ enter_new_card: Inserire dettagli per una nuova carta
+ used_saved_card: "usare la carta salvata"
+ or_enter_new_card: "Oppure, inserire dettagli di una nuova carta"
+ remember_this_card: Ricordare questa Carta?
date_picker:
format: '%Y-%m-%d'
js_format: 'aa-mm-gg'
diff --git a/config/locales/nb.yml b/config/locales/nb.yml
index 25bfc2674a..1207b53957 100644
--- a/config/locales/nb.yml
+++ b/config/locales/nb.yml
@@ -2274,6 +2274,7 @@ nb:
enterprise_register_success_notice: "Gratulerer! Registrering for %{enterprise} er fullført!"
enterprise_bulk_update_success_notice: "Bedrifter oppdatert"
enterprise_bulk_update_error: 'Oppdatering mislyktes'
+ enterprise_shop_show_error: "Butikken du leter etter eksisterer ikke eller er inaktiv på OFN. Sjekk gjerne ut andre butikker."
order_cycles_create_notice: 'Din bestillingsrunde er opprettet.'
order_cycles_update_notice: 'Din bestillingsrunde har blitt oppdatert.'
order_cycles_bulk_update_notice: 'Bestillingsrundene er oppdatert.'
@@ -3212,6 +3213,12 @@ nb:
used_saved_card: "Bruk et lagret kort:"
or_enter_new_card: "Eller skriv inn detaljer for et nytt kort:"
remember_this_card: Husk dette kortet?
+ stripe_sca:
+ choose_one: Velg en
+ enter_new_card: Skriv inn detaljer for et nytt kort
+ used_saved_card: "Bruk et lagret kort:"
+ or_enter_new_card: "Eller skriv inn detaljer for et nytt kort:"
+ remember_this_card: Husk dette kortet?
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/nl_BE.yml b/config/locales/nl_BE.yml
index a587612b83..21955a3b4b 100644
--- a/config/locales/nl_BE.yml
+++ b/config/locales/nl_BE.yml
@@ -3053,6 +3053,12 @@ nl_BE:
used_saved_card: "Gebruik een opgeslaan kaart : "
or_enter_new_card: "Of, voer de gegevens in voor een nieuwe kaart : "
remember_this_card: 'Deze kaart onthouden? '
+ stripe_sca:
+ choose_one: 'Kies één '
+ enter_new_card: Voer de gegevens in voor een nieuwe kaart
+ used_saved_card: "Gebruik een opgeslaan kaart : "
+ or_enter_new_card: "Of, voer de gegevens in voor een nieuwe kaart : "
+ remember_this_card: 'Deze kaart onthouden? '
date_picker:
format: '%Y-%m-%d'
js_format: 'yy-mm-dd'
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index ec4deb12f5..de852adb84 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -2984,6 +2984,12 @@ pt:
used_saved_card: "Usar um cartão guardado:"
or_enter_new_card: "Ou, introduza detalhes para um novo cartão:"
remember_this_card: Lembra-se deste cartão?
+ stripe_sca:
+ choose_one: Escolha um
+ enter_new_card: Inserir detalhes de novo cartão
+ used_saved_card: "Usar um cartão guardado:"
+ or_enter_new_card: "Ou, introduza detalhes para um novo cartão:"
+ remember_this_card: Lembra-se deste cartão?
date_picker:
format: '%Y-%m-%d'
js_format: 'aa-mm-dd'
diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml
index 0ca3d063fe..a48684cb62 100644
--- a/config/locales/pt_BR.yml
+++ b/config/locales/pt_BR.yml
@@ -1572,7 +1572,7 @@ pt_BR:
login_invalid: "E-mail ou senha inválidos"
modal_hubs: "Central de alimentos"
modal_hubs_abstract: Nossas centrais são o ponto de contato entre você e as pessoas que produzem sua comida!
- modal_hubs_content1: 'Você pode procurar por uma central conveniente à você por localização ou nome. Alguns possuem múltiplos pontos de entrega, onde você pode retirar suas compras, e outros ainda entregam na sua casa. Cada um é um ponto de venda independente, e por isso as ofertas e maneira de operar podem variar de um para outro. '
+ modal_hubs_content1: 'Você pode procurar a central mais próxima por localização ou nome. Alguns possuem múltiplos pontos de entrega, onde você pode retirar suas compras, e outros ainda entregam na sua casa. Cada um é um ponto de venda independente, e por isso as ofertas e maneira de operar podem variar de um para outro. '
modal_hubs_content2: Você só pode comprar de uma central por vez.
modal_groups: "Grupos / Regiões"
modal_groups_content1: Estas são as organizações e as relações entre as centrais que compõem a Open Food Brasil.
@@ -2273,6 +2273,7 @@ pt_BR:
enterprise_register_success_notice: "Parabéns! O registro para %{enterprise} está completo!"
enterprise_bulk_update_success_notice: "Iniciativas atualizadas com sucesso"
enterprise_bulk_update_error: 'Atualização falhou'
+ enterprise_shop_show_error: "A loja que você está procurando não existe na OFN ou está inativa. Por favor, busque por outras lojas. "
order_cycles_create_notice: 'Seu ciclo de pedidos foi criado.'
order_cycles_update_notice: 'Seu ciclo de pedidos foi atualizado.'
order_cycles_bulk_update_notice: 'Ciclos de pedidos foram atualizados.'
@@ -2593,6 +2594,79 @@ pt_BR:
signup_or_login: "Faça seu cadastro ou login para começar"
have_an_account: "Já possui um conta?"
action_login: "Entrar agora"
+ inflections:
+ each:
+ one: "cada"
+ other: "cada"
+ bunch:
+ one: "maço"
+ other: "maços"
+ pack:
+ one: "pacote"
+ other: "pacotes"
+ box:
+ one: "caixa"
+ other: "caixas"
+ bottle:
+ one: "garrafa"
+ other: "garrafas"
+ jar:
+ one: "jarro"
+ other: "jarros"
+ head:
+ one: "cabeça"
+ other: "cabeças"
+ bag:
+ one: "sacolas"
+ other: "sacolas"
+ loaf:
+ one: "pão"
+ other: "pães"
+ single:
+ one: "simples"
+ other: "simples"
+ tub:
+ one: "vaso"
+ other: "vasos"
+ punnet:
+ one: "vasilha"
+ other: "vasilhas"
+ packet:
+ one: "pacote"
+ other: "pacotes"
+ item:
+ one: "item"
+ other: "items"
+ dozen:
+ one: "dúzia"
+ other: "dúzias"
+ unit:
+ one: "unidade"
+ other: "unidades"
+ serve:
+ one: "servido"
+ other: "servidos"
+ tray:
+ one: "bandeja"
+ other: "bandejas"
+ piece:
+ one: "pedaço"
+ other: "pedaços"
+ pot:
+ one: "pote"
+ other: "potes"
+ bundle:
+ one: "pacote"
+ other: "pacotes"
+ flask:
+ one: "frasco"
+ other: "frascos"
+ basket:
+ one: "cesta"
+ other: "cestas"
+ sack:
+ one: "saco"
+ other: "sacos"
producers:
signup:
start_free_profile: "Comece com um perfil gratuito e expanda quando estiver pronto!"
@@ -3144,6 +3218,12 @@ pt_BR:
used_saved_card: "Use um cartão salvo:"
or_enter_new_card: "Ou insira os detalhes de um novo cartão:"
remember_this_card: Lembra-se deste cartão?
+ stripe_sca:
+ choose_one: Escolha um
+ enter_new_card: Digite os detalhes do novo cartão
+ used_saved_card: "Use um cartão já registrado"
+ or_enter_new_card: "Ou, adicione os dados para um novo cartão:"
+ remember_this_card: Lembrar deste cartão?
date_picker:
format: '%A-%m-'
js_format: 'aa-mm-dd'
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index f07152f282..4c39795e6d 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -1305,7 +1305,7 @@ tr:
cookie_stripe_desc: "Sahtekarlık tespiti için ödeme işlemcimiz Stripe tarafından toplanan veriler https://stripe.com/cookies-policy/legal. Tüm mağazalar Stripe'ı ödeme yöntemi olarak kullanmaz, ancak dolandırıcılığın tüm sayfalara uygulanmasını önlemek iyi bir yöntemdir. Stripe muhtemelen sayfalarımızdan hangilerinin genellikle API'leriyle etkileşime girdiğini gösteren bir resim oluşturur ve daha sonra olağandışı bir şeyi işaretler. Dolayısıyla, Stripe çerezinin ayarlanması, kullanıcıya ödeme yönteminin sağlanmasından daha geniş bir işleve sahiptir. Kaldırılması hizmetin güvenliğini etkileyebilir. Stripe hakkında daha fazla bilgi edinebilir ve gizlilik politikasını https://stripe.com/privacy adresinde okuyabilirsiniz."
statistics_cookies: "İstatistik Çerezleri"
statistics_cookies_desc: "Aşağıdakiler mecburi değildir, ancak kullanıcı davranışını analiz etmemize, en çok kullandığınız veya kullanmadığınız özellikleri belirlememize veya kullanıcı deneyimi sorunlarını anlamamıza vb. izin vererek size en iyi kullanıcı deneyimini sunmaya yardımcı olur."
- statistics_cookies_analytics_desc_html: "Platform kullanım verilerini toplamak ve analiz etmek için, Google Analytics'i Spree ( temelde oluşturduğumuz e-ticaret açık kaynak yazılımı) ile bağlantılı varsayılan hizmet olduğu için kullanıyoruz, ancak vizyonumuz Matomo'ya (eski Piwik, açık kaynak analitiği) geçmek GDPR uyumlu ve gizliliğinizi koruyan bir araç)."
+ statistics_cookies_analytics_desc_html: "Platform kullanım verilerini toplamak ve analiz etmek için, Google Analytics'i Spree ( temelde oluşturduğumuz e-ticaret açık kaynak yazılımı) ile bağlantılı varsayılan hizmet olduğu için kullanıyoruz, ancak vizyonumuz Matomoya (eski Piwik, açık kaynak analitiği) geçmek GDPR uyumlu ve gizliliğinizi koruyan bir araç)."
statistics_cookies_matomo_desc_html: "Platform kullanım verilerini toplamak ve analiz etmek için, GDPR uyumlu ve gizliliğinizi koruyan açık kaynaklı bir analiz aracı olan Matomo (eski Piwik) kullanıyoruz."
statistics_cookies_matomo_optout: "Matomo analitiğinden çıkmak istiyor musunuz? Herhangi bir kişisel veri toplamıyoruz ve Matomo hizmetimizi geliştirmemize yardımcı oluyor, ancak seçiminize saygı duyuyoruz :-)"
cookie_analytics_utma_desc: "Kullanıcıları ve oturumları ayırt etmek için kullanılır. Çerez, javascript kütüphanesi yürütüldüğünde ve mevcut __utma çerezleri olmadığında oluşturulur. Çerez, Google Analytics'e her veri gönderildiğinde güncellenir."
@@ -3209,6 +3209,12 @@ tr:
used_saved_card: "Kayıtlı bir kart kullanın:"
or_enter_new_card: "Veya yeni bir kart için bilgileri girin:"
remember_this_card: Bu kartı hatırlıyor musunuz?
+ stripe_sca:
+ choose_one: Birini seçin
+ enter_new_card: Yeni bir kart için bilgileri girin
+ used_saved_card: "Kayıtlı bir kart kullanın:"
+ or_enter_new_card: "Veya yeni bir kart için bilgileri girin:"
+ remember_this_card: Bu kartı hatırlıyor musunuz?
date_picker:
format: '% Y-% A-%d'
js_format: 'yy-aa-gg'
diff --git a/db/migrate/20191202165700_add_custom_data_to_versions.rb b/db/migrate/20191202165700_add_custom_data_to_versions.rb
new file mode 100644
index 0000000000..45a10ed67e
--- /dev/null
+++ b/db/migrate/20191202165700_add_custom_data_to_versions.rb
@@ -0,0 +1,5 @@
+class AddCustomDataToVersions < ActiveRecord::Migration
+ def change
+ add_column :versions, :custom_data, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2e6e0ec609..5d45d6494b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20191023172424) do
+ActiveRecord::Schema.define(:version => 20191202165700) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1207,6 +1207,7 @@ ActiveRecord::Schema.define(:version => 20191023172424) do
t.string "whodunnit"
t.text "object"
t.datetime "created_at"
+ t.string "custom_data"
end
add_index "versions", ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id", using: :btree
diff --git a/lib/active_merchant/billing/gateways/stripe_decorator.rb b/lib/active_merchant/billing/gateways/stripe_decorator.rb
new file mode 100644
index 0000000000..fcd23a6446
--- /dev/null
+++ b/lib/active_merchant/billing/gateways/stripe_decorator.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# Here we bring commit 823faaeab0d6d3bd75ee037ec894ab7c9d95d3a9 from ActiveMerchant v1.98.0
+# This is needed to make StripePaymentIntents work correctly
+# This can be removed once we upgrade to ActiveMerchant v1.98.0
+ActiveMerchant::Billing::StripeGateway.class_eval do
+ def authorization_from(success, url, method, response)
+ return response.fetch('error', {})['charge'] unless success
+
+ if url == 'customers'
+ [response['id'], response.dig('sources', 'data').first&.dig('id')].join('|')
+ elsif method == :post &&
+ (url.match(%r{customers/.*/cards}) || url.match(%r{payment_methods/.*/attach}))
+ [response['customer'], response['id']].join('|')
+ else
+ response['id']
+ end
+ end
+end
diff --git a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb
new file mode 100644
index 0000000000..9c5c602c7a
--- /dev/null
+++ b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb
@@ -0,0 +1,277 @@
+# frozen_string_literal: true
+
+# Here we bring commit 823faaeab0d6d3bd75ee037ec894ab7c9d95d3a9 from ActiveMerchant v1.98.0
+# This class integrates with the new StripePaymentIntents API
+# This can be removed once we upgrade to ActiveMerchant v1.98.0
+require 'active_support/core_ext/hash/slice'
+
+module ActiveMerchant #:nodoc:
+ module Billing #:nodoc:
+ # This gateway uses the current Stripe
+ # {Payment Intents API}[https://stripe.com/docs/api/payment_intents].
+ # For the legacy API, see the Stripe gateway
+ class StripePaymentIntentsGateway < StripeGateway
+ ALLOWED_METHOD_STATES = %w[automatic manual].freeze
+ ALLOWED_CANCELLATION_REASONS = %w[duplicate fraudulent requested_by_customer abandoned].freeze
+ CREATE_INTENT_ATTRIBUTES =
+ %i[description statement_descriptor receipt_email save_payment_method].freeze
+ CONFIRM_INTENT_ATTRIBUTES =
+ %i[receipt_email return_url save_payment_method setup_future_usage off_session].freeze
+ UPDATE_INTENT_ATTRIBUTES =
+ %i[description statement_descriptor receipt_email setup_future_usage].freeze
+ DEFAULT_API_VERSION = '2019-05-16'
+
+ def create_intent(money, payment_method, options = {})
+ post = {}
+ add_amount(post, money, options, true)
+ add_capture_method(post, options)
+ add_confirmation_method(post, options)
+ add_customer(post, options)
+ add_payment_method_token(post, payment_method, options)
+ add_metadata(post, options)
+ add_return_url(post, options)
+ add_connected_account(post, options)
+ add_shipping_address(post, options)
+ setup_future_usage(post, options)
+
+ CREATE_INTENT_ATTRIBUTES.each do |attribute|
+ add_whitelisted_attribute(post, options, attribute)
+ end
+
+ commit(:post, 'payment_intents', post, options)
+ end
+
+ def show_intent(intent_id, options)
+ commit(:get, "payment_intents/#{intent_id}", nil, options)
+ end
+
+ def confirm_intent(intent_id, payment_method, options = {})
+ post = {}
+ add_payment_method_token(post, payment_method, options)
+ CONFIRM_INTENT_ATTRIBUTES.each do |attribute|
+ add_whitelisted_attribute(post, options, attribute)
+ end
+
+ commit(:post, "payment_intents/#{intent_id}/confirm", post, options)
+ end
+
+ def create_payment_method(payment_method, options = {})
+ post = {}
+ post[:type] = 'card'
+ post[:card] = {}
+ post[:card][:number] = payment_method.number
+ post[:card][:exp_month] = payment_method.month
+ post[:card][:exp_year] = payment_method.year
+ post[:card][:cvc] = payment_method.verification_value if payment_method.verification_value
+
+ commit(:post, 'payment_methods', post, options)
+ end
+
+ def update_intent(money, intent_id, payment_method, options = {})
+ post = {}
+ post[:amount] = money if money
+
+ add_payment_method_token(post, payment_method, options)
+ add_payment_method_types(post, options)
+ add_customer(post, options)
+ add_metadata(post, options)
+ add_shipping_address(post, options)
+ add_connected_account(post, options)
+
+ UPDATE_INTENT_ATTRIBUTES.each do |attribute|
+ add_whitelisted_attribute(post, options, attribute)
+ end
+
+ commit(:post, "payment_intents/#{intent_id}", post, options)
+ end
+
+ def authorize(money, payment_method, options = {})
+ create_intent(money,
+ payment_method,
+ options.merge!(confirm: true, capture_method: 'manual'))
+ end
+
+ def purchase(money, payment_method, options = {})
+ create_intent(money,
+ payment_method,
+ options.merge!(confirm: true, capture_method: 'automatic'))
+ end
+
+ def capture(money, intent_id, options = {})
+ post = {}
+ post[:amount_to_capture] = money
+ add_connected_account(post, options)
+ commit(:post, "payment_intents/#{intent_id}/capture", post, options)
+ end
+
+ def void(intent_id, options = {})
+ post = {}
+ if ALLOWED_CANCELLATION_REASONS.include?(options[:cancellation_reason])
+ post[:cancellation_reason] = options[:cancellation_reason]
+ end
+ commit(:post, "payment_intents/#{intent_id}/cancel", post, options)
+ end
+
+ def refund(money, intent_id, options = {})
+ intent = commit(:get, "payment_intents/#{intent_id}", nil, options)
+ charge_id = intent.params.dig('charges', 'data')[0].dig('id')
+ super(money, charge_id, options)
+ end
+
+ # Note: Not all payment methods are currently supported by the
+ # {Payment Methods API}[https://stripe.com/docs/payments/payment-methods]
+ # Current implementation will create
+ # a PaymentMethod object if the method is a token or credit card
+ # All other types will default to legacy Stripe store
+ def store(payment_method, options = {})
+ params = {}
+ post = {}
+
+ # If customer option is provided, create a payment method and attach to customer id
+ # Otherwise, create a customer, then attach
+ # if payment_method.is_a?(StripePaymentToken) ||
+ # payment_method.is_a?(ActiveMerchant::Billing::CreditCard)
+ add_payment_method_token(params, payment_method, options)
+ if options[:customer]
+ customer_id = options[:customer]
+ else
+ post[:validate] = options[:validate] unless options[:validate].nil?
+ post[:description] = options[:description] if options[:description]
+ post[:email] = options[:email] if options[:email]
+ customer = commit(:post, 'customers', post, options)
+ customer_id = customer.params['id']
+
+ # return the stripe response if expected customer id is not present
+ return customer if customer_id.nil?
+ end
+ commit(:post,
+ "payment_methods/#{params[:payment_method]}/attach",
+ { customer: customer_id }, options)
+ # else
+ # super(payment, options)
+ # end
+ end
+
+ def unstore(identification, options = {}, deprecated_options = {})
+ if identification.include?('pm_')
+ _, payment_method = identification.split('|')
+ commit(:post, "payment_methods/#{payment_method}/detach", nil, options)
+ else
+ super(identification, options, deprecated_options)
+ end
+ end
+
+ private
+
+ def add_whitelisted_attribute(post, options, attribute)
+ post[attribute] = options[attribute] if options[attribute]
+ post
+ end
+
+ def add_capture_method(post, options)
+ capture_method = options[:capture_method].to_s
+ post[:capture_method] = capture_method if ALLOWED_METHOD_STATES.include?(capture_method)
+ post
+ end
+
+ def add_confirmation_method(post, options)
+ confirmation_method = options[:confirmation_method].to_s
+ if ALLOWED_METHOD_STATES.include?(confirmation_method)
+ post[:confirmation_method] = confirmation_method
+ end
+ post
+ end
+
+ def add_customer(post, options)
+ customer = options[:customer].to_s
+ post[:customer] = customer if customer.start_with?('cus_')
+ post
+ end
+
+ def add_return_url(post, options)
+ return unless options[:confirm]
+
+ post[:confirm] = options[:confirm]
+ post[:return_url] = options[:return_url] if options[:return_url]
+ post
+ end
+
+ def add_payment_method_token(post, payment_method, options)
+ return if payment_method.nil?
+
+ if payment_method.is_a?(ActiveMerchant::Billing::CreditCard)
+ p = create_payment_method(payment_method, options)
+ payment_method = p.params['id']
+ end
+
+ if payment_method.is_a?(StripePaymentToken)
+ post[:payment_method] = payment_method.payment_data['id']
+ elsif payment_method.is_a?(String)
+ if payment_method.include?('|')
+ customer_id, payment_method_id = payment_method.split('|')
+ token = payment_method_id
+ post[:customer] = customer_id
+ else
+ token = payment_method
+ end
+ post[:payment_method] = token
+ end
+ end
+
+ def add_payment_method_types(post, options)
+ payment_method_types = options[:payment_method_types] if options[:payment_method_types]
+ return if payment_method_types.nil?
+
+ post[:payment_method_types] = Array(payment_method_types)
+ post
+ end
+
+ def setup_future_usage(post, options = {})
+ if %w(on_session off_session).include?(options[:setup_future_usage])
+ post[:setup_future_usage] = options[:setup_future_usage]
+ end
+ if options[:off_session] && options[:confirm] == true
+ post[:off_session] = options[:off_session]
+ end
+ post
+ end
+
+ def add_connected_account(post, options = {})
+ return unless transfer_data = options[:transfer_data]
+
+ post[:transfer_data] = {}
+ if transfer_data[:destination]
+ post[:transfer_data][:destination] = transfer_data[:destination]
+ end
+ post[:transfer_data][:amount] = transfer_data[:amount] if transfer_data[:amount]
+ post[:on_behalf_of] = options[:on_behalf_of] if options[:on_behalf_of]
+ post[:transfer_group] = options[:transfer_group] if options[:transfer_group]
+ post[:application_fee_amount] = options[:application_fee] if options[:application_fee]
+ post
+ end
+
+ def add_shipping_address(post, options = {})
+ return unless shipping = options[:shipping]
+
+ post[:shipping] = {}
+ post[:shipping][:address] = {}
+ post[:shipping][:address][:line1] = shipping[:address][:line1]
+ post[:shipping][:address][:city] = shipping[:address][:city] if shipping[:address][:city]
+ if shipping[:address][:country]
+ post[:shipping][:address][:country] = shipping[:address][:country]
+ end
+ post[:shipping][:address][:line2] = shipping[:address][:line2] if shipping[:address][:line2]
+ if shipping[:address][:postal_code]
+ post[:shipping][:address][:postal_code] = shipping[:address][:postal_code]
+ end
+ post[:shipping][:address][:state] = shipping[:address][:state] if shipping[:address][:state]
+
+ post[:shipping][:name] = shipping[:name]
+ post[:shipping][:carrier] = shipping[:carrier] if shipping[:carrier]
+ post[:shipping][:phone] = shipping[:phone] if shipping[:phone]
+ post[:shipping][:tracking_number] = shipping[:tracking_number] if shipping[:tracking_number]
+ post
+ end
+ end
+ end
+end
diff --git a/lib/open_food_network/order_and_distributor_report.rb b/lib/open_food_network/order_and_distributor_report.rb
index b32ab803cc..64f46f41d1 100644
--- a/lib/open_food_network/order_and_distributor_report.rb
+++ b/lib/open_food_network/order_and_distributor_report.rb
@@ -68,7 +68,7 @@ module OpenFoodNetwork
else
orders.
where('spree_orders.id NOT IN (?)',
- @permissions.editable_orders)
+ @permissions.editable_orders.select(&:id))
end
end
diff --git a/lib/open_food_network/order_cycle_form_applicator.rb b/lib/open_food_network/order_cycle_form_applicator.rb
index c729bca83d..63af289913 100644
--- a/lib/open_food_network/order_cycle_form_applicator.rb
+++ b/lib/open_food_network/order_cycle_form_applicator.rb
@@ -150,7 +150,7 @@ module OpenFoodNetwork
receiver = @order_cycle.coordinator
exchange = find_exchange(sender.id, receiver.id, true)
- requested_ids = attrs[:variants].select{ |_k, v| v }.keys.map(&:to_i) # Only the ids the user has requested
+ requested_ids = variants_to_a(attrs[:variants]) # Only the ids the user has requested
existing_ids = exchange.present? ? exchange.variants.pluck(:id) : [] # The ids that already exist
editable_ids = editable_variant_ids_for_incoming_exchange_between(sender, receiver) # The ids we are allowed to add/remove
@@ -167,7 +167,7 @@ module OpenFoodNetwork
receiver = Enterprise.find(attrs[:enterprise_id])
exchange = find_exchange(sender.id, receiver.id, false)
- requested_ids = attrs[:variants].select{ |_k, v| v }.keys.map(&:to_i) # Only the ids the user has requested
+ requested_ids = variants_to_a(attrs[:variants]) # Only the ids the user has requested
existing_ids = exchange.present? ? exchange.variants.pluck(:id) : [] # The ids that already exist
editable_ids = editable_variant_ids_for_outgoing_exchange_between(sender, receiver) # The ids we are allowed to add/remove
@@ -185,7 +185,9 @@ module OpenFoodNetwork
end
def variants_to_a(variants)
- variants.select { |_k, v| v }.keys.map(&:to_i).sort
+ return [] unless variants
+
+ variants.select { |_k, v| v }.keys.map(&:to_i)
end
end
end
diff --git a/lib/open_food_network/proxy_order_syncer.rb b/lib/open_food_network/proxy_order_syncer.rb
index 7cd53c74ba..8f867057ab 100644
--- a/lib/open_food_network/proxy_order_syncer.rb
+++ b/lib/open_food_network/proxy_order_syncer.rb
@@ -17,29 +17,36 @@ module OpenFoodNetwork
end
def sync!
- return sync_all! if @subscriptions
+ return sync_subscriptions! if @subscriptions
+
return initialise_proxy_orders! unless @subscription.id
- create_proxy_orders!
- remove_orphaned_proxy_orders!
+ sync_subscription!
end
private
- def sync_all!
+ def sync_subscriptions!
@subscriptions.each do |subscription|
@subscription = subscription
- create_proxy_orders!
- remove_orphaned_proxy_orders!
+ sync_subscription!
end
end
def initialise_proxy_orders!
uninitialised_order_cycle_ids.each do |order_cycle_id|
+ Rails.logger.info "Initializing Proxy Order " \
+ "of subscription #{@subscription.id} in order cycle #{order_cycle_id}"
proxy_orders << ProxyOrder.new(subscription: subscription, order_cycle_id: order_cycle_id)
end
end
+ def sync_subscription!
+ Rails.logger.info "Syncing Proxy Orders of subscription #{@subscription.id}"
+ create_proxy_orders!
+ remove_orphaned_proxy_orders!
+ end
+
def create_proxy_orders!
return unless not_closed_in_range_order_cycles.any?
@@ -58,6 +65,8 @@ module OpenFoodNetwork
orphaned_proxy_orders.where(nil).delete_all
end
+ # Remove Proxy Orders that have not been placed yet
+ # and are in Order Cycles that are out of range
def orphaned_proxy_orders
orphaned = proxy_orders.where(placed_at: nil)
order_cycle_ids = in_range_order_cycles.pluck(:id)
diff --git a/lib/open_food_network/subscription_payment_updater.rb b/lib/open_food_network/subscription_payment_updater.rb
index 1c4d275db1..4a9c8fa144 100644
--- a/lib/open_food_network/subscription_payment_updater.rb
+++ b/lib/open_food_network/subscription_payment_updater.rb
@@ -30,7 +30,8 @@ module OpenFoodNetwork
end
def card_required?
- payment.payment_method.is_a? Spree::Gateway::StripeConnect
+ [Spree::Gateway::StripeConnect,
+ Spree::Gateway::StripeSCA].include? payment.payment_method.class
end
def card_set?
diff --git a/lib/open_food_network/subscription_summarizer.rb b/lib/open_food_network/subscription_summarizer.rb
index 7a429a9e3b..6e4b95da8b 100644
--- a/lib/open_food_network/subscription_summarizer.rb
+++ b/lib/open_food_network/subscription_summarizer.rb
@@ -17,6 +17,7 @@ module OpenFoodNetwork
end
def record_issue(type, order, message = nil)
+ Rails.logger.info "Issue in Subscription Order #{order.id}: #{type}"
summary_for(order).record_issue(type, order, message)
end
diff --git a/lib/stripe/credit_card_cloner.rb b/lib/stripe/credit_card_cloner.rb
new file mode 100644
index 0000000000..64b67b5345
--- /dev/null
+++ b/lib/stripe/credit_card_cloner.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+# Here we clone
+# - a card (card_*) or payment_method (pm_*) stored (in a customer) in a platform account
+# into
+# - a payment method (pm_*) (in a new customer) in a connected account
+#
+# This is required when using the Stripe Payment Intents API:
+# - the customer and payment methods are stored in the platform account
+# so that they can be re-used across multiple sellers
+# - when a card needs to be charged, we need to create it in the seller's stripe account
+#
+# We are doing this process every time the card is charged:
+# - this means that, if the customer uses the same card on the same seller multiple times,
+# the card will be created multiple times on the seller's account
+# - to avoid this, we would have to store the IDs of every card on each seller's stripe account
+# in our database (this way we only have to store the platform account ID)
+module Stripe
+ class CreditCardCloner
+ def clone(credit_card, connected_account_id)
+ new_payment_method = clone_payment_method(credit_card, connected_account_id)
+
+ # If no customer is given, it will clone the payment method only
+ return nil, new_payment_method.id if credit_card.gateway_customer_profile_id.blank?
+
+ new_customer = Stripe::Customer.create({ email: credit_card.user.email },
+ stripe_account: connected_account_id)
+ attach_payment_method_to_customer(new_payment_method.id,
+ new_customer.id,
+ connected_account_id)
+
+ [new_customer.id, new_payment_method.id]
+ end
+
+ private
+
+ def clone_payment_method(credit_card, connected_account_id)
+ platform_acct_payment_method_id = credit_card.gateway_payment_profile_id
+ customer_id = credit_card.gateway_customer_profile_id
+
+ Stripe::PaymentMethod.create({ customer: customer_id,
+ payment_method: platform_acct_payment_method_id },
+ stripe_account: connected_account_id)
+ end
+
+ def attach_payment_method_to_customer(payment_method_id, customer_id, connected_account_id)
+ Stripe::PaymentMethod.attach(payment_method_id,
+ { customer: customer_id },
+ stripe_account: connected_account_id)
+ end
+ end
+end
diff --git a/lib/stripe/profile_storer.rb b/lib/stripe/profile_storer.rb
index 1b8928e884..eb5212a1d2 100644
--- a/lib/stripe/profile_storer.rb
+++ b/lib/stripe/profile_storer.rb
@@ -54,10 +54,22 @@ module Stripe
def source_attrs_from(response)
{
- cc_type: @payment.source.cc_type, # side-effect of update_source!
- gateway_customer_profile_id: response.params['id'],
- gateway_payment_profile_id: response.params['default_source'] || response.params['default_card']
+ cc_type: @payment.source.cc_type,
+ gateway_customer_profile_id: customer_profile_id(response),
+ gateway_payment_profile_id: payment_profile_id(response)
}
end
+
+ def customer_profile_id(response)
+ response.params['customer'] || response.params['id']
+ end
+
+ def payment_profile_id(response)
+ if response.params['customer'] # Payment Intents API
+ response.params['id']
+ else
+ response.params['default_source'] || response.params['default_card']
+ end
+ end
end
end
diff --git a/spec/controllers/admin/bulk_line_items_controller_spec.rb b/spec/controllers/admin/bulk_line_items_controller_spec.rb
index e3d5536022..6b7fe5bff4 100644
--- a/spec/controllers/admin/bulk_line_items_controller_spec.rb
+++ b/spec/controllers/admin/bulk_line_items_controller_spec.rb
@@ -27,7 +27,7 @@ describe Admin::BulkLineItemsController, type: :controller do
context "as an administrator" do
before do
- allow(controller).to receive_messages spree_current_user: quick_login_as_admin
+ allow(controller).to receive_messages spree_current_user: create(:admin_user)
end
context "when no ransack params are passed in" do
diff --git a/spec/controllers/admin/schedules_controller_spec.rb b/spec/controllers/admin/schedules_controller_spec.rb
index 3690835809..299ff5e4d2 100644
--- a/spec/controllers/admin/schedules_controller_spec.rb
+++ b/spec/controllers/admin/schedules_controller_spec.rb
@@ -10,19 +10,6 @@ describe Admin::SchedulesController, type: :controller do
let!(:coordinated_schedule) { create(:schedule, order_cycles: [coordinated_order_cycle] ) }
let!(:uncoordinated_schedule) { create(:schedule, order_cycles: [other_order_cycle] ) }
- context "html" do
- context "where I manage an order cycle coordinator" do
- before do
- allow(controller).to receive_messages spree_current_user: managed_coordinator.owner
- end
-
- it "returns an empty @collection" do
- spree_get :index, format: :html
- expect(assigns(:collection)).to eq []
- end
- end
- end
-
context "json" do
context "where I manage an order cycle coordinator" do
before do
diff --git a/spec/controllers/api/products_controller_spec.rb b/spec/controllers/api/products_controller_spec.rb
index 5da8d89e32..6bf2cba312 100644
--- a/spec/controllers/api/products_controller_spec.rb
+++ b/spec/controllers/api/products_controller_spec.rb
@@ -253,13 +253,9 @@ describe Api::ProductsController, type: :controller do
end
it "filters results by import_date" do
- product.variants.first.import_date = 1.day.ago
- product2.variants.first.import_date = 2.days.ago
- product3.variants.first.import_date = 1.day.ago
-
- product.save
- product2.save
- product3.save
+ product.variants.first.update_attribute :import_date, 1.day.ago
+ product2.variants.first.update_attribute :import_date, 2.days.ago
+ product3.variants.first.update_attribute :import_date, 1.day.ago
api_get :bulk_products, { page: 1, per_page: 15, import_date: 1.day.ago.to_date.to_s }, format: :json
expect(returned_product_ids).to eq [product3.id, product.id]
diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb
index 084beec9bf..cf6f01cca8 100644
--- a/spec/controllers/enterprises_controller_spec.rb
+++ b/spec/controllers/enterprises_controller_spec.rb
@@ -139,4 +139,18 @@ describe EnterprisesController, type: :controller do
expect(response.status).to be 409
end
end
+
+ context "checking access on nonexistent enterprise" do
+ before do
+ spree_get :shop, id: "some_nonexistent_enterprise"
+ end
+
+ it "redirects to shops_path" do
+ expect(response).to redirect_to shops_path
+ end
+
+ it "shows a flash message with the error" do
+ expect(request.flash[:error]).to eq(I18n.t(:enterprise_shop_show_error))
+ end
+ end
end
diff --git a/spec/controllers/line_items_controller_spec.rb b/spec/controllers/line_items_controller_spec.rb
index 7e4e76bac9..116a05ef2f 100644
--- a/spec/controllers/line_items_controller_spec.rb
+++ b/spec/controllers/line_items_controller_spec.rb
@@ -52,7 +52,10 @@ describe LineItemsController, type: :controller do
end
context "where the item's order is associated with the current user" do
- before { order.update_attributes!(user_id: user.id) }
+ before do
+ order.update_attributes!(user_id: user.id)
+ allow(controller).to receive_messages spree_current_user: item.order.user
+ end
context "without an order cycle or distributor" do
it "denies deletion" do
diff --git a/spec/controllers/spree/admin/reports/distributor_totals_by_supplier_spec.rb b/spec/controllers/spree/admin/reports/distributor_totals_by_supplier_spec.rb
new file mode 100644
index 0000000000..357c0a8670
--- /dev/null
+++ b/spec/controllers/spree/admin/reports/distributor_totals_by_supplier_spec.rb
@@ -0,0 +1,249 @@
+require 'spec_helper'
+
+describe Spree::Admin::ReportsController, type: :controller do
+ let(:csv) do
+ <<-CSV.strip_heredoc
+ Hub,Producer,Product,Variant,Amount,Curr. Cost per Unit,Total Cost,Total Shipping Cost,Shipping Method
+ Mary's Online Shop,Freddy's Farm Shop,Beef - 5kg Trays,1g,1,12.0,12.0,"",Shipping Method
+ Mary's Online Shop,Freddy's Farm Shop,Fuji Apple,2g,1,4.0,4.0,"",Shipping Method
+ Mary's Online Shop,Freddy's Farm Shop,Fuji Apple,5g,5,12.0,60.0,"",Shipping Method
+ Mary's Online Shop,Freddy's Farm Shop,Fuji Apple,8g,3,15.0,45.0,"",Shipping Method
+ "",TOTAL,"","","","",121.0,2.0,""
+ CSV
+ end
+
+ before do
+ DefaultStockLocation.create!
+
+ delivery = marys_online_shop.shipping_methods.new(
+ name: "Home delivery",
+ require_ship_address: true,
+ calculator_type: "Spree::Calculator::FlatRate",
+ distributor_ids: [marys_online_shop.id]
+ )
+ delivery.shipping_categories << DefaultShippingCategory.find_or_create
+ delivery.calculator.preferred_amount = 2
+ delivery.save!
+ end
+
+ let(:taxonomy) { Spree::Taxonomy.create!(name: 'Products') }
+ let(:meat) do
+ Spree::Taxon.create!(name: 'Meat and Fish', parent_id: taxonomy.root.id, taxonomy_id: taxonomy.id)
+ end
+ let(:fruit) do
+ Spree::Taxon.create!(name: 'Fruit', parent_id: taxonomy.root.id, taxonomy_id: taxonomy.id)
+ end
+
+ let(:calculator) { Calculator::FlatPercentPerItem.new(preferred_flat_percent: 10) }
+
+ let(:mary) do
+ password = Spree::User.friendly_token
+ Spree::User.create!(
+ email: 'mary_retailer@example.org',
+ password: password,
+ password_confirmation: password,
+ confirmation_sent_at: Time.zone.now,
+ confirmed_at: Time.zone.now
+ )
+ end
+ let(:marys_online_shop) do
+ Enterprise.create!(
+ name: "Mary's Online Shop",
+ owner: mary,
+ is_primary_producer: false,
+ sells: "any",
+ address: create(:address)
+ )
+ end
+ before do
+ fee = marys_online_shop.enterprise_fees.new(
+ fee_type: "sales", name: "markup", inherits_tax_category: true
+ )
+ fee.calculator = calculator
+ fee.save!
+ end
+
+ let(:freddy) do
+ password = Spree::User.friendly_token
+ Spree::User.create!(
+ email: 'freddy_shop_farmer@example.org',
+ password: password,
+ password_confirmation: password,
+ confirmation_sent_at: Time.zone.now,
+ confirmed_at: Time.zone.now
+ )
+ end
+ let(:freddys_farm_shop) do
+ Enterprise.create!(
+ name: "Freddy's Farm Shop",
+ owner: freddy,
+ is_primary_producer: true,
+ sells: "own",
+ address: create(:address)
+ )
+ end
+ before do
+ fee = freddys_farm_shop.enterprise_fees.new(
+ fee_type: "sales", name: "markup", inherits_tax_category: true,
+ )
+ fee.calculator = calculator
+ fee.save!
+ end
+
+ let!(:beef) do
+ product = Spree::Product.new(
+ name: 'Beef - 5kg Trays',
+ price: 50.00,
+ supplier_id: freddys_farm_shop.id,
+ primary_taxon_id: meat.id,
+ variant_unit: "weight",
+ variant_unit_scale: 1,
+ unit_value: 1,
+ )
+ product.shipping_category = DefaultShippingCategory.find_or_create
+ product.save!
+ product.variants.first.update_attribute(:on_demand, true)
+
+ InventoryItem.create!(
+ enterprise: marys_online_shop,
+ variant: product.variants.first,
+ visible: true
+ )
+ VariantOverride.create!(
+ variant: product.variants.first,
+ hub: marys_online_shop,
+ price: 12,
+ on_demand: false,
+ count_on_hand: 5
+ )
+
+ product
+ end
+
+ let!(:apple) do
+ product = Spree::Product.new(
+ name: 'Fuji Apple',
+ price: 5.00,
+ supplier_id: freddys_farm_shop.id,
+ primary_taxon_id: fruit.id,
+ variant_unit: "weight",
+ variant_unit_scale: 1,
+ unit_value: 1,
+ shipping_category: DefaultShippingCategory.find_or_create
+ )
+ product.shipping_category = DefaultShippingCategory.find_or_create
+ product.save!
+ product.variants.first.update_attribute :on_demand, true
+
+ VariantOverride.create!(
+ variant: product.variants.first,
+ hub: marys_online_shop,
+ price: 12,
+ on_demand: false,
+ count_on_hand: 5
+ )
+
+ product
+ end
+ let!(:apple_variant_2) do
+ variant = apple.variants.create!(weight: 0.0, unit_value: 2.0, price: 4.0)
+ VariantOverride.create!(
+ variant: variant, hub: marys_online_shop, on_demand: false, count_on_hand: 4
+ )
+ variant
+ end
+ let!(:apple_variant_5) do
+ variant = apple.variants.create!(weight: 0.0, unit_value: 5.0, price: 12.0)
+ VariantOverride.create!(
+ variant: variant, hub: marys_online_shop, on_demand: false, count_on_hand: 5
+ )
+ variant.update_attribute :on_demand, true
+ variant
+ end
+ let!(:apple_variant_8) do
+ variant = apple.variants.create!(weight: 0.0, unit_value: 8.0, price: 15.0)
+ VariantOverride.create!(
+ variant: variant, hub: marys_online_shop, on_demand: false, count_on_hand: 3
+ )
+ variant.update_attribute :on_demand, true
+ variant
+ end
+
+ let!(:beef_variant) do
+ variant = beef.variants.first
+ OpenFoodNetwork::ScopeVariantToHub.new(marys_online_shop).scope(variant)
+ variant
+ end
+
+ let!(:order_cycle) do
+ cycle = OrderCycle.create!(
+ name: "Mary's Online Shop OC",
+ orders_open_at: 1.day.ago,
+ orders_close_at: 1.month.from_now,
+ coordinator: marys_online_shop
+ )
+ cycle.coordinator_fees << marys_online_shop.enterprise_fees.first
+
+ incoming = Exchange.create!(
+ order_cycle: cycle, sender: freddys_farm_shop, receiver: cycle.coordinator, incoming: true
+ )
+ outgoing = Exchange.create!(
+ order_cycle: cycle, sender: cycle.coordinator, receiver: marys_online_shop, incoming: false
+ )
+
+ freddys_farm_shop.supplied_products.each do |product|
+ incoming.variants << product.variants.first
+ outgoing.variants << product.variants.first
+ end
+
+ cycle
+ end
+
+ let(:order) do
+ create(
+ :order,
+ distributor: marys_online_shop,
+ order_cycle: order_cycle,
+ ship_address: create(:address)
+ )
+ end
+
+ before do
+ order.add_variant(beef_variant, 1, nil, order.currency)
+ order.add_variant(apple_variant_2, 1, nil, order.currency)
+ order.add_variant(apple_variant_5, 5, nil, order.currency)
+ order.add_variant(apple_variant_8, 3, nil, order.currency)
+
+ order.create_proposed_shipments
+ order.finalize!
+
+ order.completed_at = Time.zone.parse("2020-02-05 00:00:00 +1100")
+ order.save
+
+ allow(controller).to receive(:spree_current_user).and_return(mary)
+ end
+
+ it 'returns the right CSV' do
+ spree_post :orders_and_fulfillment, {
+ q: {
+ completed_at_gt: "2020-01-11 00:00:00 +1100",
+ completed_at_lt: "2020-02-12 00:00:00 +1100",
+ distributor_id_in: [marys_online_shop.id],
+ order_cycle_id_in: [""]
+ },
+ report_type: "order_cycle_distributor_totals_by_supplier",
+ csv: true
+ }
+
+ csv_report = assigns(:csv_report)
+ report_lines = csv_report.split("\n")
+ csv_fixture_lines = csv.split("\n")
+
+ expect(report_lines[0]).to eq(csv_fixture_lines[0])
+ expect(report_lines[1]).to eq(csv_fixture_lines[1])
+ expect(report_lines[2]).to eq(csv_fixture_lines[2])
+ expect(report_lines[3]).to eq(csv_fixture_lines[3])
+ expect(report_lines[4]).to eq(csv_fixture_lines[4])
+ expect(report_lines[5]).to eq(csv_fixture_lines[5])
+ end
+end
diff --git a/spec/controllers/spree/admin/variants_controller_spec.rb b/spec/controllers/spree/admin/variants_controller_spec.rb
index 2f8ed849b1..d88eaf12a4 100644
--- a/spec/controllers/spree/admin/variants_controller_spec.rb
+++ b/spec/controllers/spree/admin/variants_controller_spec.rb
@@ -69,7 +69,7 @@ module Spree
variant.exchanges << exchange
spree_delete :destroy, id: variant.id, product_id: variant.product.permalink, format: 'html'
- expect(variant.exchanges).to be_empty
+ expect(variant.exchanges.reload).to be_empty
end
end
end
diff --git a/spec/factories.rb b/spec/factories.rb
index 40de71effa..7d484e0ac8 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -151,8 +151,15 @@ FactoryBot.define do
preferred_enterprise_id { distributors.first.id }
end
+ factory :stripe_sca_payment_method, class: Spree::Gateway::StripeSCA do
+ name 'StripeSCA'
+ environment 'test'
+ distributors { [FactoryBot.create(:stripe_account).enterprise] }
+ preferred_enterprise_id { distributors.first.id }
+ end
+
factory :stripe_account do
- enterprise { FactoryBot.create :distributor_enterprise }
+ enterprise { FactoryBot.create(:distributor_enterprise) }
stripe_user_id "abc123"
stripe_publishable_key "xyz456"
end
diff --git a/spec/features/admin/payments_spec.rb b/spec/features/admin/payments_spec.rb
index 006fd09b93..2ea666f121 100644
--- a/spec/features/admin/payments_spec.rb
+++ b/spec/features/admin/payments_spec.rb
@@ -10,7 +10,6 @@ feature '
scenario "visiting the payment form" do
quick_login_as_admin
-
visit spree.new_admin_order_payment_path order
expect(page).to have_content "New Payment"
@@ -28,10 +27,38 @@ feature '
scenario "visiting the payment form" do
quick_login_as_admin
-
visit spree.new_admin_order_payment_path order
expect(page).to have_content "New Payment"
end
end
+
+ context "with a StripeSCA payment method" do
+ before do
+ stripe_payment_method = create(:stripe_sca_payment_method, distributors: [order.distributor])
+ order.payments << create(:payment, payment_method: stripe_payment_method, order: order)
+ end
+
+ it "renders the payment details" do
+ quick_login_as_admin
+ visit spree.admin_order_payments_path order
+
+ page.click_link("StripeSCA")
+ expect(page).to have_content order.payments.last.source.last_digits
+ end
+
+ context "with a deleted credit card" do
+ before do
+ order.payments.last.update_attribute(:source, nil)
+ end
+
+ it "renders the payment details" do
+ quick_login_as_admin
+ visit spree.admin_order_payments_path order
+
+ page.click_link("StripeSCA")
+ expect(page).to have_content order.payments.last.amount
+ end
+ end
+ end
end
diff --git a/spec/jobs/subscription_placement_job_spec.rb b/spec/jobs/subscription_placement_job_spec.rb
index c4f6349d0a..e43343e795 100644
--- a/spec/jobs/subscription_placement_job_spec.rb
+++ b/spec/jobs/subscription_placement_job_spec.rb
@@ -45,7 +45,7 @@ describe SubscriptionPlacementJob do
before do
allow(job).to receive(:proxy_orders) { ProxyOrder.where(id: proxy_order.id) }
- allow(job).to receive(:process)
+ allow(job).to receive(:place_order)
end
it "marks placeable proxy_orders as processed by setting placed_at" do
@@ -55,7 +55,7 @@ describe SubscriptionPlacementJob do
it "processes placeable proxy_orders" do
job.perform
- expect(job).to have_received(:process).with(proxy_order.reload.order)
+ expect(job).to have_received(:place_order).with(proxy_order.reload.order)
end
end
end
@@ -143,7 +143,7 @@ describe SubscriptionPlacementJob do
it "records an issue and ignores it" do
ActionMailer::Base.deliveries.clear
expect(job).to receive(:record_issue).with(:complete, order).once
- expect{ job.send(:process, order) }.to_not change{ order.reload.state }
+ expect{ job.send(:place_order, order) }.to_not change{ order.reload.state }
expect(order.payments.first.state).to eq "checkout"
expect(ActionMailer::Base.deliveries.count).to be 0
end
@@ -156,7 +156,7 @@ describe SubscriptionPlacementJob do
end
it "uses the same shipping method after advancing the order" do
- job.send(:process, order)
+ job.send(:place_order, order)
expect(order.state).to eq "complete"
order.reload
expect(order.shipping_method).to eq(shipping_method)
@@ -169,7 +169,7 @@ describe SubscriptionPlacementJob do
end
it "does not place the order, clears, all adjustments, and sends an empty_order email" do
- expect{ job.send(:process, order) }.to_not change{ order.reload.completed_at }.from(nil)
+ expect{ job.send(:place_order, order) }.to_not change{ order.reload.completed_at }.from(nil)
expect(order.adjustments).to be_empty
expect(order.total).to eq 0
expect(order.adjustment_total).to eq 0
@@ -182,13 +182,13 @@ describe SubscriptionPlacementJob do
it "processes the order to completion, but does not process the payment" do
# If this spec starts complaining about no shipping methods being available
# on CI, there is probably another spec resetting the currency though Rails.cache.clear
- expect{ job.send(:process, order) }.to change{ order.reload.completed_at }.from(nil)
+ expect{ job.send(:place_order, order) }.to change{ order.reload.completed_at }.from(nil)
expect(order.completed_at).to be_within(5.seconds).of Time.zone.now
expect(order.payments.first.state).to eq "checkout"
end
it "does not enqueue confirmation emails" do
- expect{ job.send(:process, order) }.to_not enqueue_job ConfirmOrderJob
+ expect{ job.send(:place_order, order) }.to_not enqueue_job ConfirmOrderJob
expect(job).to have_received(:send_placement_email).with(order, anything).once
end
@@ -198,7 +198,7 @@ describe SubscriptionPlacementJob do
it "records an error and does not attempt to send an email" do
expect(job).to_not receive(:send_placement_email)
expect(job).to receive(:record_and_log_error).once
- job.send(:process, order)
+ job.send(:place_order, order)
end
end
end
diff --git a/spec/lib/open_food_network/address_finder_spec.rb b/spec/lib/open_food_network/address_finder_spec.rb
index b2d3293866..e192aa1dab 100644
--- a/spec/lib/open_food_network/address_finder_spec.rb
+++ b/spec/lib/open_food_network/address_finder_spec.rb
@@ -137,7 +137,6 @@ module OpenFoodNetwork
describe "last_used_ship_address" do
let(:address) { create(:address) }
let(:distributor) { create(:distributor_enterprise) }
- let(:order) { create(:shipped_order, user: nil, email: email, distributor: distributor, shipments: [], ship_address: nil) }
let(:finder) { AddressFinder.new(email) }
context "when searching by email is not allowed" do
@@ -146,8 +145,9 @@ module OpenFoodNetwork
end
context "and an order with a required ship address exists" do
+ let(:order) { create(:shipped_order, user: nil, email: email, distributor: distributor, shipments: [], ship_address: address) }
+
before do
- order.update_attribute(:ship_address, address)
order.shipping_method.update_attribute(:require_ship_address, true)
end
@@ -163,7 +163,7 @@ module OpenFoodNetwork
end
context "and an order with a ship address exists" do
- before { order.update_attribute(:ship_address, address) }
+ let(:order) { create(:shipped_order, user: nil, email: email, distributor: distributor, shipments: [], ship_address: address) }
context "and the shipping method requires an address" do
before { order.shipping_method.update_attribute(:require_ship_address, true) }
@@ -183,7 +183,7 @@ module OpenFoodNetwork
end
context "and an order without a ship address exists" do
- before { order }
+ let!(:order) { create(:shipped_order, user: nil, email: email, distributor: distributor, shipments: [], ship_address: nil) }
it "return nil" do
expect(finder.send(:last_used_ship_address)).to eq nil
diff --git a/spec/lib/open_food_network/subscription_summarizer_spec.rb b/spec/lib/open_food_network/subscription_summarizer_spec.rb
index 2ecdfac1ea..a0d2b3f7bf 100644
--- a/spec/lib/open_food_network/subscription_summarizer_spec.rb
+++ b/spec/lib/open_food_network/subscription_summarizer_spec.rb
@@ -1,3 +1,4 @@
+require 'spec_helper'
require 'open_food_network/subscription_summarizer'
module OpenFoodNetwork
@@ -5,6 +6,8 @@ module OpenFoodNetwork
let(:order) { create(:order) }
let(:summarizer) { OpenFoodNetwork::SubscriptionSummarizer.new }
+ before { allow(Rails.logger).to receive(:info) }
+
describe "#summary_for" do
let(:order) { double(:order, distributor_id: 123) }
@@ -53,6 +56,7 @@ module OpenFoodNetwork
describe "#record_issue" do
it "requests a summary for the order and calls #record_issue on it" do
+ expect(order).to receive(:id)
expect(summary).to receive(:record_issue).with(:type, order, "message").once
summarizer.record_issue(:type, order, "message")
end
@@ -69,7 +73,6 @@ module OpenFoodNetwork
end
it "sends error info to the rails logger and calls #record_issue on itself with an error message" do
- expect(Rails.logger).to receive(:info)
expect(summarizer).to receive(:record_issue).with(:processing, order, "Errors: Some error")
summarizer.record_and_log_error(:processing, order)
end
@@ -81,7 +84,6 @@ module OpenFoodNetwork
end
it "falls back to calling record_issue" do
- expect(Rails.logger).to_not receive(:info)
expect(summarizer).to receive(:record_issue).with(:processing, order)
summarizer.record_and_log_error(:processing, order)
end
diff --git a/spec/lib/stripe/credit_card_cloner_spec.rb b/spec/lib/stripe/credit_card_cloner_spec.rb
new file mode 100644
index 0000000000..40854560d6
--- /dev/null
+++ b/spec/lib/stripe/credit_card_cloner_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'stripe/credit_card_cloner'
+
+module Stripe
+ describe CreditCardCloner do
+ describe "#clone" do
+ let(:cloner) { Stripe::CreditCardCloner.new }
+
+ let(:customer_id) { "cus_A123" }
+ let(:payment_method_id) { "pm_1234" }
+ let(:new_customer_id) { "cus_A456" }
+ let(:new_payment_method_id) { "pm_456" }
+ let(:stripe_account_id) { "acct_456" }
+ let(:customer_response_mock) { { status: 200, body: customer_response_body } }
+ let(:payment_method_response_mock) { { status: 200, body: payment_method_response_body } }
+
+ let(:credit_card) { create(:credit_card, user: create(:user)) }
+
+ let(:payment_method_response_body) {
+ JSON.generate(id: new_payment_method_id)
+ }
+ let(:customer_response_body) {
+ JSON.generate(id: new_customer_id)
+ }
+
+ before do
+ allow(Stripe).to receive(:api_key) { "sk_test_12345" }
+
+ stub_request(:post, "https://api.stripe.com/v1/customers")
+ .with(body: { email: credit_card.user.email },
+ headers: { 'Stripe-Account' => stripe_account_id })
+ .to_return(customer_response_mock)
+
+ stub_request(:post,
+ "https://api.stripe.com/v1/payment_methods/#{new_payment_method_id}/attach")
+ .with(body: { customer: new_customer_id },
+ headers: { 'Stripe-Account' => stripe_account_id })
+ .to_return(payment_method_response_mock)
+
+ credit_card.update_attribute :gateway_payment_profile_id, payment_method_id
+ end
+
+ context "when called with a card without a customer (one time usage card)" do
+ before do
+ stub_request(:post, "https://api.stripe.com/v1/payment_methods")
+ .with(body: { payment_method: payment_method_id },
+ headers: { 'Stripe-Account' => stripe_account_id })
+ .to_return(payment_method_response_mock)
+ end
+
+ it "clones the payment method only" do
+ customer_id, payment_method_id = cloner.clone(credit_card, stripe_account_id)
+
+ expect(payment_method_id).to eq new_payment_method_id
+ expect(customer_id).to eq nil
+ end
+ end
+
+ context "when called with a valid customer and payment_method" do
+ before do
+ stub_request(:post, "https://api.stripe.com/v1/payment_methods")
+ .with(body: { customer: customer_id, payment_method: payment_method_id },
+ headers: { 'Stripe-Account' => stripe_account_id })
+ .to_return(payment_method_response_mock)
+
+ credit_card.update_attribute :gateway_customer_profile_id, customer_id
+ end
+
+ it "clones both the payment method and the customer" do
+ customer_id, payment_method_id = cloner.clone(credit_card, stripe_account_id)
+
+ expect(payment_method_id).to eq new_payment_method_id
+ expect(customer_id).to eq new_customer_id
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/stripe/profile_storer_spec.rb b/spec/lib/stripe/profile_storer_spec.rb
new file mode 100644
index 0000000000..a5c7cfc7d2
--- /dev/null
+++ b/spec/lib/stripe/profile_storer_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+module Stripe
+ describe ProfileStorer do
+ describe "create_customer_from_token" do
+ let(:payment) { create(:payment) }
+ let(:stripe_payment_method) { create(:stripe_payment_method) }
+ let(:profile_storer) { Stripe::ProfileStorer.new(payment, stripe_payment_method.provider) }
+
+ let(:customer_id) { "cus_A123" }
+ let(:card_id) { "card_2342" }
+ let(:customer_response_mock) { { status: 200, body: customer_response_body } }
+
+ before do
+ allow(Stripe).to receive(:api_key) { "sk_test_12345" }
+
+ stub_request(:post, "https://api.stripe.com/v1/customers")
+ .with(basic_auth: ["sk_test_12345", ""], body: { email: payment.order.email })
+ .to_return(customer_response_mock)
+ end
+
+ context "when called from Stripe Connect" do
+ let(:customer_response_body) {
+ JSON.generate(id: customer_id, default_card: card_id, sources: { data: [{ id: "1" }] })
+ }
+
+ it "fetches the customer id and the card id from the correct response fields" do
+ profile_storer.create_customer_from_token
+
+ expect(payment.source.gateway_customer_profile_id).to eq customer_id
+ expect(payment.source.gateway_payment_profile_id).to eq card_id
+ end
+ end
+
+ context "when called from Stripe SCA" do
+ let(:customer_response_body) {
+ JSON.generate(customer: customer_id, id: card_id, sources: { data: [{ id: "1" }] })
+ }
+
+ it "fetches the customer id and the card id from the correct response fields" do
+ profile_storer.create_customer_from_token
+
+ expect(payment.source.gateway_customer_profile_id).to eq customer_id
+ expect(payment.source.gateway_payment_profile_id).to eq card_id
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/enterprise_caching_spec.rb b/spec/models/enterprise_caching_spec.rb
index 047fc6ab02..2fe4bd3f7a 100644
--- a/spec/models/enterprise_caching_spec.rb
+++ b/spec/models/enterprise_caching_spec.rb
@@ -33,7 +33,7 @@ describe Enterprise do
it "touches enterprise when the supplier of a product changes" do
expect {
product.update_attributes!(supplier: supplier2)
- }.to change { enterprise.updated_at }
+ }.to change { enterprise.reload.updated_at }
end
end
diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb
index 35efd68488..865deac767 100644
--- a/spec/models/product_importer_spec.rb
+++ b/spec/models/product_importer_spec.rb
@@ -714,66 +714,6 @@ describe ProductImport::ProductImporter do
expect(cabbage.count_on_hand).to eq 0 # In enterprise, not in file (reset)
expect(lettuce.count_on_hand).to eq 96 # In different enterprise; unchanged
end
-
- it "can overwrite fields with selected defaults when importing to product list" do
- csv_data = CSV.generate do |csv|
- csv << ["name", "producer", "category", "on_hand", "price", "units", "unit_type", "tax_category_id", "available_on", "shipping_category"]
- csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g", tax_category.id, "", shipping_category.name]
- csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1", "kg", "", "", shipping_category.name]
- end
- settings = { enterprise.id.to_s => {
- 'import_into' => 'product_list',
- 'defaults' => {
- 'on_hand' => {
- 'active' => true,
- 'mode' => 'overwrite_all',
- 'value' => '9000'
- },
- 'tax_category_id' => {
- 'active' => true,
- 'mode' => 'overwrite_empty',
- 'value' => tax_category2.id
- },
- 'shipping_category_id' => {
- 'active' => true,
- 'mode' => 'overwrite_all',
- 'value' => shipping_category.id
- },
- 'available_on' => {
- 'active' => true,
- 'mode' => 'overwrite_all',
- 'value' => '2020-01-01'
- }
- }
- } }
-
- importer = import_data csv_data, settings: settings
-
- importer.validate_entries
- entries = JSON.parse(importer.entries_json)
-
- expect(filter('valid', entries)).to eq 2
- expect(filter('invalid', entries)).to eq 0
- expect(filter('create_product', entries)).to eq 2
-
- importer.save_entries
-
- expect(importer.products_created_count).to eq 2
- expect(importer.updated_ids).to be_a(Array)
- expect(importer.updated_ids.count).to eq 2
-
- carrots = Spree::Product.find_by(name: 'Carrots')
- expect(carrots.on_hand).to eq 9000
- expect(carrots.tax_category_id).to eq tax_category.id
- expect(carrots.shipping_category_id).to eq shipping_category.id
- expect(carrots.available_on).to be_within(1.day).of(Time.zone.local(2020, 1, 1))
-
- potatoes = Spree::Product.find_by(name: 'Potatoes')
- expect(potatoes.on_hand).to eq 9000
- expect(potatoes.tax_category_id).to eq tax_category2.id
- expect(potatoes.shipping_category_id).to eq shipping_category.id
- expect(potatoes.available_on).to be_within(1.day).of(Time.zone.local(2020, 1, 1))
- end
end
end
diff --git a/spec/models/spree/tax_rate_spec.rb b/spec/models/spree/tax_rate_spec.rb
index a51b0fa34f..e2f7e7eb65 100644
--- a/spec/models/spree/tax_rate_spec.rb
+++ b/spec/models/spree/tax_rate_spec.rb
@@ -1,3 +1,5 @@
+require 'spec_helper'
+
module Spree
describe TaxRate do
describe "selecting tax rates to apply to an order" do
diff --git a/spec/models/stock/package_spec.rb b/spec/models/stock/package_spec.rb
index 726eed6c37..6a3ac355cc 100644
--- a/spec/models/stock/package_spec.rb
+++ b/spec/models/stock/package_spec.rb
@@ -6,8 +6,8 @@ module Stock
subject(:package) { Package.new(stock_location, order, contents) }
- let(:enterprise) { build(:enterprise) }
- let(:other_enterprise) { build(:enterprise) }
+ let(:enterprise) { create(:enterprise) }
+ let(:other_enterprise) { create(:enterprise) }
let(:order) { build(:order, distributor: enterprise) }
diff --git a/spec/requests/checkout/stripe_connect_spec.rb b/spec/requests/checkout/stripe_connect_spec.rb
index a64bb44ab8..697664b806 100644
--- a/spec/requests/checkout/stripe_connect_spec.rb
+++ b/spec/requests/checkout/stripe_connect_spec.rb
@@ -7,16 +7,6 @@ describe "checking out an order with a Stripe Connect payment method", type: :re
let!(:order_cycle) { create(:simple_order_cycle) }
let!(:enterprise) { create(:distributor_enterprise) }
- let!(:exchange) do
- create(
- :exchange,
- order_cycle: order_cycle,
- sender: order_cycle.coordinator,
- receiver: enterprise,
- incoming: false,
- pickup_time: "Monday"
- )
- end
let!(:shipping_method) do
create(
:shipping_method,
diff --git a/spec/requests/checkout/stripe_sca_spec.rb b/spec/requests/checkout/stripe_sca_spec.rb
new file mode 100644
index 0000000000..9c8f3d1b96
--- /dev/null
+++ b/spec/requests/checkout/stripe_sca_spec.rb
@@ -0,0 +1,311 @@
+require 'spec_helper'
+
+describe "checking out an order with a Stripe SCA payment method", type: :request do
+ include ShopWorkflow
+ include AuthenticationWorkflow
+ include OpenFoodNetwork::ApiHelper
+
+ let!(:order_cycle) { create(:simple_order_cycle) }
+ let!(:enterprise) { create(:distributor_enterprise) }
+ let!(:shipping_method) do
+ create(
+ :shipping_method,
+ calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0),
+ distributors: [enterprise]
+ )
+ end
+ let!(:payment_method) { create(:stripe_sca_payment_method, distributors: [enterprise]) }
+ let!(:stripe_account) { create(:stripe_account, enterprise: enterprise) }
+ let!(:line_item) { create(:line_item, price: 12.34) }
+ let!(:order) { line_item.order }
+ let(:address) { create(:address) }
+ let(:stripe_payment_method) { "pm_123" }
+ let(:customer_id) { "cus_A123" }
+ let(:hubs_stripe_payment_method) { "pm_456" }
+ let(:payments_attributes) do
+ {
+ payment_method_id: payment_method.id,
+ source_attributes: {
+ gateway_payment_profile_id: stripe_payment_method,
+ cc_type: "visa",
+ last_digits: "4242",
+ month: 10,
+ year: 2025,
+ first_name: 'Jill',
+ last_name: 'Jeffreys'
+ }
+ }
+ end
+ let(:allowed_address_attributes) do
+ [
+ "firstname",
+ "lastname",
+ "address1",
+ "address2",
+ "phone",
+ "city",
+ "zipcode",
+ "state_id",
+ "country_id"
+ ]
+ end
+ let(:params) do
+ {
+ format: :json, order: {
+ shipping_method_id: shipping_method.id,
+ payments_attributes: [payments_attributes],
+ bill_address_attributes: address.attributes.slice(*allowed_address_attributes),
+ ship_address_attributes: address.attributes.slice(*allowed_address_attributes)
+ }
+ }
+ end
+ let(:payment_intent_response_mock) do
+ { status: 200, body: JSON.generate(object: "payment_intent", amount: 2000, charges: { data: [{ id: "ch_1234", amount: 2000 }]}) }
+ end
+
+ before do
+ order_cycle_distributed_variants = double(:order_cycle_distributed_variants)
+ allow(OrderCycleDistributedVariants).to receive(:new) { order_cycle_distributed_variants }
+ allow(order_cycle_distributed_variants).to receive(:distributes_order_variants?) { true }
+
+ allow(Stripe).to receive(:api_key) { "sk_test_12345" }
+ order.update_attributes(distributor_id: enterprise.id, order_cycle_id: order_cycle.id)
+ order.reload.update_totals
+ set_order order
+ end
+
+ context "when the user submits a new card and doesn't request that the card is saved for later" do
+ let(:hubs_payment_method_response_mock) do
+ { status: 200, body: JSON.generate(id: hubs_stripe_payment_method) }
+ end
+
+ before do
+ # Clones the payment method to the hub's stripe account
+ stub_request(:post, "https://api.stripe.com/v1/payment_methods")
+ .with(body: { payment_method: stripe_payment_method },
+ headers: { 'Stripe-Account' => 'abc123' })
+ .to_return(hubs_payment_method_response_mock)
+
+ # Charges the card
+ stub_request(:post, "https://api.stripe.com/v1/payment_intents")
+ .with(basic_auth: ["sk_test_12345", ""], body: /#{hubs_stripe_payment_method}.*#{order.number}/)
+ .to_return(payment_intent_response_mock)
+ end
+
+ context "and the paymeent intent request is successful" do
+ it "should process the payment without storing card details" do
+ put update_checkout_path, params
+
+ expect(json_response["path"]).to eq spree.order_path(order)
+ expect(order.payments.completed.count).to be 1
+
+ card = order.payments.completed.first.source
+
+ expect(card.gateway_customer_profile_id).to eq nil
+ expect(card.gateway_payment_profile_id).to eq stripe_payment_method
+ expect(card.cc_type).to eq "visa"
+ expect(card.last_digits).to eq "4242"
+ expect(card.first_name).to eq "Jill"
+ expect(card.last_name).to eq "Jeffreys"
+ end
+ end
+
+ context "when the payment intent request returns an error message" do
+ let(:payment_intent_response_mock) do
+ { status: 402, body: JSON.generate(error: { message: "payment-intent-failure" }) }
+ end
+
+ it "should not process the payment" do
+ put update_checkout_path, params
+
+ expect(response.status).to be 400
+
+ expect(json_response["flash"]["error"]).to eq "payment-intent-failure"
+ expect(order.payments.completed.count).to be 0
+ end
+ end
+ end
+
+ context "when saving a card or using a stored card is involved" do
+ let(:hubs_payment_method_response_mock) do
+ {
+ status: 200,
+ body: JSON.generate(id: hubs_stripe_payment_method, customer: customer_id)
+ }
+ end
+ let(:customer_response_mock) do
+ {
+ status: 200,
+ body: JSON.generate(id: customer_id, sources: { data: [{ id: "1" }] })
+ }
+ end
+
+ before do
+ # Clones the payment method to the hub's stripe account
+ stub_request(:post, "https://api.stripe.com/v1/payment_methods")
+ .with(body: { customer: customer_id, payment_method: stripe_payment_method },
+ headers: { 'Stripe-Account' => 'abc123' })
+ .to_return(hubs_payment_method_response_mock)
+
+ # Creates a customer (this stubs the customers call to the main stripe account and also the call to the connected account)
+ stub_request(:post, "https://api.stripe.com/v1/customers")
+ .with(body: { email: order.email })
+ .to_return(customer_response_mock)
+
+ # Attaches the payment method to the customer in the hub's stripe account
+ stub_request(:post, "https://api.stripe.com/v1/payment_methods/#{hubs_stripe_payment_method}/attach")
+ .with(body: { customer: customer_id },
+ headers: { 'Stripe-Account' => 'abc123' })
+ .to_return(hubs_payment_method_response_mock)
+ end
+
+ context "when the user submits a new card and requests that the card is saved for later" do
+ let(:payment_method_attach_response_mock) do
+ {
+ status: 200,
+ body: JSON.generate(id: stripe_payment_method, customer: customer_id)
+ }
+ end
+
+ before do
+ source_attributes = params[:order][:payments_attributes][0][:source_attributes]
+ source_attributes[:save_requested_by_customer] = '1'
+
+ # Attaches the payment method to the customer
+ stub_request(:post, "https://api.stripe.com/v1/payment_methods/#{stripe_payment_method}/attach")
+ .with(body: { customer: customer_id })
+ .to_return(payment_method_attach_response_mock)
+
+ # Charges the card
+ stub_request(:post, "https://api.stripe.com/v1/payment_intents")
+ .with(
+ basic_auth: ["sk_test_12345", ""],
+ body: /.*#{order.number}/
+ ).to_return(payment_intent_response_mock)
+ end
+
+ context "and the customer, payment_method and payment_intent requests are successful" do
+ it "should process the payment, and store the card/customer details" do
+ put update_checkout_path, params
+
+ expect(json_response["path"]).to eq spree.order_path(order)
+ expect(order.payments.completed.count).to be 1
+
+ card = order.payments.completed.first.source
+
+ expect(card.gateway_customer_profile_id).to eq customer_id
+ expect(card.gateway_payment_profile_id).to eq stripe_payment_method
+ expect(card.cc_type).to eq "visa"
+ expect(card.last_digits).to eq "4242"
+ expect(card.first_name).to eq "Jill"
+ expect(card.last_name).to eq "Jeffreys"
+ end
+ end
+
+ context "when the customer request returns an error message" do
+ let(:customer_response_mock) do
+ { status: 402, body: JSON.generate(error: { message: "customer-store-failure" }) }
+ end
+
+ it "should not process the payment" do
+ put update_checkout_path, params
+
+ expect(response.status).to be 400
+
+ expect(json_response["flash"]["error"])
+ .to eq(I18n.t(:spree_gateway_error_flash_for_checkout, error: 'customer-store-failure'))
+ expect(order.payments.completed.count).to be 0
+ end
+ end
+
+ context "when the payment intent request returns an error message" do
+ let(:payment_intent_response_mock) do
+ { status: 402, body: JSON.generate(error: { message: "payment-intent-failure" }) }
+ end
+
+ it "should not process the payment" do
+ put update_checkout_path, params
+
+ expect(response.status).to be 400
+
+ expect(json_response["flash"]["error"]).to eq "payment-intent-failure"
+ expect(order.payments.completed.count).to be 0
+ end
+ end
+
+ context "when the payment_method request returns an error message" do
+ let(:hubs_payment_method_response_mock) do
+ { status: 402, body: JSON.generate(error: { message: "payment-method-failure" }) }
+ end
+
+ it "should not process the payment" do
+ put update_checkout_path, params
+
+ expect(response.status).to be 400
+
+ expect(json_response["flash"]["error"]).to include "payment-method-failure"
+ expect(order.payments.completed.count).to be 0
+ end
+ end
+ end
+
+ context "when the user selects an existing card" do
+ let(:credit_card) do
+ create(
+ :credit_card,
+ user_id: order.user_id,
+ gateway_payment_profile_id: stripe_payment_method,
+ gateway_customer_profile_id: customer_id,
+ last_digits: "4321",
+ cc_type: "master",
+ first_name: "Sammy",
+ last_name: "Signpost",
+ month: 11, year: 2026
+ )
+ end
+
+ before do
+ params[:order][:existing_card_id] = credit_card.id
+ quick_login_as(order.user)
+
+ # Charges the card
+ stub_request(:post, "https://api.stripe.com/v1/payment_intents")
+ .with(basic_auth: ["sk_test_12345", ""], body: %r{#{customer_id}.*#{hubs_stripe_payment_method}})
+ .to_return(payment_intent_response_mock)
+ end
+
+ context "and the payment intent and payment method requests are accepted" do
+ it "should process the payment, and keep the profile ids and other card details" do
+ put update_checkout_path, params
+
+ expect(json_response["path"]).to eq spree.order_path(order)
+ expect(order.payments.completed.count).to be 1
+
+ card = order.payments.completed.first.source
+
+ expect(card.gateway_customer_profile_id).to eq customer_id
+ expect(card.gateway_payment_profile_id).to eq stripe_payment_method
+ expect(card.cc_type).to eq "master"
+ expect(card.last_digits).to eq "4321"
+ expect(card.first_name).to eq "Sammy"
+ expect(card.last_name).to eq "Signpost"
+ end
+ end
+
+ context "when the payment intent request returns an error message" do
+ let(:payment_intent_response_mock) do
+ { status: 402, body: JSON.generate(error: { message: "payment-intent-failure" }) }
+ end
+
+ it "should not process the payment" do
+ put update_checkout_path, params
+
+ expect(response.status).to be 400
+
+ expect(json_response["flash"]["error"]).to eq "payment-intent-failure"
+ expect(order.payments.completed.count).to be 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/cart_service_spec.rb b/spec/services/cart_service_spec.rb
index 8a01a8cdb0..1f6b03949d 100644
--- a/spec/services/cart_service_spec.rb
+++ b/spec/services/cart_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe CartService do
let(:order) { double(:order, id: 123) }
- let(:currency) { double(:currency) }
+ let(:currency) { "EUR" }
let(:params) { {} }
let(:distributor) { double(:distributor) }
let(:order_cycle) { double(:order_cycle) }
diff --git a/spec/views/spree/shared/_order_details.html.haml_spec.rb b/spec/views/spree/shared/_order_details.html.haml_spec.rb
new file mode 100644
index 0000000000..3bd04caf7c
--- /dev/null
+++ b/spec/views/spree/shared/_order_details.html.haml_spec.rb
@@ -0,0 +1,54 @@
+require "spec_helper"
+
+describe "spree/shared/_order_details.html.haml" do
+ include AuthenticationWorkflow
+ helper Spree::BaseHelper
+
+ let(:order) { create(:completed_order_with_fees) }
+
+ before do
+ assign(:order, order)
+ allow(view).to receive_messages(
+ order: order,
+ current_order: order,
+ )
+ end
+
+ it "shows how the order is paid for" do
+ order.payments.first.payment_method.name = "Bartering"
+
+ render
+
+ expect(rendered).to have_content("Paying via: Bartering")
+ end
+
+ it "displays payment methods safely" do
+ order.payments.first.payment_method.name = "Barter→ing"
+
+ render
+
+ expect(rendered).to have_content("Paying via: Barter→ing")
+ end
+
+ it "shows the last used payment method" do
+ first_payment = order.payments.first
+ second_payment = create(
+ :payment,
+ order: order,
+ payment_method: create(:payment_method, name: "Cash")
+ )
+ third_payment = create(
+ :payment,
+ order: order,
+ payment_method: create(:payment_method, name: "Credit")
+ )
+ first_payment.update_column(:created_at, 3.days.ago)
+ second_payment.update_column(:created_at, 2.days.ago)
+ third_payment.update_column(:created_at, 1.day.ago)
+ order.payments.reload
+
+ render
+
+ expect(rendered).to have_content("Paying via: Credit")
+ end
+end