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