diff --git a/.rubocop.yml b/.rubocop.yml index 04839c512d..2e664e7c2b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -143,6 +143,10 @@ Style/WordArray: Enabled: false StyleGuide: http://relaxed.ruby.style/#stylewordarray +Style/SymbolArray: + Enabled: false + StyleGuide: https://rubocop.readthedocs.io/en/latest/cops_style/#stylesymbolarray + Lint/AmbiguousRegexpLiteral: Enabled: false StyleGuide: http://relaxed.ruby.style/#lintambiguousregexpliteral diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ee8e61e8a2..136580b472 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --exclude-limit 100` -# on 2017-07-12 10:36:44 +0200 using RuboCop version 0.49.1. +# on 2017-08-25 14:27:48 +1000 using RuboCop version 0.49.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -14,7 +14,7 @@ Bundler/OrderedGems: Exclude: - 'Gemfile' -# Offense count: 77 +# Offense count: 128 # Cop supports --auto-correct. Layout/AlignArray: Exclude: @@ -59,7 +59,7 @@ Layout/AlignHash: - 'spec/models/spree/shipping_method_spec.rb' - 'spec/models/spree/variant_spec.rb' -# Offense count: 42 +# Offense count: 43 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: with_first_parameter, with_fixed_indentation @@ -113,11 +113,12 @@ Layout/CaseIndentation: - 'app/models/enterprise.rb' - 'app/models/product_importer.rb' -# Offense count: 2 +# Offense count: 3 # Cop supports --auto-correct. Layout/ClosingParenthesisIndentation: Exclude: - 'app/overrides/add_capture_order_shortcut.rb' + - 'spec/serializers/variant_serializer_spec.rb' # Offense count: 8 # Cop supports --auto-correct. @@ -137,7 +138,7 @@ Layout/ElseAlignment: - 'app/serializers/api/admin/order_cycle_serializer.rb' - 'lib/open_food_network/sales_tax_report.rb' -# Offense count: 223 +# Offense count: 215 # Cop supports --auto-correct. Layout/EmptyLines: Enabled: false @@ -152,7 +153,7 @@ Layout/EmptyLinesAroundAccessModifier: - 'spec/helpers/products_helper_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 73 +# Offense count: 70 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: empty_lines, no_empty_lines @@ -170,7 +171,6 @@ Layout/EmptyLinesAroundBlockBody: - 'app/models/spree/calculator_decorator.rb' - 'app/models/spree/money_decorator.rb' - 'app/models/spree/option_value_decorator.rb' - - 'app/models/spree/payment_decorator.rb' - 'lib/open_food_network/group_buy_report.rb' - 'lib/spree/money_decorator.rb' - 'lib/tasks/dev.rake' @@ -188,10 +188,10 @@ Layout/EmptyLinesAroundBlockBody: - 'spec/features/admin/orders_spec.rb' - 'spec/features/admin/reports_spec.rb' - 'spec/features/admin/variant_overrides_spec.rb' + - 'spec/features/consumer/shopping/embedded_shopfronts_spec.rb' - 'spec/features/consumer/shopping/shopping_spec.rb' - 'spec/features/consumer/shopping/variant_overrides_spec.rb' - 'spec/helpers/admin/business_model_configuration_helper_spec.rb' - - 'spec/helpers/injection_helper_spec.rb' - 'spec/helpers/shared_helper_spec.rb' - 'spec/helpers/shop_helper_spec.rb' - 'spec/helpers/spree/orders_helper_spec.rb' @@ -212,8 +212,7 @@ Layout/EmptyLinesAroundBlockBody: - 'spec/models/tag_rule/filter_shipping_methods_spec.rb' - 'spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb' - 'spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb' - - 'spec/serializers/order_serializer_spec.rb' - - 'spec/serializers/orders_by_distributor_serializer_spec.rb' + - 'spec/serializers/variant_serializer_spec.rb' - 'spec/support/matchers/delegate_matchers.rb' - 'spec/support/matchers/select2_matchers.rb' - 'spec/support/matchers/table_matchers.rb' @@ -270,7 +269,7 @@ Layout/EmptyLinesAroundMethodBody: - 'app/serializers/api/product_serializer.rb' - 'lib/open_food_network/orders_and_fulfillments_report.rb' -# Offense count: 13 +# Offense count: 12 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines @@ -278,7 +277,6 @@ Layout/EmptyLinesAroundModuleBody: Exclude: - 'app/helpers/add_to_cart_helper.rb' - 'app/helpers/groups_helper.rb' - - 'app/helpers/injection_helper.rb' - 'app/helpers/spree/admin/base_helper_decorator.rb' - 'lib/open_food_network/column_preference_defaults.rb' - 'lib/open_food_network/group_buy_report.rb' @@ -289,7 +287,7 @@ Layout/EmptyLinesAroundModuleBody: - 'spec/support/request/distribution_helper.rb' - 'spec/support/request/ui_component_helper.rb' -# Offense count: 54 +# Offense count: 55 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. Layout/ExtraSpacing: @@ -326,29 +324,28 @@ Layout/ExtraSpacing: - 'spec/models/enterprise_spec.rb' - 'spec/models/order_cycle_spec.rb' - 'spec/models/spree/adjustment_spec.rb' + - 'spec/models/spree/gateway/stripe_connect_spec.rb' - 'spec/models/spree/order_spec.rb' - 'spec/models/variant_override_spec.rb' - 'spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb' - 'spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 1 +# Offense count: 2 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: consistent, special_for_inner_method_call, special_for_inner_method_call_in_parentheses Layout/FirstParameterIndentation: Exclude: - 'lib/open_food_network/permissions.rb' + - 'spec/serializers/variant_serializer_spec.rb' -# Offense count: 6 +# Offense count: 4 # Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# Configuration parameters: SupportedStyles, IndentationWidth. # SupportedStyles: special_inside_parentheses, consistent, align_brackets Layout/IndentArray: - Exclude: - - 'lib/open_food_network/users_and_enterprises_report.rb' - - 'spec/features/admin/reports_spec.rb' - - 'spec/lib/open_food_network/products_and_inventory_report_spec.rb' + EnforcedStyle: consistent # Offense count: 2 # Cop supports --auto-correct. @@ -403,7 +400,7 @@ Layout/IndentationConsistency: - 'spec/models/spree/line_item_spec.rb' - 'spec/models/spree/product_spec.rb' -# Offense count: 20 +# Offense count: 18 # Cop supports --auto-correct. # Configuration parameters: Width, IgnoredPatterns. Layout/IndentationWidth: @@ -411,13 +408,11 @@ Layout/IndentationWidth: - 'app/controllers/admin/invoice_settings_controller.rb' - 'app/controllers/admin/order_cycles_controller.rb' - 'app/controllers/api/order_cycles_controller.rb' - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'app/models/spree/line_item_decorator.rb' - 'app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb' - 'app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb' - 'app/serializers/api/admin/order_cycle_serializer.rb' - 'lib/discourse/single_sign_on.rb' - - 'lib/open_food_network/last_used_address.rb' - 'spec/features/consumer/shopping/variant_overrides_spec.rb' - 'spec/helpers/admin/business_model_configuration_helper_spec.rb' - 'spec/helpers/groups_helper_spec.rb' @@ -455,7 +450,6 @@ Layout/LeadingCommentSpace: - 'spec/factories.rb' - 'spec/features/admin/products_spec.rb' - 'spec/features/admin/reports_spec.rb' - - 'spec/features/consumer/shops_spec.rb' - 'spec/jobs/finalize_account_invoices_spec.rb' - 'spec/lib/open_food_network/order_and_distributor_report_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' @@ -526,7 +520,15 @@ Layout/MultilineMethodCallBraceLayout: - 'lib/open_food_network/products_renderer.rb' - 'spec/lib/open_food_network/products_and_inventory_report_spec.rb' -# Offense count: 34 +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. +# SupportedStyles: aligned, indented, indented_relative_to_receiver +Layout/MultilineMethodCallIndentation: + Exclude: + - 'spec/serializers/variant_serializer_spec.rb' + +# Offense count: 33 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth. # SupportedStyles: aligned, indented @@ -539,7 +541,6 @@ Layout/MultilineOperationIndentation: - 'app/models/producer_property.rb' - 'app/models/product_importer.rb' - 'app/models/spree/ability_decorator.rb' - - 'app/models/spree/order_decorator.rb' - 'app/models/spree/product_set.rb' - 'app/models/variant_override_set.rb' - 'lib/open_food_network/accounts_and_billing_settings_validator.rb' @@ -559,20 +560,18 @@ Layout/SpaceAfterColon: - 'spec/models/variant_override_spec.rb' - 'spec/spec_helper.rb' -# Offense count: 58 +# Offense count: 53 # Cop supports --auto-correct. Layout/SpaceAfterComma: Exclude: - 'app/controllers/admin/order_cycles_controller.rb' - 'app/controllers/spree/admin/products_controller_decorator.rb' - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'app/controllers/spree/orders_controller_decorator.rb' - 'app/models/column_preference.rb' - 'app/models/product_importer.rb' - 'lib/discourse/single_sign_on.rb' - 'lib/open_food_network/accounts_and_billing_settings_validator.rb' - 'lib/open_food_network/business_model_configuration_validator.rb' - - 'lib/open_food_network/order_and_distributor_report.rb' - 'lib/open_food_network/order_cycle_form_applicator.rb' - 'lib/open_food_network/users_and_enterprises_report.rb' - 'spec/controllers/admin/enterprises_controller_spec.rb' @@ -649,7 +648,7 @@ Layout/SpaceAroundEqualsInParameterDefault: - 'spec/support/request/distribution_helper.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 58 +# Offense count: 60 # Cop supports --auto-correct. # Configuration parameters: AllowForAlignment. Layout/SpaceAroundOperators: @@ -678,6 +677,7 @@ Layout/SpaceAroundOperators: - 'spec/helpers/order_cycles_helper_spec.rb' - 'spec/jobs/update_billable_periods_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' + - 'spec/lib/stripe/account_connector_spec.rb' - 'spec/models/calculator/weight_spec.rb' - 'spec/support/cancan_helper.rb' - 'spec/support/seeds.rb' @@ -716,7 +716,7 @@ Layout/SpaceInLambdaLiteral: - 'app/models/spree/product_decorator.rb' - 'app/models/spree/variant_decorator.rb' -# Offense count: 194 +# Offense count: 187 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces, SpaceBeforeBlockParameters. # SupportedStyles: space, no_space @@ -766,11 +766,9 @@ Layout/SpaceInsideBlockBraces: - 'spec/models/spree/order_spec.rb' - 'spec/models/spree/payment_spec.rb' - 'spec/models/spree/product_spec.rb' - - 'spec/models/spree/user_spec.rb' - 'spec/models/tag_rule/discount_order_spec.rb' - 'spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb' - 'spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb' - - 'spec/serializers/orders_by_distributor_serializer_spec.rb' - 'spec/spec_helper.rb' - 'spec/support/cancan_helper.rb' @@ -797,7 +795,7 @@ Layout/SpaceInsideBrackets: - 'spec/lib/open_food_network/users_and_enterprises_report_spec.rb' - 'spec/performance/orders_controller_spec.rb' -# Offense count: 729 +# Offense count: 766 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SupportedStylesForEmptyBraces. # SupportedStyles: space, no_space, compact @@ -824,12 +822,11 @@ Layout/Tab: - 'spec/lib/spree/product_filters_spec.rb' - 'spec/models/spree/line_item_spec.rb' -# Offense count: 32 +# Offense count: 42 # Cop supports --auto-correct. Layout/TrailingWhitespace: Exclude: - 'app/models/distributor_shipping_method.rb' - - 'app/models/product_importer.rb' - 'app/models/spree/money_decorator.rb' - 'app/serializers/api/image_serializer.rb' - 'app/serializers/api/shipping_method_serializer.rb' @@ -837,7 +834,6 @@ Layout/TrailingWhitespace: - 'app/views/json/_enterprises.rabl' - 'app/views/json/_producer.rabl' - 'app/views/json/partials/_producer.rabl' - - 'lib/open_food_network/group_buy_report.rb' - 'lib/tasks/dev.rake' - 'spec/controllers/spree/store_controller_spec.rb' - 'spec/features/admin/enterprise_user_spec.rb' @@ -845,17 +841,16 @@ Layout/TrailingWhitespace: - 'spec/lib/open_food_network/option_value_namer_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' - 'spec/serializers/admin/enterprise_serializer_spec.rb' + - 'spec/serializers/variant_serializer_spec.rb' - 'spec/support/request/menu_helper.rb' - 'spec/views/json/producers.json.rabl_spec.rb' -# Offense count: 8 +# Offense count: 6 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyleAlignWith, SupportedStylesAlignWith. # SupportedStylesAlignWith: either, start_of_block, start_of_line Lint/BlockAlignment: Exclude: - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - - 'lib/open_food_network/last_used_address.rb' - 'spec/lib/open_food_network/enterprise_fee_calculator_spec.rb' - 'spec/models/enterprise_spec.rb' - 'spec/models/spree/calculator/flat_percent_item_total_spec.rb' @@ -1012,7 +1007,7 @@ Lint/UselessAccessModifier: - 'lib/open_food_network/reports/bulk_coop_report.rb' - 'spec/lib/open_food_network/reports/report_spec.rb' -# Offense count: 340 +# Offense count: 341 Lint/Void: Exclude: - 'app/serializers/api/enterprise_serializer.rb' @@ -1081,7 +1076,7 @@ Lint/Void: - 'spec/serializers/enterprise_serializer_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 706 +# Offense count: 747 # Configuration parameters: CountComments, ExcludedMethods. Metrics/BlockLength: Max: 711 @@ -1166,7 +1161,7 @@ Rails/Delegate: - 'app/serializers/api/admin/tag_rule_serializer.rb' - 'app/serializers/api/variant_serializer.rb' -# Offense count: 6 +# Offense count: 7 Rails/FilePath: Exclude: - 'lib/tasks/karma.rake' @@ -1204,7 +1199,7 @@ Rails/HasAndBelongsToMany: - 'app/models/spree/line_item_decorator.rb' - 'app/models/spree/payment_method_decorator.rb' -# Offense count: 11 +# Offense count: 10 Rails/OutputSafety: Exclude: - 'app/controllers/spree/admin/reports_controller_decorator.rb' @@ -1212,7 +1207,6 @@ Rails/OutputSafety: - 'app/helpers/spree/reports_helper.rb' - 'app/serializers/api/product_serializer.rb' - 'lib/spree/money_decorator.rb' - - 'lib/tasks/karma.rake' # Offense count: 6 # Cop supports --auto-correct. @@ -1303,15 +1297,16 @@ Rails/Validation: - 'app/models/spree/variant_decorator.rb' - 'app/models/variant_override.rb' -# Offense count: 6 +# Offense count: 7 Style/AccessorMethodName: Exclude: - 'app/models/product_importer.rb' - 'app/models/spree/adjustment_decorator.rb' - 'app/models/spree/order_decorator.rb' - 'spec/support/request/shop_workflow.rb' + - 'spec/support/request/web_helper.rb' -# Offense count: 35 +# Offense count: 34 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: always, conditionals @@ -1327,7 +1322,6 @@ Style/AndOr: - 'app/models/product_importer.rb' - 'app/models/spreadsheet_entry.rb' - 'app/models/spree/adjustment_decorator.rb' - - 'app/models/spree/order_decorator.rb' - 'app/models/spree/product_set.rb' - 'app/views/json/partials/_enterprise.rabl' - 'lib/spree/core/controller_helpers/respond_with_decorator.rb' @@ -1341,7 +1335,7 @@ Style/BarePercentLiterals: - 'spec/features/admin/variants_spec.rb' - 'spec/support/request/web_helper.rb' -# Offense count: 209 +# Offense count: 208 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: braces, no_braces, context_dependent @@ -1411,7 +1405,6 @@ Style/BracesAroundHashParameters: - 'spec/models/spree/product_spec.rb' - 'spec/models/spree/taxon_spec.rb' - 'spec/serializers/admin/customer_serializer_spec.rb' - - 'spec/serializers/orders_by_distributor_serializer_spec.rb' - 'spec/spec_helper.rb' - 'spec/support/cancan_helper.rb' - 'spec/support/request/authentication_workflow.rb' @@ -1423,7 +1416,7 @@ Style/CaseEquality: - 'app/helpers/angular_form_helper.rb' - 'spec/models/spree/payment_spec.rb' -# Offense count: 86 +# Offense count: 88 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: nested, compact Style/ClassAndModuleChildren: @@ -1469,7 +1462,9 @@ Style/ClassAndModuleChildren: - 'app/serializers/api/admin/line_item_serializer.rb' - 'app/serializers/api/admin/order_cycle_serializer.rb' - 'app/serializers/api/admin/order_serializer.rb' - - 'app/serializers/api/admin/payment_method_serializer.rb' + - 'app/serializers/api/admin/payment_method/base_serializer.rb' + - 'app/serializers/api/admin/payment_method/payment_method_serializer.rb' + - 'app/serializers/api/admin/payment_method/stripe_serializer.rb' - 'app/serializers/api/admin/product_serializer.rb' - 'app/serializers/api/admin/shipping_method_serializer.rb' - 'app/serializers/api/admin/tag_rule_serializer.rb' @@ -1602,7 +1597,7 @@ Style/FileName: Style/FormatStringToken: EnforcedStyle: template -# Offense count: 88 +# Offense count: 89 # Configuration parameters: MinBodyLength. Style/GuardClause: Exclude: @@ -1656,7 +1651,7 @@ Style/GuardClause: - 'spec/support/request/distribution_helper.rb' - 'spec/support/request/shop_workflow.rb' -# Offense count: 1241 +# Offense count: 1255 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys @@ -1686,11 +1681,10 @@ Style/InverseMethods: - 'app/controllers/admin/column_preferences_controller.rb' - 'spec/support/cancan_helper.rb' -# Offense count: 9 +# Offense count: 5 # Cop supports --auto-correct. Style/LineEndConcatenation: Exclude: - - 'app/controllers/spree/admin/base_controller_decorator.rb' - 'lib/spree/core/controller_helpers/respond_with_decorator.rb' - 'spec/controllers/spree/admin/base_controller_spec.rb' @@ -1737,11 +1731,10 @@ Style/MultilineIfModifier: - 'lib/open_food_network/enterprise_issue_validator.rb' - 'lib/spree/core/controller_helpers/respond_with_decorator.rb' -# Offense count: 8 +# Offense count: 7 # Cop supports --auto-correct. Style/MutableConstant: Exclude: - - 'app/controllers/spree/admin/reports_controller_decorator.rb' - 'app/models/enterprise.rb' - 'app/models/enterprise_fee.rb' - 'app/models/spree/payment_method_decorator.rb' @@ -1799,7 +1792,7 @@ Style/NumericLiteralPrefix: Exclude: - 'spec/features/admin/order_cycles_spec.rb' -# Offense count: 16 +# Offense count: 15 # Cop supports --auto-correct. # Configuration parameters: Strict. Style/NumericLiterals: @@ -2026,7 +2019,7 @@ Style/StructInheritance: Exclude: - 'lib/open_food_network/enterprise_fee_applicator.rb' -# Offense count: 182 +# Offense count: 183 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, MinSize, SupportedStyles. # SupportedStyles: percent, brackets @@ -2070,7 +2063,7 @@ Style/SymbolArray: - 'spec/models/exchange_spec.rb' - 'spec/models/spree/ability_spec.rb' -# Offense count: 97 +# Offense count: 96 # Cop supports --auto-correct. # Configuration parameters: IgnoredMethods. # IgnoredMethods: respond_to, define_method @@ -2102,7 +2095,6 @@ Style/SymbolProc: - 'spec/features/admin/order_cycles_spec.rb' - 'spec/lib/open_food_network/group_buy_report_spec.rb' - 'spec/lib/open_food_network/order_grouper_spec.rb' - - 'spec/models/spree/user_spec.rb' # Offense count: 6 # Cop supports --auto-correct. diff --git a/Gemfile b/Gemfile index d4c55bb63f..72c12fa7c5 100644 --- a/Gemfile +++ b/Gemfile @@ -10,15 +10,20 @@ gem 'i18n-js', '~> 3.0.0' gem 'nokogiri', '>= 1.6.7.1' gem 'pg' -gem 'spree', github: 'openfoodfoundation/spree', branch: 'spree-upgrade-step1c' +gem 'spree', github: 'openfoodfoundation/spree', branch: 'step-6a', ref: '5a76d45' gem 'spree_i18n', github: 'spree/spree_i18n', branch: '1-3-stable' -gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '1-3-stable' +gem 'spree_auth_devise', github: 'openfoodfoundation/spree_auth_devise', branch: 'spree-upgrade-intermediate' # Our branch contains two changes # - Pass customer email and phone number to PayPal (merged to upstream master) # - Change type of password from string to password to hide it in the form -gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "hide-password" +gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "spree-upgrade-intermediate" #gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable" +gem 'stripe', '~> 3.3.1' +gem 'activemerchant', '~> 1.71.0' + +gem 'oauth2', '~> 1.2.0' # Used for Stripe Connect +gem 'jwt', '~> 1.5' gem 'delayed_job_active_record' gem 'daemons' @@ -121,7 +126,8 @@ group :test do end group :development do - gem 'pry-byebug' + gem 'byebug', '~> 9.0.0' # 9.1 requires ruby 2.2 + gem 'pry-byebug', '>= 3.4.3' gem 'debugger-linecache' gem 'guard' gem 'guard-livereload' diff --git a/Gemfile.lock b/Gemfile.lock index fcb7283ef9..241c6298bb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,12 +14,12 @@ GIT GIT remote: git://github.com/openfoodfoundation/better_spree_paypal_express.git - revision: 840d973cd5bd3250b17674a624dad494aeb09eb3 - branch: hide-password + revision: 8d95f4544c682634812becaf50999fba0cd04df0 + branch: spree-upgrade-intermediate specs: spree_paypal_express (2.0.3) paypal-sdk-merchant (= 1.106.1) - spree_core (~> 1.3.4) + spree_core (~> 1.3.99) GIT remote: git://github.com/openfoodfoundation/ofn-qz.git @@ -30,49 +30,78 @@ GIT GIT remote: git://github.com/openfoodfoundation/spree.git - revision: a4c439570b77afa50f9e36299811f293232bd281 - branch: spree-upgrade-step1c + revision: 5a76d456ff70aea7aae3d25156558d71eb7febf2 + ref: 5a76d45 + branch: step-6a specs: spree (1.3.99) spree_api (= 1.3.99) + spree_backend (= 1.3.99) spree_cmd (= 1.3.99) spree_core (= 1.3.99) spree_dash (= 1.3.99) - spree_promo (= 1.3.99) + spree_frontend (= 1.3.99) spree_sample (= 1.3.99) spree_api (1.3.99) + rabl (= 0.7.2) spree_core (= 1.3.99) versioncake (= 0.4.0) + spree_backend (1.3.99) + deface (>= 0.9.0) + jquery-rails (~> 2.0) + rails (~> 3.2.8) + select2-rails (~> 3.2) + spree_api (= 1.3.99) + spree_core (= 1.3.99) + stringex (~> 1.3.2) spree_cmd (1.3.99) thor (>= 0.14.6) spree_core (1.3.99) - activemerchant (~> 1.50.0) - acts_as_list (= 0.1.4) + activemerchant (~> 1.50) + acts_as_list (= 0.1.9) awesome_nested_set (= 2.1.5) aws-sdk (~> 1.11.1) cancan (= 1.6.8) deface (>= 0.9.0) ffaker (~> 1.15.0) - highline (= 1.6.11) - jquery-rails (~> 2.0) + highline (= 1.6.15) + httparty (= 0.9.0) json (>= 1.5.5) kaminari (= 0.13.0) - money (= 5.0.0) + money (= 5.1.0) paperclip (~> 3.0) - rabl (= 0.7.2) rails (~> 3.2.13) ransack (= 0.7.2) - select2-rails (~> 3.2) state_machine (= 1.2.0) stringex (~> 1.3.2) + truncate_html (= 0.9.2) spree_dash (1.3.99) - httparty (~> 0.8.1) - spree_core (= 1.3.99) - spree_promo (1.3.99) + httparty (~> 0.9.0) + spree_backend (= 1.3.99) + spree_frontend (= 1.3.99) + spree_frontend (1.3.99) + deface (>= 0.9.0) + jquery-rails (~> 2.2.1) + rails (~> 3.2.8) + select2-rails (~> 3.2) + spree_api (= 1.3.99) spree_core (= 1.3.99) + stringex (~> 1.3.2) spree_sample (1.3.99) spree_core (= 1.3.99) +GIT + remote: git://github.com/openfoodfoundation/spree_auth_devise.git + revision: da9eecefc6fe13dedf4c6f3febec79caad839ec3 + branch: spree-upgrade-intermediate + specs: + spree_auth_devise (2.0.0) + devise (~> 2.2.5) + devise-encryptable (= 0.1.2) + spree_backend (~> 1.3.6) + spree_core (~> 1.3.6) + spree_frontend (~> 1.3.6) + GIT remote: git://github.com/spree/deface.git revision: 1110a1336252109bce7f98f9182042e0bc2930ae @@ -83,17 +112,6 @@ GIT nokogiri (~> 1.6.0) rails (>= 3.1) -GIT - remote: git://github.com/spree/spree_auth_devise.git - revision: ba95589a85368297c844f096c2a0c121e5b08138 - branch: 1-3-stable - specs: - spree_auth_devise (1.3.0) - cancan (~> 1.6.7) - devise (~> 2.2.3) - devise-encryptable (= 0.1.2) - spree_core - GIT remote: git://github.com/spree/spree_i18n.git revision: 752eb67204e9c5a4e22b62591a8fd55fe2285e43 @@ -133,8 +151,8 @@ GEM sprockets (~> 2.2.1) active_model_serializers (0.8.3) activemodel (>= 3.0) - activemerchant (1.50.0) - activesupport (>= 3.2.14, < 5.0.0) + activemerchant (1.71.0) + activesupport (>= 3.2.14, < 6.x) builder (>= 2.1.2, < 4.0.0) i18n (>= 0.6.9) nokogiri (~> 1.4) @@ -154,8 +172,8 @@ GEM multi_json (~> 1.0) acts-as-taggable-on (3.5.0) activerecord (>= 3.2, < 5) - acts_as_list (0.1.4) - addressable (2.3.3) + acts_as_list (0.1.9) + addressable (2.4.0) andand (1.3.3) angular-rails-templates (0.2.0) railties (>= 3.1) @@ -174,15 +192,13 @@ GEM json (~> 1.4) nokogiri (>= 1.4.4) uuidtools (~> 2.1) - bcrypt (3.1.7) + bcrypt (3.1.11) bcrypt-ruby (3.1.5) bcrypt (>= 3.1.3) blockenspiel (0.4.5) bugsnag (4.1.0) builder (3.0.4) - byebug (2.7.0) - columnize (~> 0.3) - debugger-linecache (~> 1.2) + byebug (9.0.6) cancan (1.6.8) capybara (2.7.1) addressable @@ -195,12 +211,11 @@ GEM timers (~> 1.1.0) chronic (0.10.2) chunky_png (1.3.4) - climate_control (0.0.3) - activesupport (>= 3.0) + climate_control (0.1.0) cliver (0.3.2) cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) - coderay (1.0.9) + coderay (1.1.2) coffee-rails (3.2.2) coffee-script (>= 2.2.0) railties (~> 3.2.0) @@ -208,8 +223,7 @@ GEM coffee-script-source execjs coffee-script-source (1.10.0) - colorize (0.7.7) - columnize (0.9.0) + colorize (0.8.1) compass (1.0.3) chunky_png (~> 1.2) compass-core (~> 1.0.2) @@ -226,8 +240,8 @@ GEM compass (~> 1.0.0) sass-rails (<= 5.0.1) sprockets (< 2.13) - crack (0.4.1) - safe_yaml (~> 0.9.0) + crack (0.4.3) + safe_yaml (~> 1.0.0) css_parser (1.3.5) addressable css_splitter (0.4.5) @@ -266,6 +280,8 @@ GEM factory_girl_rails (3.3.0) factory_girl (~> 3.3.0) railties (>= 3.0.0) + faraday (0.9.2) + multipart-post (>= 1.2, < 3) ffaker (1.15.0) ffi (1.9.3) figaro (0.7.0) @@ -410,10 +426,10 @@ GEM rspec (~> 2.14) haml (4.0.4) tilt - highline (1.6.11) + highline (1.6.15) hike (1.2.3) http_parser.rb (0.5.3) - httparty (0.8.3) + httparty (0.9.0) multi_json (~> 1.0) multi_xml i18n (0.6.11) @@ -426,13 +442,14 @@ GEM ipaddress (0.8.0) journey (1.0.4) jquery-migrate-rails (1.2.1) - jquery-rails (2.3.0) + jquery-rails (2.2.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - json (1.8.3) + json (1.8.6) json_spec (1.1.1) multi_json (~> 1.0) rspec (~> 2.0) + jwt (1.5.4) kaminari (0.13.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -454,19 +471,25 @@ GEM mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) - method_source (0.8.2) + method_source (0.9.0) mime-types (1.25.1) - mini_portile2 (2.0.0) + mini_portile2 (2.1.0) momentjs-rails (2.5.1) railties (>= 3.1) - money (5.0.0) - i18n (~> 0.4) - json + money (5.1.0) + i18n (~> 0.6.0) multi_json (1.12.1) - multi_xml (0.5.5) + multi_xml (0.6.0) + multipart-post (2.0.0) newrelic_rpm (3.12.0.288) - nokogiri (1.6.7.2) - mini_portile2 (~> 2.0.0.rc2) + nokogiri (1.6.8.1) + mini_portile2 (~> 2.1.0) + oauth2 (1.2.0) + faraday (>= 0.8, < 0.10) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) oj (2.1.2) orm_adapter (0.5.0) paper_trail (3.0.8) @@ -497,18 +520,17 @@ GEM activerecord (~> 3.0) polyglot (0.3.5) powerpack (0.1.1) - pry (0.9.12.2) - coderay (~> 1.0.5) - method_source (~> 0.8) - slop (~> 3.4) - pry-byebug (1.3.2) - byebug (~> 2.7) - pry (~> 0.9.12) + pry (0.11.1) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + pry-byebug (3.4.3) + byebug (>= 9.0, < 9.1) + pry (~> 0.10) rabl (0.7.2) activesupport (>= 2.3.14) multi_json (~> 1.0) rack (1.4.7) - rack-cache (1.6.1) + rack-cache (1.7.0) rack (>= 0.4) rack-livereload (0.3.15) rack @@ -537,7 +559,7 @@ GEM rainbow (2.2.2) rake raindrops (0.13.0) - rake (11.3.0) + rake (10.5.0) ransack (0.7.2) actionpack (~> 3.0) activerecord (~> 3.0) @@ -574,8 +596,9 @@ GEM rspec-expectations (2.14.0) diff-lcs (>= 1.1.3, < 2.0) rspec-mocks (2.14.2) - rspec-rails (2.14.0) + rspec-rails (2.14.2) actionpack (>= 3.0) + activemodel (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) rspec-core (~> 2.14.0) @@ -592,7 +615,7 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.8.1) rubyzip (1.2.0) - safe_yaml (0.9.5) + safe_yaml (1.0.4) sass (3.3.14) sass-rails (3.2.6) railties (~> 3.2.0) @@ -602,7 +625,6 @@ GEM thor (~> 0.14) shoulda-matchers (1.1.0) activesupport (>= 3.0.0) - slop (3.4.5) spinjs-rails (1.3) rails (>= 3.1) sprockets (2.2.3) @@ -612,23 +634,25 @@ GEM tilt (~> 1.1, != 1.3.0) state_machine (1.2.0) stringex (1.3.3) + stripe (3.3.1) + faraday (~> 0.9) therubyracer (0.12.0) libv8 (~> 3.16.14.0) ref - thor (0.19.1) + thor (0.19.4) tilt (1.4.1) timecop (0.8.1) timers (1.1.0) treetop (1.4.15) polyglot polyglot (>= 0.3.1) - truncate_html (0.5.5) + truncate_html (0.9.2) turbo-sprockets-rails3 (0.3.6) railties (> 3.2.8, < 4.0.0) sprockets (>= 2.0.0) turn (0.8.3) ansi - tzinfo (0.3.49) + tzinfo (0.3.53) uglifier (2.7.1) execjs (>= 0.3.0) json (>= 1.8.0) @@ -645,11 +669,11 @@ GEM actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - warden (1.2.3) + warden (1.2.6) rack (>= 1.0) - webmock (1.13.0) + webmock (1.8.11) addressable (>= 2.2.7) - crack (>= 0.3.2) + crack (>= 0.1.7) websocket-driver (0.6.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) @@ -658,7 +682,7 @@ GEM chronic (>= 0.6.3) wicked_pdf (1.1.0) wkhtmltopdf-binary (0.12.3.1) - xml-simple (1.1.4) + xml-simple (1.1.5) xpath (2.0.0) nokogiri (~> 1.3) @@ -667,6 +691,7 @@ PLATFORMS DEPENDENCIES active_model_serializers + activemerchant (~> 1.71.0) acts-as-taggable-on (~> 3.4) andand angular-rails-templates (~> 0.2.0) @@ -677,6 +702,7 @@ DEPENDENCIES aws-sdk blockenspiel bugsnag + byebug (~> 9.0.0) capybara coffee-rails (~> 3.2.1) compass-rails @@ -711,11 +737,13 @@ DEPENDENCIES jquery-migrate-rails jquery-rails json_spec + jwt (~> 1.5) knapsack letter_opener momentjs-rails newrelic_rpm nokogiri (>= 1.6.7.1) + oauth2 (~> 1.2.0) ofn-qz! oj paper_trail (~> 3.0.8) @@ -723,7 +751,7 @@ DEPENDENCIES parallel_tests pg poltergeist - pry-byebug + pry-byebug (>= 3.4.3) rabl rack-livereload rack-ssl @@ -745,6 +773,7 @@ DEPENDENCIES spree_auth_devise! spree_i18n! spree_paypal_express! + stripe (~> 3.3.1) therubyracer timecop truncate_html @@ -762,4 +791,4 @@ RUBY VERSION ruby 2.1.5p273 BUNDLED WITH - 1.15.2 + 1.15.4 diff --git a/README.md b/README.md index b6843b41d5..b010b880e4 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Do not forget to run `rake tmp:cache:clear` after locales are updated to reload * Maikel Linke (https://github.com/mkllnk) * Lynne Davis (https://github.com/lin-d-hop) * Paul Mackay (https://github.com/pmackay) -* Steve Petitt (https://github.com/stveep) +* Steve Pettitt (https://github.com/stveep) ## Licence diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 1524268900..34fab997a7 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -14,9 +14,8 @@ //= require angular-resource //= require angular-animate //= require angular-sanitize -//= require admin/spree_core +//= require admin/spree_backend //= require admin/spree_auth -//= require admin/spree_promo //= require admin/spree_paypal_express //= require ../shared/ng-infinite-scroll.min.js //= require ../shared/ng-tags-input.min.js diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index fa346e100f..c69e109236 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -22,7 +22,7 @@ angular.module("admin.enterprises") { name: 'users', label: t('users'), icon_class: "icon-user" } ] - $scope.select(0) + SideMenu.init() $scope.showItem = (item) -> if item.show? diff --git a/app/assets/javascripts/admin/payment_methods/controllers/stripe_controller.js.coffee b/app/assets/javascripts/admin/payment_methods/controllers/stripe_controller.js.coffee new file mode 100644 index 0000000000..2e55dae3a0 --- /dev/null +++ b/app/assets/javascripts/admin/payment_methods/controllers/stripe_controller.js.coffee @@ -0,0 +1,18 @@ +angular.module("admin.paymentMethods").controller "StripeController", ($scope, $http, shops) -> + $scope.shops = shops + $scope.stripe_account = {} + + $scope.$watch "paymentMethod.preferred_enterprise_id", (newID, oldID) -> + return unless newID? + $scope.stripe_account = {} + $http.get("/admin/stripe_accounts/status.json?enterprise_id=#{newID}").success (data) -> + angular.extend($scope.stripe_account, data) + .error (response) -> + $scope.stripe_account.status = "request_failed" + + $scope.current_enterprise_stripe_path = -> + return unless $scope.paymentMethod.preferred_enterprise_id? + permalink = shops.filter((shop) -> + shop.id == $scope.paymentMethod.preferred_enterprise_id + )[0].permalink + "/admin/enterprises/#{permalink}/edit#/payment_methods" diff --git a/app/assets/javascripts/admin/payments/new.js b/app/assets/javascripts/admin/payments/new.js new file mode 100644 index 0000000000..e50c348daa --- /dev/null +++ b/app/assets/javascripts/admin/payments/new.js @@ -0,0 +1,33 @@ +// Override of Spree's logic in the file of the same name +// Changes made as per https://github.com/spree/spree/commit/8a3a80b08abf80fbed2fcee4b429ba1caf68baf1 +// which allows the form partial in admin/payments/new to be switched using radio buttons +// We can remove this file when we reach 2.3.0 + +$(document).ready(function() { + if ($("#new_payment").is("*")) { + $('.payment_methods_radios').click( + function() { + $('.payment-methods').hide(); + if (this.checked) { + $('#payment_method_' + this.value).show(); + } + } + ); + + $('.payment_methods_radios').each( + function() { + if (this.checked) { + $('#payment_method_' + this.value).show(); + } else { + $('#payment_method_' + this.value).hide(); + } + } + ); + + $(".card_new").radioControlsVisibilityOfElement('.card_form'); + + $('select.jump_menu').change(function(){ + window.location = this.options[this.selectedIndex].value; + }); + } +}); diff --git a/app/assets/javascripts/admin/resources/resources/line_item_resource.js.coffee b/app/assets/javascripts/admin/resources/resources/line_item_resource.js.coffee index 4301f8df82..2ec5703c82 100644 --- a/app/assets/javascripts/admin/resources/resources/line_item_resource.js.coffee +++ b/app/assets/javascripts/admin/resources/resources/line_item_resource.js.coffee @@ -1,5 +1,5 @@ angular.module("admin.resources").factory 'LineItemResource', ($resource) -> - $resource('/admin/:orders/:order_number/line_items/:id.json', {}, { + $resource('/admin/bulk_line_items/:id.json', {}, { 'index': method: 'GET' isArray: true diff --git a/app/assets/javascripts/admin/resources/services/line_items.js.coffee b/app/assets/javascripts/admin/resources/services/line_items.js.coffee index 182ef81e6a..7c96caecd8 100644 --- a/app/assets/javascripts/admin/resources/services/line_items.js.coffee +++ b/app/assets/javascripts/admin/resources/services/line_items.js.coffee @@ -26,7 +26,7 @@ angular.module("admin.resources").factory 'LineItems', ($q, LineItemResource) -> save: (lineItem) -> deferred = $q.defer() lineItem.errors = {} - lineItem.$update({id: lineItem.id, orders: "orders", order_number: lineItem.order.number}) + lineItem.$update({id: lineItem.id}) .then( (data) => @pristineByID[lineItem.id] = angular.copy(lineItem) deferred.resolve(data) @@ -54,7 +54,7 @@ angular.module("admin.resources").factory 'LineItems', ($q, LineItemResource) -> delete: (lineItem, callback=null) -> deferred = $q.defer() - lineItem.$delete({id: lineItem.id, orders: "orders", order_number: lineItem.order.number}) + lineItem.$delete({id: lineItem.id}) .then( (data) => delete @byID[lineItem.id] delete @pristineByID[lineItem.id] diff --git a/app/assets/javascripts/admin/services/bulk_products.js.coffee b/app/assets/javascripts/admin/services/bulk_products.js.coffee index 4e06a87bd2..0d2de5ce77 100644 --- a/app/assets/javascripts/admin/services/bulk_products.js.coffee +++ b/app/assets/javascripts/admin/services/bulk_products.js.coffee @@ -1,4 +1,4 @@ -angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher) -> +angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher, $http) -> new class BulkProducts products: [] @@ -11,14 +11,8 @@ angular.module("ofn.admin").factory "BulkProducts", (PagedFetcher, dataFetcher) PagedFetcher.fetch url, (data) => @addProducts data.products cloneProduct: (product) -> - dataFetcher("/admin/products/" + product.permalink_live + "/clone.json").then (data) => - # Ideally we would use Spree's built in respond_override helper here to redirect the - # user after a successful clone with .json in the accept headers - # However, at the time of writing there appears to be an issue which causes the - # respond_with block in the destroy action of Spree::Admin::Product to break - # when a respond_overrride for the clone action is used. - id = data.product.id - dataFetcher("/api/products/" + id + "?template=bulk_show").then (newProduct) => + $http.post("/api/products/" + product.id + "/clone").success (data) => + dataFetcher("/api/products/" + data.id + "?template=bulk_show").then (newProduct) => @unpackProduct newProduct @insertProductAfter(product, newProduct) diff --git a/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee b/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee index 020f0981d8..4f6fe5ab59 100644 --- a/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee +++ b/app/assets/javascripts/admin/side_menu/services/side_menu.js.coffee @@ -1,9 +1,21 @@ angular.module("admin.side_menu") - .factory "SideMenu", -> + .factory "SideMenu", ($location) -> new class SideMenu items: [] selected: null + + # Checks for path and uses it to set the view + # If no path, loads first view + init: => + path = $location.path()?.match(/^\/\w+$/)?[0] + index = if path + name = path[1..] + @items.indexOf(@find_by_name(name)) + else + 0 + @select(index) + setItems: (items) => @items = items item.visible = true for item in @items @@ -13,6 +25,7 @@ angular.module("admin.side_menu") @selected.selected = false if @selected @selected = @items[index] @selected.selected = true + $location.path(@selected.name) find_by_name: (name) => for item in @items when item.name is name diff --git a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee b/app/assets/javascripts/admin/utils/directives/ofn-select2.js.coffee similarity index 93% rename from app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee rename to app/assets/javascripts/admin/utils/directives/ofn-select2.js.coffee index 6890589b9d..b0afdc46e5 100644 --- a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee +++ b/app/assets/javascripts/admin/utils/directives/ofn-select2.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.indexUtils").directive "ofnSelect2", ($sanitize, $timeout, $filter) -> +angular.module("admin.utils").directive "ofnSelect2", ($sanitize, $timeout, $filter) -> require: 'ngModel' restrict: 'C' scope: diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee index 91161c6f8c..e85f9e5cee 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/accordion_controller.js.coffee @@ -13,8 +13,9 @@ Darkswarm.controller "AccordionCtrl", ($scope, localStorageService, $timeout, $d # scroll location is closed by show(), scrollTo() will scroll to the old location of # the element. Putting this in a 50 ms timeout is enough delay for the DOM to # have updated. - $timeout (-> - $document.scrollTo $("##{section}"), offset_height, 500), 50 + $timeout -> + $document.scrollTo($("##{section}"), offset_height, 500) + , 50 $scope.$on 'purchaseFormInvalid', (event, form) -> # Scroll to first invalid section diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee index 8a4f187773..3397077a00 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/checkout_controller.js.coffee @@ -22,6 +22,6 @@ Darkswarm.controller "CheckoutCtrl", ($scope, localStorageService, Checkout, Cur event.preventDefault() $scope.submitted = true if form.$valid - $scope.Checkout.submit() + $scope.Checkout.purchase() else $scope.$broadcast 'purchaseFormInvalid', form diff --git a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee index 924df3c447..04ff6ab45e 100644 --- a/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/checkout/payment_controller.js.coffee @@ -1,23 +1,11 @@ -Darkswarm.controller "PaymentCtrl", ($scope, $timeout) -> +Darkswarm.controller "PaymentCtrl", ($scope, $timeout, savedCreditCards, Dates) -> angular.extend(this, new FieldsetMixin($scope)) + + $scope.savedCreditCards = savedCreditCards $scope.name = "payment" + $scope.months = Dates.months + $scope.years = Dates.years - $scope.months = [ - {key: t("january"), value: "1"}, - {key: t("february"), value: "2"}, - {key: t("march"), value: "3"}, - {key: t("april"), value: "4"}, - {key: t("may"), value: "5"}, - {key: t("june"), value: "6"}, - {key: t("july"), value: "7"}, - {key: t("august"), value: "8"}, - {key: t("september"), value: "9"}, - {key: t("october"), value: "10"}, - {key: t("november"), value: "11"}, - {key: t("december"), value: "12"}, - ] - - $scope.years = [moment().year()..(moment().year()+15)] $scope.secrets.card_month = "1" $scope.secrets.card_year = moment().year() diff --git a/app/assets/javascripts/darkswarm/controllers/credit_cards_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/credit_cards_controller.js.coffee new file mode 100644 index 0000000000..7fa3e4f8c4 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/credit_cards_controller.js.coffee @@ -0,0 +1,12 @@ +Darkswarm.controller "CreditCardsCtrl", ($scope, $timeout, CreditCard, CreditCards, Dates) -> + angular.extend(this, new FieldsetMixin($scope)) + $scope.savedCreditCards = CreditCards.saved + $scope.CreditCard = CreditCard + $scope.secrets = CreditCard.secrets + $scope.showForm = CreditCard.show + $scope.storeCard = -> + if $scope.new_card_form.$valid + CreditCard.requestToken() + + $scope.allow_name_change = true + $scope.disable_fields = false diff --git a/app/assets/javascripts/darkswarm/controllers/distributor_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/distributor_node_controller.js.coffee deleted file mode 100644 index 6705634771..0000000000 --- a/app/assets/javascripts/darkswarm/controllers/distributor_node_controller.js.coffee +++ /dev/null @@ -1,9 +0,0 @@ -Darkswarm.controller "DistributorNodeCtrl", ($scope, HashNavigation, $anchorScroll) -> - $scope.toggle = -> - HashNavigation.toggle $scope.distributor.hash - - $scope.open = -> - HashNavigation.active($scope.distributor.hash) - - if $scope.open() - $anchorScroll() diff --git a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee index f0a174a47c..4f85093df5 100644 --- a/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/enterprises_controller.js.coffee @@ -6,10 +6,9 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, $location $scope.openModal = EnterpriseModal.open $scope.activeTaxons = [] $scope.show_profiles = false + $scope.show_closed = false $scope.filtersActive = false $scope.distanceMatchesShown = false - $scope.filterExpression = {active: true} - $scope.$watch "query", (query)-> Enterprises.flagMatching query @@ -36,7 +35,7 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, $location # When filter settings change, this could change which name match is at the top, or even # result in no matches. This affects the reference point that the distance matches are # calculated from, so we need to recalculate distances. - $scope.$watch '[activeTaxons, activeProperties, shippingTypes, show_profiles]', -> + $scope.$watch '[activeTaxons, activeProperties, shippingTypes, show_profiles, show_closed]', -> $timeout -> Enterprises.calculateDistance $scope.query, $scope.firstNameMatch() $rootScope.$broadcast 'enterprisesChanged' @@ -74,9 +73,9 @@ Darkswarm.controller "EnterprisesCtrl", ($scope, $rootScope, $timeout, $location undefined $scope.showClosedShops = -> - delete $scope.filterExpression['active'] + $scope.show_closed = true $location.search('show_closed', '1') $scope.hideClosedShops = -> - $scope.filterExpression['active'] = true + $scope.show_closed = false $location.search('show_closed', null) diff --git a/app/assets/javascripts/darkswarm/controllers/orders_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/orders_controller.js.coffee index 116a2b6fd8..63e28f77a8 100644 --- a/app/assets/javascripts/darkswarm/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/orders_controller.js.coffee @@ -1,2 +1,2 @@ -Darkswarm.controller "OrdersCtrl", ($scope, $rootScope, $timeout, Orders, Search, $document, HashNavigation, FilterSelectorsService, EnterpriseModal, enterpriseMatchesNameQueryFilter, distanceWithinKmFilter) -> +Darkswarm.controller "OrdersCtrl", ($scope, Orders) -> $scope.Orders = Orders diff --git a/app/assets/javascripts/darkswarm/controllers/registration/registration_form_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/registration/registration_form_controller.js.coffee index fabc2c382a..546186f53d 100644 --- a/app/assets/javascripts/darkswarm/controllers/registration/registration_form_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/registration/registration_form_controller.js.coffee @@ -1,15 +1,23 @@ Darkswarm.controller "RegistrationFormCtrl", ($scope, RegistrationService, EnterpriseRegistrationService) -> $scope.submitted = false + $scope.isDisabled = false $scope.valid = (form) -> $scope.submitted = !form.$valid form.$valid $scope.create = (form) -> - EnterpriseRegistrationService.create() if $scope.valid(form) + $scope.disableButton() + EnterpriseRegistrationService.create($scope.enableButton) if $scope.valid(form) $scope.update = (nextStep, form) -> EnterpriseRegistrationService.update(nextStep) if $scope.valid(form) $scope.selectIfValid = (nextStep, form) -> RegistrationService.select(nextStep) if $scope.valid(form) + + $scope.disableButton = -> + $scope.isDisabled = true + + $scope.enableButton = -> + $scope.isDisabled = false diff --git a/app/assets/javascripts/darkswarm/controllers/shop_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/shop_node_controller.js.coffee new file mode 100644 index 0000000000..34c72f21b7 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/shop_node_controller.js.coffee @@ -0,0 +1,9 @@ +Darkswarm.controller "ShopNodeCtrl", ($scope, HashNavigation, $anchorScroll) -> + $scope.toggle = -> + HashNavigation.toggle $scope.shop.hash + + $scope.open = -> + HashNavigation.active($scope.shop.hash) + + if $scope.open() + $anchorScroll() diff --git a/app/assets/javascripts/darkswarm/directives/stripe_elements.js.coffee b/app/assets/javascripts/darkswarm/directives/stripe_elements.js.coffee new file mode 100644 index 0000000000..d325b6f962 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/stripe_elements.js.coffee @@ -0,0 +1,35 @@ +Darkswarm.directive "stripeElements", ($injector, StripeElements) -> + restrict: 'E' + template: "" + + link: (scope, elem, attr)-> + if $injector.has('stripeObject') + stripe = $injector.get('stripeObject') + + card = stripe.elements().create 'card', + hidePostalCode: false + style: + base: + fontFamily: "Roboto, Arial, sans-serif" + fontSize: '16px' + color: '#5c5c5c' + '::placeholder': + color: '#6c6c6c' + card.mount('#card-element') + + # Elements validates user input as it is typed. To help your customers + # catch mistakes, you should listen to change events on the card Element + # and display any errors: + card.addEventListener 'change', (event) -> + displayError = document.getElementById('card-errors') + if event.error + displayError.textContent = event.error.message + else + displayError.textContent = '' + return + + StripeElements.stripe = stripe + StripeElements.card = card diff --git a/app/assets/javascripts/darkswarm/directives/tab.js.coffee b/app/assets/javascripts/darkswarm/directives/tab.js.coffee new file mode 100644 index 0000000000..d68e629688 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/tab.js.coffee @@ -0,0 +1,12 @@ +Darkswarm.directive "tab", -> + restrict: "C" + require: "^^tabsetCtrl" + scope: + name: "@" + link: (scope, element, attrs, ctrl) -> + element.on "click", -> + scope.$apply -> + ctrl.toggle(scope.name) + + ctrl.registerSelectionListener (prefix, selection) -> + element.toggleClass('selected', selection == scope.name) diff --git a/app/assets/javascripts/darkswarm/directives/tab_view.coffee b/app/assets/javascripts/darkswarm/directives/tab_view.coffee new file mode 100644 index 0000000000..b9b35317b8 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/tab_view.coffee @@ -0,0 +1,15 @@ +Darkswarm.directive "tabView", -> + restrict: "C" + require: "^^tabsetCtrl" + template: "
" + scope: + templates: "=" + link: (scope, element, attrs, ctrl) -> + scope.template = null + + ctrl.registerSelectionListener (prefix, selection) -> + if selection? + selection = "#{prefix}/#{selection}" if prefix? + scope.template = "#{selection}.html" + else + scope.template = null diff --git a/app/assets/javascripts/darkswarm/directives/tabset_ctrl.js.coffee b/app/assets/javascripts/darkswarm/directives/tabset_ctrl.js.coffee new file mode 100644 index 0000000000..dd8d9a279b --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/tabset_ctrl.js.coffee @@ -0,0 +1,28 @@ +Darkswarm.directive "tabsetCtrl", (Tabsets, $location) -> + restrict: "C" + scope: + id: "@" + selected: "@" + navigate: "=" + prefix: "@?" + controller: ($scope, $element) -> + if $scope.navigate + path = $location.path()?.match(/^\/\w+$/)?[0] + $scope.selected = path[1..] if path + + this.toggle = (name) -> + Tabsets.toggle($scope.id, name) + + this.select = (selection) -> + $scope.$broadcast("selection:changed", selection) + $element.toggleClass("expanded", selection?) + $location.path(selection) if $scope.navigate + + this.registerSelectionListener = (callback) -> + $scope.$on "selection:changed", (event, selection) -> + callback($scope.prefix, selection) + + this + + link: (scope, element, attrs, ctrl) -> + Tabsets.register(ctrl, scope.id, scope.selected) diff --git a/app/assets/javascripts/darkswarm/filters/closed_shops.js.coffee b/app/assets/javascripts/darkswarm/filters/closed_shops.js.coffee new file mode 100644 index 0000000000..23d5de8255 --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/closed_shops.js.coffee @@ -0,0 +1,7 @@ +Darkswarm.filter 'closedShops', -> + (enterprises, show_closed) -> + enterprises ||= [] + show_closed ?= false + + enterprises.filter (enterprise) => + show_closed or enterprise.active or !enterprise.is_distributor diff --git a/app/assets/javascripts/darkswarm/filters/localize_currency.js.coffee b/app/assets/javascripts/darkswarm/filters/localize_currency.js.coffee index 7087e09fc3..37de2759cc 100644 --- a/app/assets/javascripts/darkswarm/filters/localize_currency.js.coffee +++ b/app/assets/javascripts/darkswarm/filters/localize_currency.js.coffee @@ -3,13 +3,12 @@ Darkswarm.filter "localizeCurrency", (currencyConfig)-> (amount) -> # Set country code (eg. "US"). currency_code = if currencyConfig.display_currency then " " + currencyConfig.currency else "" - # Set decimal points, 2 or 0 if hide_cents. + # Set decimal points, 2 or 0 if hide_cents. decimals = if currencyConfig.hide_cents == "true" then 0 else 2 - # We need to use parseFloat before toFixed as the amount should come in as a string. - amount_fixed = parseFloat(amount).toFixed(decimals) + # Set format if the currency symbol should come after the number, otherwise (default) use the locale setting. + format = if currencyConfig.symbol_position == "after" then "%n %u" else undefined + # We need to use parseFloat as the amount should come in as a string. + amount = parseFloat(amount) - # Build the final price string. TODO use spree decimal point and spacer character settings. - if currencyConfig.symbol_position == 'before' - currencyConfig.symbol + amount_fixed + currency_code - else - amount_fixed + " " + currencyConfig.symbol + currency_code + # Build the final price string. + I18n.toCurrency(amount, {precision: decimals, unit: currencyConfig.symbol, format: format}) + currency_code diff --git a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee index e5aa1375c0..739d2de9bd 100644 --- a/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee +++ b/app/assets/javascripts/darkswarm/mixins/fieldset_mixin.js.coffee @@ -1,6 +1,7 @@ window.FieldsetMixin = ($scope)-> $scope.next = (event = false)-> event.preventDefault() if event + return unless $scope.nextPanel $scope.show $scope.nextPanel $scope.onTimeout = -> diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee index 8f43b783a2..204c35ac66 100644 --- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee @@ -1,12 +1,18 @@ -Darkswarm.factory 'Checkout', (CurrentOrder, ShippingMethods, PaymentMethods, $http, Navigation, CurrentHub, RailsFlashLoader, Loading)-> +Darkswarm.factory 'Checkout', ($injector, CurrentOrder, ShippingMethods, StripeElements, PaymentMethods, $http, Navigation, CurrentHub, RailsFlashLoader, Loading)-> new class Checkout errors: {} secrets: {} order: CurrentOrder.order - submit: -> + purchase: -> + if @paymentMethod()?.method_type == 'stripe' && !@secrets.selected_card + StripeElements.requestToken(@secrets, @submit) + else + @submit() + + submit: => Loading.message = t 'submitting_order' - $http.put('/checkout', {order: @preprocess()}).success (data, status)=> + $http.put('/checkout.json', {order: @preprocess()}).success (data, status)=> Navigation.go data.path .error (response, status)=> if response.path @@ -53,6 +59,23 @@ Darkswarm.factory 'Checkout', (CurrentOrder, ShippingMethods, PaymentMethods, $h last_name: @order.bill_address.lastname } + if @paymentMethod()?.method_type == 'stripe' + if @secrets.selected_card + angular.extend munged_order, { + existing_card_id: @secrets.selected_card + } + else + angular.extend munged_order.payments_attributes[0], { + source_attributes: + gateway_payment_profile_id: @secrets.token + cc_type: @secrets.cc_type + last_digits: @secrets.card.last4 + month: @secrets.card.exp_month + year: @secrets.card.exp_year + first_name: @order.bill_address.firstname + last_name: @order.bill_address.lastname + save_requested_by_customer: @secrets.save_requested_by_customer + } munged_order shippingMethod: -> diff --git a/app/assets/javascripts/darkswarm/services/credit_card.js.coffee b/app/assets/javascripts/darkswarm/services/credit_card.js.coffee new file mode 100644 index 0000000000..28c2cde5f3 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/credit_card.js.coffee @@ -0,0 +1,41 @@ +Darkswarm.factory 'CreditCard', ($injector, $rootScope, CreditCards, StripeElements, Navigation, $http, RailsFlashLoader, Loading)-> + new class CreditCard + visible: false + errors: {} + secrets: {} + + requestToken: => + @setFullName() + StripeElements.requestToken(@secrets, @submit, t("saving_credit_card")) + + submit: => + params = @process_params() + $http.put('/credit_cards/new_from_token', params ) + .success (data, status) => + Loading.clear() + @reset() + CreditCards.add(data) + .error (response, status) => + if response.path + Navigation.go response.path + else + Loading.clear() + @errors = response.errors + RailsFlashLoader.loadFlash(response.flash) + + setFullName: -> + @secrets.name = "#{@secrets.first_name} #{@secrets.last_name}" + + process_params: -> + {"exp_month": @secrets.card.exp_month, + "exp_year": @secrets.card.exp_year, + "last4": @secrets.card.last4, + "token": @secrets.token, + "cc_type": @secrets.cc_type} + + show: => @visible = true + + reset: => + @visible = false + delete @secrets[k] for k, v of @secrets + delete @errors[k] for k, v of @errors diff --git a/app/assets/javascripts/darkswarm/services/credit_cards.js.coffee b/app/assets/javascripts/darkswarm/services/credit_cards.js.coffee new file mode 100644 index 0000000000..c588ef681c --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/credit_cards.js.coffee @@ -0,0 +1,6 @@ +Darkswarm.factory 'CreditCards', (savedCreditCards)-> + new class CreditCard + saved: savedCreditCards + + add: (card) -> + @saved.push card diff --git a/app/assets/javascripts/darkswarm/services/dates.js.coffee b/app/assets/javascripts/darkswarm/services/dates.js.coffee new file mode 100644 index 0000000000..d9a0b6e8c4 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/dates.js.coffee @@ -0,0 +1,18 @@ +Darkswarm.factory "Dates", -> + new class Dates + months: [ + {key: t("january"), value: "1"}, + {key: t("february"), value: "2"}, + {key: t("march"), value: "3"}, + {key: t("april"), value: "4"}, + {key: t("may"), value: "5"}, + {key: t("june"), value: "6"}, + {key: t("july"), value: "7"}, + {key: t("august"), value: "8"}, + {key: t("september"), value: "9"}, + {key: t("october"), value: "10"}, + {key: t("november"), value: "11"}, + {key: t("december"), value: "12"}, + ] + + years: [moment().year()..(moment().year()+15)] diff --git a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee index 1434ffa44f..25ec644a66 100644 --- a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee @@ -11,7 +11,11 @@ Darkswarm.factory "EnterpriseRegistrationService", ($http, RegistrationService, for key, value of enterpriseAttributes @enterprise[key] = value - create: => + # Creates the enterprise and redirects to the about step on success. + # + # @param callback [Function] executed at the end of the operation both in + # case of success or failure. + create: (callback) => Loading.message = t('creating') + " " + @enterprise.name $http( method: "POST" @@ -33,6 +37,7 @@ Darkswarm.factory "EnterpriseRegistrationService", ($http, RegistrationService, else alert(t('failed_to_create_enterprise_unknown')) ) + callback.call() if callback? update: (step) => Loading.message = t('updating') + " " + @enterprise.name diff --git a/app/assets/javascripts/darkswarm/services/navigation.js.coffee b/app/assets/javascripts/darkswarm/services/navigation.js.coffee index e2b04f3ad1..f511e29e0c 100644 --- a/app/assets/javascripts/darkswarm/services/navigation.js.coffee +++ b/app/assets/javascripts/darkswarm/services/navigation.js.coffee @@ -21,3 +21,6 @@ Darkswarm.factory 'Navigation', ($location, $window) -> $window.location.href = path else $window.location.pathname = path + + reload: -> + $window.location.reload() diff --git a/app/assets/javascripts/darkswarm/services/orders.js.coffee b/app/assets/javascripts/darkswarm/services/orders.js.coffee index 049c3baa5e..2e44f4010a 100644 --- a/app/assets/javascripts/darkswarm/services/orders.js.coffee +++ b/app/assets/javascripts/darkswarm/services/orders.js.coffee @@ -1,22 +1,25 @@ -Darkswarm.factory 'Orders', (orders_by_distributor, currencyConfig, CurrentHub, Taxons, Dereferencer, visibleFilter, Matcher, Geo, $rootScope)-> +Darkswarm.factory 'Orders', (orders, shops, currencyConfig)-> new class Orders + all: orders + changeable: [] + shops: shops + shopsByID: {} + currencySymbol = currencyConfig.symbol + constructor: -> - # Populate Orders.orders from json in page. - @orders_by_distributor = orders_by_distributor - @changeable_orders = [] - @currency_symbol = currencyConfig.symbol + for shop in @shops + shop.orders = [] + shop.balance = 0.0 + @shopsByID[shop.id] = shop - for distributor in @orders_by_distributor - @findChangeableOrders(distributor.distributed_orders) - @updateRunningBalance(distributor.distributed_orders) + for order in @all by -1 + shop = @shopsByID[order.shop_id] + shop.orders.unshift order + @changeable.unshift(order) if order.changes_allowed - updateRunningBalance: (orders) -> - for order, i in orders - balances = orders.slice(i,orders.length).map (o) -> parseFloat(o.outstanding_balance) - running_balance = balances.reduce (a,b) -> a+b - order.running_balance = running_balance.toFixed(2) + @updateRunningBalance(shop, order) - findChangeableOrders: (orders) -> - for order in orders when order.changes_allowed - @changeable_orders.push(order) + updateRunningBalance: (shop, order) -> + shop.balance += parseFloat(order.outstanding_balance) + order.runningBalance = shop.balance.toFixed(2) diff --git a/app/assets/javascripts/darkswarm/services/rails_flash_loader.js.coffee b/app/assets/javascripts/darkswarm/services/rails_flash_loader.js.coffee index 9398b5f5cc..4ef72122a7 100644 --- a/app/assets/javascripts/darkswarm/services/rails_flash_loader.js.coffee +++ b/app/assets/javascripts/darkswarm/services/rails_flash_loader.js.coffee @@ -1,7 +1,18 @@ Darkswarm.factory 'RailsFlashLoader', (flash, railsFlash)-> new class RailsFlashLoader + # The 'flash' service requires type key to + # be one of: success, info, warn, error + typePairings: + success: 'success' + error: 'error' + notice: 'success' + info: 'info' + warn: 'warn' + initFlash: -> @loadFlash railsFlash + loadFlash: (rails_flash)-> for type, message of rails_flash + type = @typePairings[type] flash[type] = message diff --git a/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee b/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee new file mode 100644 index 0000000000..1b0db2dcc9 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/stripe_elements.js.coffee @@ -0,0 +1,47 @@ +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 + requestToken: (secrets, submit, loading_message = t("processing_payment")) -> + return unless @stripe? && @card? + + Loading.message = loading_message + cardData = @makeCardData(secrets) + + @stripe.createToken(@card, cardData).then (response) => + if(response.error) + Loading.clear() + RailsFlashLoader.loadFlash({error: t("error") + ": #{response.error.message}"}) + else + secrets.token = response.token.id + secrets.cc_type = @mapCC(response.token.card.brand) + secrets.card = response.token.card + submit() + + # Maps the brand returned by Stripe to that required by activemerchant + mapCC: (ccType) -> + if ccType == 'MasterCard' + return 'master' + else if ccType == 'Visa' + return 'visa' + else if ccType == 'American Express' + return 'american_express' + else if ccType == 'Discover' + return 'discover' + else if ccType == 'JCB' + return 'jcb' + else if ccType == 'Diners Club' + return 'diners_club' + return + + # It doesn't matter if any of these are nil, all are optional. + makeCardData: (secrets) -> + {'name': secrets.name, + 'address1': secrets.address1, + 'city': secrets.city, + 'zipcode': secrets.zipcode} diff --git a/app/assets/javascripts/darkswarm/services/tabsets.js.coffee b/app/assets/javascripts/darkswarm/services/tabsets.js.coffee new file mode 100644 index 0000000000..9c24c48485 --- /dev/null +++ b/app/assets/javascripts/darkswarm/services/tabsets.js.coffee @@ -0,0 +1,22 @@ +Darkswarm.factory 'Tabsets', -> + new class Tabsets + tabsets: [] + + register: (ctrl, id, selected=null) -> + if ctrl? && id? + @tabsets.push { ctrl: ctrl, id: id, selected: selected } + ctrl.select(selected) if selected? + + toggle: (id, name, state=null) -> + tabset = @findTabsetByObject(id) + if tabset.selected == name + @select(tabset, null) unless state == "open" + else + @select(tabset, name) unless state == "closed" + + select: (tabset, name) -> + tabset.selected = name + tabset.ctrl.select(name) + + findTabsetByObject: (id) -> + (tabset for tabset in @tabsets when tabset.id == id)[0] diff --git a/app/assets/javascripts/store/all.js b/app/assets/javascripts/store/all.js index 723f03af31..a8386e4b7f 100644 --- a/app/assets/javascripts/store/all.js +++ b/app/assets/javascripts/store/all.js @@ -6,8 +6,7 @@ // //= require 'jquery' -//= require store/spree_core +//= require store/spree_frontend //= require store/spree_auth -//= require store/spree_promo //= require_tree . diff --git a/app/assets/stylesheets/admin/all.css b/app/assets/stylesheets/admin/all.css index e913e9e710..47bf928477 100644 --- a/app/assets/stylesheets/admin/all.css +++ b/app/assets/stylesheets/admin/all.css @@ -4,9 +4,8 @@ * the top of the compiled file, but it's generally better to create a new file per style scope. * - *= require admin/spree_core + *= require admin/spree_backend *= require admin/spree_auth - *= require admin/spree_promo *= require shared/jquery-ui-timepicker-addon *= require shared/textAngular diff --git a/app/assets/stylesheets/admin/components/alert-box.css.scss b/app/assets/stylesheets/admin/components/alert-box.css.scss new file mode 100644 index 0000000000..dcd89adf25 --- /dev/null +++ b/app/assets/stylesheets/admin/components/alert-box.css.scss @@ -0,0 +1,80 @@ +@import "../../darkswarm/mixins"; + +.alert-box { + position: relative; + display: block; + background-color: #eff5dc; + border: 1px solid #9fc820; + color: #666; + margin-top: 1em; + margin-bottom: 1em; + + @include border-radius(3px); + + transition: opacity 300ms ease-out; + padding: 0.77778em 1.33333em 0.77778em 0.77778em; + + a.close { + position: absolute; + right: 5px; + top: 5px; + font-size: 1.5em; + } + + &.ok { + border: 1px solid #9fc820; + background-color: #fbffee; + color: #9fc820; + font-weight: bold; + + a.button { + padding: 3px 10px; + background-color: #a7c44d; + &:hover { + background-color: #9fc820; + } + } + + a.close { + color: #9fc820; + } + } + + &.error { + border: 1px solid #c82020; + background-color: #f5dcdc; + color: #c82020; + font-weight: bold; + + a.button { + padding: 3px 10px; + background-color: #c85252; + &:hover { + background-color: #c82020; + } + } + + a.close { + color: #c82020; + } + } + + &.warning { + border: 1px solid #e6912e; + background-color: #fff4e6; + color: #e6912e; + font-weight: bold; + + a.button { + padding: 3px 10px; + background-color: #db9350; + &:hover { + background-color: #e6912e; + } + } + + a.close { + color: #e6912e; + } + } +} diff --git a/app/assets/stylesheets/admin/components/jquery_dialog.scss b/app/assets/stylesheets/admin/components/jquery_dialog.scss index f3fe08da60..13e2f87bda 100644 --- a/app/assets/stylesheets/admin/components/jquery_dialog.scss +++ b/app/assets/stylesheets/admin/components/jquery_dialog.scss @@ -24,8 +24,6 @@ light: #ccc } } -/*.ui-dialog .ui-icon-closethick{background:url(/static/assets/dialogCloseButton.png);}*/ - .ui-dialog .ui-widget-header{ background-image: none; background-color: #ffffff; @@ -42,21 +40,18 @@ light: #ccc .ui-dialog .ui-corner-all{ border-radius: 8px; } -.ui-dialog { - .ui-state-hover, .ui-state-focus{ - border: none; - background: none; - color: #545454; - } -} .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-widget-content .ui-state-hover { background-color: #ffffff; background: none; } -.ui-dialog-titlebar-close { +.ui-dialog .ui-dialog-titlebar .ui-dialog-titlebar-close { float: right; + + border: none; + background: none; + &:before { color: #000000; font-size: 2em; @@ -76,9 +71,18 @@ light: #ccc display: none; } } + + .ui-button-text { + display: none; + } } .ui-widget-overlay { background: #000000; opacity: 0.5; + position: fixed; + height: 100vh; + width: 100vw; + left: 0px; + top: 0px; } diff --git a/app/assets/stylesheets/admin/components/stripe_connect_button.css.scss b/app/assets/stylesheets/admin/components/stripe_connect_button.css.scss new file mode 100644 index 0000000000..5dcc8d8cdd --- /dev/null +++ b/app/assets/stylesheets/admin/components/stripe_connect_button.css.scss @@ -0,0 +1,73 @@ +a.stripe-connect { + display: inline-block; + margin-bottom: 1px; + background-image: linear-gradient(#28a0e5, #015e94); + border: 0; + padding: 1px; + height: 30px; + text-decoration: none; + border-radius: 4px; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); + cursor: pointer; + user-select: none; + span { + display: block; + position: relative; + padding: 0 12px 0 44px; + height: 30px; + background: #1275ff; + background-image: linear-gradient(#7dc5ee, #008cdd 85%, #30a2e4); + font-size: 14px; + line-height: 30px; + color: white; + font-weight: bold; + font-family: Helvetica Neue, Helvetica, Arial, sans-serif; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); + border-radius: 3px; + &:before { + content: ''; + display: block; + position: absolute; + left: 11px; + top: 50%; + width: 23px; + height: 24px; + margin-top: -12px; + background-repeat: no-repeat; + background-size: 23px 24px; + background-image: url("data:image/png;base64,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"); + } + } + &:active { + background: #005d93; + span { + color: #eee; + background: #008cdd; + background-image: linear-gradient(#008cdd, #008cdd 85%, #239adf); + //Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); + &:before { + } + } + } + &.blue span:before { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAYCAYAAAARfGZ1AAAKRGlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUFNcXx9/MbC+0XZYiZem9twWkLr1IlSYKy+4CS1nWZRewN0QFIoqICFYkKGLAaCgSK6JYCAgW7AEJIkoMRhEVlczGHPX3Oyf5/U7eH3c+8333nnfn3vvOGQAoASECYQ6sAEC2UCKO9PdmxsUnMPG9AAZEgAM2AHC4uaLQKL9ogK5AXzYzF3WS8V8LAuD1LYBaAK5bBIQzmX/p/+9DkSsSSwCAwtEAOx4/l4tyIcpZ+RKRTJ9EmZ6SKWMYI2MxmiDKqjJO+8Tmf/p8Yk8Z87KFPNRHlrOIl82TcRfKG/OkfJSREJSL8gT8fJRvoKyfJc0WoPwGZXo2n5MLAIYi0yV8bjrK1ihTxNGRbJTnAkCgpH3FKV+xhF+A5gkAO0e0RCxIS5cwjbkmTBtnZxYzgJ+fxZdILMI53EyOmMdk52SLOMIlAHz6ZlkUUJLVlokW2dHG2dHRwtYSLf/n9Y+bn73+GWS9/eTxMuLPnkGMni/al9gvWk4tAKwptDZbvmgpOwFoWw+A6t0vmv4+AOQLAWjt++p7GLJ5SZdIRC5WVvn5+ZYCPtdSVtDP6386fPb8e/jqPEvZeZ9rx/Thp3KkWRKmrKjcnKwcqZiZK+Jw+UyL/x7ifx34VVpf5WEeyU/li/lC9KgYdMoEwjS03UKeQCLIETIFwr/r8L8M+yoHGX6aaxRodR8BPckSKPTRAfJrD8DQyABJ3IPuQJ/7FkKMAbKbF6s99mnuUUb3/7T/YeAy9BXOFaQxZTI7MprJlYrzZIzeCZnBAhKQB3SgBrSAHjAGFsAWOAFX4Al8QRAIA9EgHiwCXJAOsoEY5IPlYA0oAiVgC9gOqsFeUAcaQBM4BtrASXAOXARXwTVwE9wDQ2AUPAOT4DWYgSAID1EhGqQGaUMGkBlkC7Egd8gXCoEioXgoGUqDhJAUWg6tg0qgcqga2g81QN9DJ6Bz0GWoH7oDDUPj0O/QOxiBKTAd1oQNYSuYBXvBwXA0vBBOgxfDS+FCeDNcBdfCR+BW+Bx8Fb4JD8HP4CkEIGSEgeggFggLYSNhSAKSioiRlUgxUonUIk1IB9KNXEeGkAnkLQaHoWGYGAuMKyYAMx/DxSzGrMSUYqoxhzCtmC7MdcwwZhLzEUvFamDNsC7YQGwcNg2bjy3CVmLrsS3YC9ib2FHsaxwOx8AZ4ZxwAbh4XAZuGa4UtxvXjDuL68eN4KbweLwa3gzvhg/Dc/ASfBF+J/4I/gx+AD+Kf0MgE7QJtgQ/QgJBSFhLqCQcJpwmDBDGCDNEBaIB0YUYRuQRlxDLiHXEDmIfcZQ4Q1IkGZHcSNGkDNIaUhWpiXSBdJ/0kkwm65KdyRFkAXk1uYp8lHyJPEx+S1GimFLYlESKlLKZcpBylnKH8pJKpRpSPakJVAl1M7WBep76kPpGjiZnKRcox5NbJVcj1yo3IPdcnihvIO8lv0h+qXyl/HH5PvkJBaKCoQJbgaOwUqFG4YTCoMKUIk3RRjFMMVuxVPGw4mXFJ0p4JUMlXyWeUqHSAaXzSiM0hKZHY9O4tHW0OtoF2igdRzeiB9Iz6CX07+i99EllJWV75RjlAuUa5VPKQwyEYcgIZGQxyhjHGLcY71Q0VbxU+CqbVJpUBlSmVeeoeqryVYtVm1Vvqr5TY6r5qmWqbVVrU3ugjlE3VY9Qz1ffo35BfWIOfY7rHO6c4jnH5tzVgDVMNSI1lmkc0OjRmNLU0vTXFGnu1DyvOaHF0PLUytCq0DqtNa5N03bXFmhXaJ/RfspUZnoxs5hVzC7mpI6GToCOVGe/Tq/OjK6R7nzdtbrNug/0SHosvVS9Cr1OvUl9bf1Q/eX6jfp3DYgGLIN0gx0G3QbThkaGsYYbDNsMnxipGgUaLTVqNLpvTDX2MF5sXGt8wwRnwjLJNNltcs0UNnUwTTetMe0zg80czQRmu836zbHmzuZC81rzQQuKhZdFnkWjxbAlwzLEcq1lm+VzK32rBKutVt1WH60drLOs66zv2SjZBNmstemw+d3W1JZrW2N7w45q52e3yq7d7oW9mT3ffo/9bQeaQ6jDBodOhw+OTo5ixybHcSd9p2SnXU6DLDornFXKuuSMdfZ2XuV80vmti6OLxOWYy2+uFq6Zroddn8w1msufWzd3xE3XjeO2323Ineme7L7PfchDx4PjUevxyFPPk+dZ7znmZeKV4XXE67m3tbfYu8V7mu3CXsE+64P4+PsU+/T6KvnO9632fein65fm1+g36e/gv8z/bAA2IDhga8BgoGYgN7AhcDLIKWhFUFcwJTgquDr4UYhpiDikIxQODQrdFnp/nsE84by2MBAWGLYt7EG4Ufji8B8jcBHhETURjyNtIpdHdkfRopKiDke9jvaOLou+N994vnR+Z4x8TGJMQ8x0rE9seexQnFXcirir8erxgvj2BHxCTEJ9wtQC3wXbF4wmOiQWJd5aaLSwYOHlReqLshadSpJP4iQdT8YmxyYfTn7PCePUcqZSAlN2pUxy2dwd3Gc8T14Fb5zvxi/nj6W6pZanPklzS9uWNp7ukV6ZPiFgC6oFLzICMvZmTGeGZR7MnM2KzWrOJmQnZ58QKgkzhV05WjkFOf0iM1GRaGixy+LtiyfFweL6XCh3YW67hI7+TPVIjaXrpcN57nk1eW/yY/KPFygWCAt6lpgu2bRkbKnf0m+XYZZxl3Uu11m+ZvnwCq8V+1dCK1NWdq7SW1W4anS1/+pDa0hrMtf8tNZ6bfnaV+ti13UUahauLhxZ77++sUiuSFw0uMF1w96NmI2Cjb2b7Dbt3PSxmFd8pcS6pLLkfSm39Mo3Nt9UfTO7OXVzb5lj2Z4tuC3CLbe2emw9VK5YvrR8ZFvottYKZkVxxavtSdsvV9pX7t1B2iHdMVQVUtW+U3/nlp3vq9Orb9Z41zTv0ti1adf0bt7ugT2ee5r2au4t2ftun2Df7f3++1trDWsrD+AO5B14XBdT1/0t69uGevX6kvoPB4UHhw5FHupqcGpoOKxxuKwRbpQ2jh9JPHLtO5/v2pssmvY3M5pLjoKj0qNPv0/+/tax4GOdx1nHm34w+GFXC62luBVqXdI62ZbeNtQe395/IuhEZ4drR8uPlj8ePKlzsuaU8qmy06TThadnzyw9M3VWdHbiXNq5kc6kznvn487f6Iro6r0QfOHSRb+L57u9us9ccrt08rLL5RNXWFfarjpebe1x6Gn5yeGnll7H3tY+p772a87XOvrn9p8e8Bg4d93n+sUbgTeu3px3s//W/Fu3BxMHh27zbj+5k3Xnxd28uzP3Vt/H3i9+oPCg8qHGw9qfTX5uHnIcOjXsM9zzKOrRvRHuyLNfcn95P1r4mPq4ckx7rOGJ7ZOT437j154ueDr6TPRsZqLoV8Vfdz03fv7Db56/9UzGTY6+EL+Y/b30pdrLg6/sX3VOhU89fJ39ema6+I3am0NvWW+738W+G5vJf49/X/XB5EPHx+CP92ezZ2f/AAOY8/wRDtFgAAADQklEQVRIDbWVaUiUQRjHZ96dXY/d1fYQj1U03dJSw9YkFgy6DIkILRArQSSC7PjQjQQqVH7oQ0GHQUWgpQhKHzoNSqiUwpXcsrwIjzVtPVrzbPV9Z6bZhYV3N3WXYAeGmWeeZ37z8J95GEgpBf5oeXn1Es4fYAdzPDlM6je4RBYhR+LMU89UxiCBGiCgkUwsBYSA+SlPKLQBQAYEAZm+3j42K96z3NyOF7VOeMrp62opRcacjPW5+43rDTpNSKQ8QKZAEg7xmPCTs/O27uGJgXuNbW0pxyvLfTmAEBzthEsFZLxRvPdi5rpYo2cmUiQJDA4IVeo0obGdlvGfXUPj0Sym2zPuHxvzcWjDyVupJ/YYizKTGNjLw/HiduNTAqIRIUJ6Vpp+ky8bCSFgwQ2xgkGxFi1ioNWEBGuJB31gbLIv/2pd7SpFoGxtpCYkLSEq4ptlzIYFO7tc7w0TKkeEYg5ADnrWkkYhD8s26GPq3nW0WKxTptftPYBI4Mj3O2fHvKNZBMVSDmMwarXNjDkSF3d5kExZeiCr8M2VI+VFu9IvsPcYtzAvkfoEZkEEE45jMppq3ppbCNPFIY1nD1cpo07lbMmvOXeoDCF8BLKy9uUAAjDkBh+c6bz78mNtVVP7MwET7JBnqb4xXpdWVpC1OVzWn+ELHLCsneX/s7rkRWl1463cy1U3WroG21jhCGKJXPOtKQnpAuENvsAppgDB3TcDVIrpDHbK5Kd+y7W8iodNybHh22rOHyxUK+UaMYjZaoyp25rYL54TSihSKmwZ14v3lc3ZFxdbeywjn/tGJnkmzrydX1ApxOEACKymmXLYfXVpi1JMEOGxPi1ep18doY4r2J7uFumQQ9yGf01bMcZW8dpyc0oIjxxpuC5wuUDX+ovWrnYeg3aXvdLIqnmOvXPsfH6uA5YbTb1DX8ofvTLzTy6ZV4K6fAw+gXiATfdffmjeaUgc1UdpdWplsCooQBrEnqUw82dhdnjit/Vxc4f59tP3DRjzJvYteqrl4rmNlJIfrOwpgNklesDRNQBCHYtQAQqD2CgACNjHAJnG1EyfV/S67fZiJB5t2OGEe4n7L3fS4fpEv/2hUEATfoPbuam5v8N7nps70YTbAAAAAElFTkSuQmCC"); + } + &.light-blue span:before { + background-image: url("data:image/png;base64,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"); + } +} +@media only screen and(-webkit-min-device-pixel-ratio: 1.5), only screen and(min--moz-device-pixel-ratio: 1.5), only screen and(min-device-pixel-ratio: 1.5) { + .stripe-connect { + span:before { + background-image: url("data:image/png;base64,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"); + } + &.blue span:before { + background-image: url("data:image/png;base64,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"); + } + &.light-blue span:before { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAwCAYAAABuZUjcAAAKRGlDQ1BJQ0MgUHJvZmlsZQAASA2dlndUFNcXx9/MbC+0XZYiZem9twWkLr1IlSYKy+4CS1nWZRewN0QFIoqICFYkKGLAaCgSK6JYCAgW7AEJIkoMRhEVlczGHPX3Oyf5/U7eH3c+8333nnfn3vvOGQAoASECYQ6sAEC2UCKO9PdmxsUnMPG9AAZEgAM2AHC4uaLQKL9ogK5AXzYzF3WS8V8LAuD1LYBaAK5bBIQzmX/p/+9DkSsSSwCAwtEAOx4/l4tyIcpZ+RKRTJ9EmZ6SKWMYI2MxmiDKqjJO+8Tmf/p8Yk8Z87KFPNRHlrOIl82TcRfKG/OkfJSREJSL8gT8fJRvoKyfJc0WoPwGZXo2n5MLAIYi0yV8bjrK1ihTxNGRbJTnAkCgpH3FKV+xhF+A5gkAO0e0RCxIS5cwjbkmTBtnZxYzgJ+fxZdILMI53EyOmMdk52SLOMIlAHz6ZlkUUJLVlokW2dHG2dHRwtYSLf/n9Y+bn73+GWS9/eTxMuLPnkGMni/al9gvWk4tAKwptDZbvmgpOwFoWw+A6t0vmv4+AOQLAWjt++p7GLJ5SZdIRC5WVvn5+ZYCPtdSVtDP6386fPb8e/jqPEvZeZ9rx/Thp3KkWRKmrKjcnKwcqZiZK+Jw+UyL/x7ifx34VVpf5WEeyU/li/lC9KgYdMoEwjS03UKeQCLIETIFwr/r8L8M+yoHGX6aaxRodR8BPckSKPTRAfJrD8DQyABJ3IPuQJ/7FkKMAbKbF6s99mnuUUb3/7T/YeAy9BXOFaQxZTI7MprJlYrzZIzeCZnBAhKQB3SgBrSAHjAGFsAWOAFX4Al8QRAIA9EgHiwCXJAOsoEY5IPlYA0oAiVgC9gOqsFeUAcaQBM4BtrASXAOXARXwTVwE9wDQ2AUPAOT4DWYgSAID1EhGqQGaUMGkBlkC7Egd8gXCoEioXgoGUqDhJAUWg6tg0qgcqga2g81QN9DJ6Bz0GWoH7oDDUPj0O/QOxiBKTAd1oQNYSuYBXvBwXA0vBBOgxfDS+FCeDNcBdfCR+BW+Bx8Fb4JD8HP4CkEIGSEgeggFggLYSNhSAKSioiRlUgxUonUIk1IB9KNXEeGkAnkLQaHoWGYGAuMKyYAMx/DxSzGrMSUYqoxhzCtmC7MdcwwZhLzEUvFamDNsC7YQGwcNg2bjy3CVmLrsS3YC9ib2FHsaxwOx8AZ4ZxwAbh4XAZuGa4UtxvXjDuL68eN4KbweLwa3gzvhg/Dc/ASfBF+J/4I/gx+AD+Kf0MgE7QJtgQ/QgJBSFhLqCQcJpwmDBDGCDNEBaIB0YUYRuQRlxDLiHXEDmIfcZQ4Q1IkGZHcSNGkDNIaUhWpiXSBdJ/0kkwm65KdyRFkAXk1uYp8lHyJPEx+S1GimFLYlESKlLKZcpBylnKH8pJKpRpSPakJVAl1M7WBep76kPpGjiZnKRcox5NbJVcj1yo3IPdcnihvIO8lv0h+qXyl/HH5PvkJBaKCoQJbgaOwUqFG4YTCoMKUIk3RRjFMMVuxVPGw4mXFJ0p4JUMlXyWeUqHSAaXzSiM0hKZHY9O4tHW0OtoF2igdRzeiB9Iz6CX07+i99EllJWV75RjlAuUa5VPKQwyEYcgIZGQxyhjHGLcY71Q0VbxU+CqbVJpUBlSmVeeoeqryVYtVm1Vvqr5TY6r5qmWqbVVrU3ugjlE3VY9Qz1ffo35BfWIOfY7rHO6c4jnH5tzVgDVMNSI1lmkc0OjRmNLU0vTXFGnu1DyvOaHF0PLUytCq0DqtNa5N03bXFmhXaJ/RfspUZnoxs5hVzC7mpI6GToCOVGe/Tq/OjK6R7nzdtbrNug/0SHosvVS9Cr1OvUl9bf1Q/eX6jfp3DYgGLIN0gx0G3QbThkaGsYYbDNsMnxipGgUaLTVqNLpvTDX2MF5sXGt8wwRnwjLJNNltcs0UNnUwTTetMe0zg80czQRmu836zbHmzuZC81rzQQuKhZdFnkWjxbAlwzLEcq1lm+VzK32rBKutVt1WH60drLOs66zv2SjZBNmstemw+d3W1JZrW2N7w45q52e3yq7d7oW9mT3ffo/9bQeaQ6jDBodOhw+OTo5ixybHcSd9p2SnXU6DLDornFXKuuSMdfZ2XuV80vmti6OLxOWYy2+uFq6Zroddn8w1msufWzd3xE3XjeO2323Ineme7L7PfchDx4PjUevxyFPPk+dZ7znmZeKV4XXE67m3tbfYu8V7mu3CXsE+64P4+PsU+/T6KvnO9632fein65fm1+g36e/gv8z/bAA2IDhga8BgoGYgN7AhcDLIKWhFUFcwJTgquDr4UYhpiDikIxQODQrdFnp/nsE84by2MBAWGLYt7EG4Ufji8B8jcBHhETURjyNtIpdHdkfRopKiDke9jvaOLou+N994vnR+Z4x8TGJMQ8x0rE9seexQnFXcirir8erxgvj2BHxCTEJ9wtQC3wXbF4wmOiQWJd5aaLSwYOHlReqLshadSpJP4iQdT8YmxyYfTn7PCePUcqZSAlN2pUxy2dwd3Gc8T14Fb5zvxi/nj6W6pZanPklzS9uWNp7ukV6ZPiFgC6oFLzICMvZmTGeGZR7MnM2KzWrOJmQnZ58QKgkzhV05WjkFOf0iM1GRaGixy+LtiyfFweL6XCh3YW67hI7+TPVIjaXrpcN57nk1eW/yY/KPFygWCAt6lpgu2bRkbKnf0m+XYZZxl3Uu11m+ZvnwCq8V+1dCK1NWdq7SW1W4anS1/+pDa0hrMtf8tNZ6bfnaV+ti13UUahauLhxZ77++sUiuSFw0uMF1w96NmI2Cjb2b7Dbt3PSxmFd8pcS6pLLkfSm39Mo3Nt9UfTO7OXVzb5lj2Z4tuC3CLbe2emw9VK5YvrR8ZFvottYKZkVxxavtSdsvV9pX7t1B2iHdMVQVUtW+U3/nlp3vq9Orb9Z41zTv0ti1adf0bt7ugT2ee5r2au4t2ftun2Df7f3++1trDWsrD+AO5B14XBdT1/0t69uGevX6kvoPB4UHhw5FHupqcGpoOKxxuKwRbpQ2jh9JPHLtO5/v2pssmvY3M5pLjoKj0qNPv0/+/tax4GOdx1nHm34w+GFXC62luBVqXdI62ZbeNtQe395/IuhEZ4drR8uPlj8ePKlzsuaU8qmy06TThadnzyw9M3VWdHbiXNq5kc6kznvn487f6Iro6r0QfOHSRb+L57u9us9ccrt08rLL5RNXWFfarjpebe1x6Gn5yeGnll7H3tY+p772a87XOvrn9p8e8Bg4d93n+sUbgTeu3px3s//W/Fu3BxMHh27zbj+5k3Xnxd28uzP3Vt/H3i9+oPCg8qHGw9qfTX5uHnIcOjXsM9zzKOrRvRHuyLNfcn95P1r4mPq4ckx7rOGJ7ZOT437j154ueDr6TPRsZqLoV8Vfdz03fv7Db56/9UzGTY6+EL+Y/b30pdrLg6/sX3VOhU89fJ39ema6+I3am0NvWW+738W+G5vJf49/X/XB5EPHx+CP92ezZ2f/AAOY8/wRDtFgAAAHH0lEQVRoBdVZ628UVRS/857dme3strvblpaXCiI+WkCkpFAoECAgr0oqxASjiAZMiF9MiI80/AfqB+WD3/xABOMrKCgRJCBSLCACQUEIEai8ywJ97GNm/J3ZbizM7C7trpG9m7N39t5z7/2dM+eec+5dzrZtVoqFL0XQhLlkgYulpnGOYxxhLjngW7Zsdayk5IB3RyJSSWrcMP1aSQJPJfnwoIA3LFhTy3hrAdx+IzbIOMbsGkQAR3pM1Icdcxv1ZZtxf+D5OGPm3vbJo4/YbW0WLVSswglCLc3F5QtAzyx6ZbbA7Hc5jp8hCAIj4nmecTy2NyRwCqShOEZzWZbFTMtkpmky27Ku2Da36cC2j9vSjIV/b93RsZpmybo5n2htlct6yz6SReFlWZaZIitMURRGz6IkMoEXHPAOFAewnQacSrFkMsUSiTgoEU0kk4vBUzTgHM87GvcE3traKgTjxleyT5mvaTrTdY2pqo9JBNjReBp0v0sFLtI4tA2ClqFtIpPF43EIEdcd4Yr0hSWy23hnIvi2T/PPDwaDLBAIMFVRmSACbMY0XCDSImTCsOOvYDr0hqxUQnGxF9AA4/T2Ks2LXwsD9Iby8nIWNIJMVmTGZwWcAwFW4AWIYmfEycE7mC6OZfHjqviCYZT5gobhaIw24VALjRz6aO9Vsdm9I6eu6XN1mIcC8+ALAO0sS28qvY43iiG0csxydOHanJqm1ZFNk8vLp67hVeHjLfMbvx9ZHY7Fbvco17pi2vlL1youXemKXLh8Y8SV610jelPJIcDLP8QFXJHlELm77BsxPaltW6xx4vgDo2uiN6klZOh9RGNG1VzHz1Ogn6j99LkLcaqLXVzA4acRnIS82k6lTLbjx/aqhgmPvglQMZAMItcXAkVAw4nGjKq9hbroxQVcVeVenuN9//po7zUpQp44ffbZOSvWb48nEhv3fr5pBzhJu6TxP0E/g6iUpavifrt8VUXIuEC27eyrHDVFTtoLiqo2SKK4vem5tQebWl5dwW3ceO+c/4nG712EwUaPIhDmRU5RtMwoY5FwhIXg83VNmyxJ6uamY5ePNbWsXVFc/bpncwFfMnvqN4oi3iRTyfXh+zVO0bUyGmXRykpWXkEC6ONlWdo8c/m6L+atWpXJHt0rF9jiAq7rvpPzGuu/hqlYjjskr5mFKDiRB/Ijtw8FQywaibJKCEBvwOf3L032lf0wbcnqQIEYPYe7gIPrRPPU+kONk8Z/jVAPb38fH0gpiiLA+lgwaDgCRMJhJGf6FFXV3vNcucBGL+Am5ty2dM6UjkWzp3ziU+Vb+TZqpp9yGhLADwFCoXKYTgVD3vPSrBXr6wrE6RruBZyYzoK+nT7psdMb1rS8P+Hxh3bKstiT19X0S4CcGSmDzAzkO9gDHHL5510rF9jg8uMD5juC55jfry5aubBpb+xOz8Fd+3+rO3bqr6ndvX0VA/i8HyEEHT4CeoAl4/GFYHrLm3Fordk0npmNNP8haJeh+7uWzW04+M665R9MmzT+S0kU+jImkq2mJE1RFab6fA9nJixWnUvjmTUoS6K84xfQU0i+piya9fRhjrftfR2/L3M8TobToxYFEScnqehu0QW8ufX1eoGXJPNy6Mju3W2pAVgSeO4AHQLV+SR5pIVES+CQ1+QolPeoqlr0RMsFXJTkpXDbbVxVV/eclW+04wjTDod4HGe907aQuiImOV7RfbXVVdWNeqCMCUpu4ORM4Zl6csg2pC4X8GHRsNbdl6BrBs1MpWbh4DuLrhvoEGzZODVJHA7GPOuLJ5iG0ELAchUcn5mh63/n4hlKnwt4bW11uCvW65x+cLXAkgkQDgMpXDtQRkhAydXRKQnJVTqq5liZTv/V0dDJHCyD6rIZT5mU+15Fgk36/X7n/oQ0beGawQTgtMZxT4UP2a1zt4I6n8bxPlLNU+u+GxS6HMwch43lBZzu+tHpXPaIPDRKWi2gPDKi6sDo2sqjBUxx91CbOWdBN6r+hCqfJu+ezfuXEfCdX7lw+k70nvDmGHwr7KSbRrmA9+POa7v5lgwHA2debJn5KSIvxQBnsXxj7qcfwe4a8bmAD4tWnLp6s7uzN2lWw33kdhkeK/lUpat+3Kg9C2ZMPIzuC6A9HmxDbsJeozndwNesXLCf2mO376gnz3TW4Jph2I3Y7cidnr7ynt54MJky/ZZli8jFTZHnE7Ikdmt+9Ua0wjg/bvSwM0+OHXER0ZV2PqULn4EGBjH8LKzgJH+OZnBpHG3kczuNgF7dUD/2DJ6JBlO6wLwP9OtgBt0vr22a3hrHBHQnQkSXlTWgahBlg+WgIMgHIoEpb6cdTvZ7A3QRRFruBDm+FnXRiyhZ3jY+YCXKLwgI0QNTYkKPt1d5YBBmAaJdver48bx/pWQZ/781wx06nq7kgGc0lu8ElOF74OqSBf4P9hj31KSAw4AAAAAASUVORK5CYII="); + } + } +} diff --git a/app/assets/stylesheets/admin/dashboard-single-ent.css.scss b/app/assets/stylesheets/admin/dashboard-single-ent.css.scss index b9aca09481..e2f1926c6e 100644 --- a/app/assets/stylesheets/admin/dashboard-single-ent.css.scss +++ b/app/assets/stylesheets/admin/dashboard-single-ent.css.scss @@ -1,27 +1,5 @@ @import "../darkswarm/mixins"; -.alert-box { - position: relative; - display: block; - background-color: #eff5dc; - border: 1px solid #9fc820; - color: #666; - margin-top: 1em; - margin-bottom: 1em; - - @include border-radius(3px); - - transition: opacity 300ms ease-out; - padding: 0.77778em 1.33333em 0.77778em 0.77778em; - - a.close { - position: absolute; - right: 5px; - top: 0px; - font-size: 1.5em; - } -} - .dashboard_item.single-ent { .header { padding: 0.77778em 1.33333em 0.77778em 0.77778em; diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index df55d3065b..4c520108a9 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -43,6 +43,13 @@ input.red { margin-right: 5px; } +a.button.red { + &:not(:hover) { + color: #fff; + background-color: #DA5354; + } +} + input.orange { background-color: #FF9848; margin-right: 5px; diff --git a/app/assets/stylesheets/admin/products.css.scss b/app/assets/stylesheets/admin/products.css.scss index b14dadd866..79854e4c33 100644 --- a/app/assets/stylesheets/admin/products.css.scss +++ b/app/assets/stylesheets/admin/products.css.scss @@ -69,4 +69,10 @@ table#listing_products.bulk { margin-bottom: 0.5em; } } + + td.left-actions { + a.view-variants, a.add-variant { + cursor: pointer; + } + } } diff --git a/app/assets/stylesheets/darkswarm/account.css.scss b/app/assets/stylesheets/darkswarm/account.css.scss index 4c60033a2a..c41cd2ef98 100644 --- a/app/assets/stylesheets/darkswarm/account.css.scss +++ b/app/assets/stylesheets/darkswarm/account.css.scss @@ -5,9 +5,32 @@ color: #4a4a4a; } +.credit_cards { + .saved_cards { + table { + width: 100%; + } + } + + .saved_cards, .no_cards { + margin-bottom: 1em; + } + + .new_card { + opacity: 0; + -webkit-transition: opacity 0.4s linear; + transition: opacity 0.4s linear; + &.visible { + opacity: 1; + } + + input.ng-invalid { + margin-bottom: 0px; + } + } +} .orders { - margin-top: 50px; margin-bottom: 100px; a { @@ -24,6 +47,10 @@ height: auto; } + &.active_table { + margin: 0px; + } + .active_table_row { h3 { margin-top: 0.5em; diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass index f13d2d1bc6..ff18863259 100644 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -81,3 +81,6 @@ checkout display: inline span.accordion-down display: none + + .error + color: #c82020 diff --git a/app/assets/stylesheets/darkswarm/stripe-elements.css.scss b/app/assets/stylesheets/darkswarm/stripe-elements.css.scss new file mode 100644 index 0000000000..5ef854746a --- /dev/null +++ b/app/assets/stylesheets/darkswarm/stripe-elements.css.scss @@ -0,0 +1,16 @@ +stripe-elements { + margin-bottom: 15px; + display: block; + + #card-element { + background: white; + box-sizing: border-box; + font-weight: 400; + padding: 0.6rem 0.5rem; + border: 1px solid #cccccc; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + border-radius: 0px; + height: 42px; + width: 100%; + } +} diff --git a/app/assets/stylesheets/darkswarm/tabset.css.scss b/app/assets/stylesheets/darkswarm/tabset.css.scss new file mode 100644 index 0000000000..eaefe188a8 --- /dev/null +++ b/app/assets/stylesheets/darkswarm/tabset.css.scss @@ -0,0 +1,71 @@ +@import "typography"; +@import "mixins"; +@import "branding"; + +.tabset-ctrl { + .tab-view { + padding-top: 30px; + } + + .tab { + text-align: center; + + @media all and (max-width: 640px) { + text-align: left; + } + + a { + @include headingFont; + + background: transparent; + text-transform: uppercase; + font-size: 1.5em; + text-shadow: 0 -1px 1px #ffffff; + padding: 1em; + border: none; + + @media all and (max-width: 640px) { + padding: 0.35em 0 0.65em 0; + text-shadow: none; + } + } + + border-bottom: 4px solid transparent; + + &:hover, &:focus, &:active { + transition: all 0.4s ease-in-out; + border-bottom: 4px solid $clr-brick-bright; + cursor: pointer; + + @media all and (max-width: 640px) { + transition: none; + color: white; + background-color: $clr-brick-bright; + } + + a { + color: $clr-brick-bright; + + @media all and (max-width: 640px) { + color: #ffffff; + } + } + } + + &.selected { + border-bottom: 4px solid $clr-brick; + + @media all and (max-width: 640px) { + background-color: $clr-brick; + } + + a { + color: $clr-brick; + + @media all and (max-width: 640px) { + color: #ffffff; + } + } + } + } +} diff --git a/app/assets/stylesheets/mail/email.css.scss b/app/assets/stylesheets/mail/email.css.scss index 0b2f641b4f..10a16941dd 100644 --- a/app/assets/stylesheets/mail/email.css.scss +++ b/app/assets/stylesheets/mail/email.css.scss @@ -408,3 +408,12 @@ ul { display: inline-block; margin: 0px; } + +/* + * Fix overlapping table header on second page of long invoices. + * Problem description: https://github.com/openfoodfoundation/openfoodnetwork/issues/1738 + * Solution: https://github.com/wkhtmltopdf/wkhtmltopdf/issues/1770#issuecomment-73530576 + */ +thead { display: table-header-group } +tfoot { display: table-row-group } +tr { page-break-inside: avoid } diff --git a/app/assets/stylesheets/store/all.css b/app/assets/stylesheets/store/all.css index c1d3d851bc..ea25eaeefc 100644 --- a/app/assets/stylesheets/store/all.css +++ b/app/assets/stylesheets/store/all.css @@ -4,9 +4,8 @@ * the top of the compiled file, but it's generally better to create a new file per style scope. * - *= require store/spree_core + *= require store/spree_frontend *= require store/spree_auth - *= require store/spree_promo *= require_self *= require_tree . diff --git a/app/controllers/admin/bulk_line_items_controller.rb b/app/controllers/admin/bulk_line_items_controller.rb new file mode 100644 index 0000000000..c0c1b60b91 --- /dev/null +++ b/app/controllers/admin/bulk_line_items_controller.rb @@ -0,0 +1,68 @@ +module Admin + class BulkLineItemsController < Spree::Admin::BaseController + # GET /admin/bulk_line_items.json + # + def index + order_params = params[:q].andand.delete :order + orders = OpenFoodNetwork::Permissions.new(spree_current_user).editable_orders.ransack(order_params).result + line_items = OpenFoodNetwork::Permissions.new(spree_current_user).editable_line_items.where(order_id: orders).ransack(params[:q]) + render_as_json line_items.result.reorder('order_id ASC, id ASC') + end + + # PUT /admin/bulk_line_items/:id.json + # + def update + load_line_item + authorize_update! + + # `with_lock` acquires an exclusive row lock on order so no other + # requests can update it until the transaction is commited. + # See https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/locking/pessimistic.rb#L69 + # and https://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE + order.with_lock do + if @line_item.update_attributes(params[:line_item]) + order.update_distribution_charge! + render nothing: true, status: 204 # No Content, does not trigger ng resource auto-update + else + render json: { errors: @line_item.errors }, status: 412 + end + end + end + + # DELETE /admin/bulk_line_items/:id.json + # + def destroy + load_line_item + authorize! :update, order + + @line_item.destroy + render nothing: true, status: 204 # No Content, does not trigger ng resource auto-update + end + + private + + def load_line_item + @line_item = Spree::LineItem.find(params[:id]) + end + + def model_class + Spree::LineItem + end + + # Returns the appropriate serializer for this controller + # + # @return [Api::Admin::LineItemSerializer] + def serializer(_ams_prefix) + Api::Admin::LineItemSerializer + end + + def authorize_update! + authorize! :update, order + authorize! :read, order + end + + def order + @line_item.order + end + end +end diff --git a/app/controllers/admin/stripe_accounts_controller.rb b/app/controllers/admin/stripe_accounts_controller.rb new file mode 100644 index 0000000000..e0a72c093c --- /dev/null +++ b/app/controllers/admin/stripe_accounts_controller.rb @@ -0,0 +1,49 @@ +require 'stripe/account_connector' + +module Admin + class StripeAccountsController < Spree::Admin::BaseController + def connect + payload = params.slice(:enterprise_id) + key = Openfoodnetwork::Application.config.secret_token + url_params = { state: JWT.encode(payload, key, 'HS256'), scope: "read_write" } + redirect_to Stripe::OAuth.authorize_url(url_params) + end + + def destroy + stripe_account = StripeAccount.find(params[:id]) + authorize! :destroy, stripe_account + + if stripe_account.deauthorize_and_destroy + flash[:success] = "Stripe account disconnected." + else + flash[:error] = "Failed to disconnect Stripe." + end + + redirect_to main_app.edit_admin_enterprise_path(stripe_account.enterprise) + rescue ActiveRecord::RecordNotFound + flash[:error] = "Failed to disconnect Stripe." + redirect_to spree.admin_path + end + + def status + return render json: { status: :stripe_disabled } unless Spree::Config.stripe_connect_enabled + stripe_account = StripeAccount.find_by_enterprise_id(params[:enterprise_id]) + return render json: { status: :account_missing } unless stripe_account + authorize! :status, stripe_account + + begin + status = Stripe::Account.retrieve(stripe_account.stripe_user_id) + attrs = %i[id business_name charges_enabled] + render json: status.to_hash.slice(*attrs).merge( status: :connected) + rescue Stripe::APIError + render json: { status: :access_revoked } + end + end + + private + + def model_class + StripeAccount + end + end +end diff --git a/app/controllers/admin/stripe_connect_settings_controller.rb b/app/controllers/admin/stripe_connect_settings_controller.rb new file mode 100644 index 0000000000..b4ab06d3b3 --- /dev/null +++ b/app/controllers/admin/stripe_connect_settings_controller.rb @@ -0,0 +1,40 @@ +# This controller is used by super admin users to update the settings the app is using + +module Admin + class StripeConnectSettingsController < Spree::Admin::BaseController + StripeConnectSettings = Struct.new(:stripe_connect_enabled) + + before_filter :load_settings, only: [:edit] + + def edit + return @stripe_account = { status: :empty_api_key } if Stripe.api_key.blank? + attrs = %i[id business_name charges_enabled] + @obfuscated_secret_key = obfuscated_secret_key + @stripe_account = Stripe::Account.retrieve.to_hash.slice(*attrs).merge(status: :ok) + rescue Stripe::AuthenticationError + @stripe_account = { status: :auth_fail } + end + + def update + Spree::Config.set(params[:settings]) + resource = t('admin.controllers.stripe_connect_settings.resource') + flash[:success] = t(:successfully_updated, :resource => resource) + redirect_to_edit + end + + private + + def load_settings + @settings = StripeConnectSettings.new(Spree::Config[:stripe_connect_enabled]) + end + + def redirect_to_edit + redirect_to main_app.edit_admin_stripe_connect_settings_path + end + + def obfuscated_secret_key + key = Stripe.api_key + key.first(8) + "****" + key.last(4) + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 001805928f..b241dede09 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -57,7 +57,7 @@ class ApplicationController < ActionController::Base def enable_embedded_shopfront whitelist = Spree::Config[:embedded_shopfronts_whitelist] return unless Spree::Config[:enable_embedded_shopfronts] && whitelist.present? - return if request.referer && URI(request.referer).scheme != 'https' && !Rails.env.test? + return if request.referer && URI(request.referer).scheme != 'https' && !Rails.env.test? && !Rails.env.development? response.headers.delete 'X-Frame-Options' response.headers['Content-Security-Policy'] = "frame-ancestors #{whitelist}" diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index e002a7c723..ee0bb1f77d 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -15,6 +15,10 @@ class CheckoutController < Spree::CheckoutController include EnterprisesHelper def edit + # This is only required because of spree_paypal_express. If we implement + # a version of paypal that uses this controller, and more specifically + # the #update_failed method, then we can remove this call + restart_checkout end def update @@ -26,28 +30,29 @@ class CheckoutController < Spree::CheckoutController return if redirect_to_paypal_express_form_if_needed end - if advance_order_state(@order) - state_callback(:after) + next if advance_order_state(@order) + + if @order.errors.present? + flash[:error] = @order.errors.full_messages.to_sentence else - if @order.errors.present? - flash[:error] = @order.errors.full_messages.to_sentence - else - flash[:error] = t(:payment_processing_failed) - end - update_failed - return + flash[:error] = t(:payment_processing_failed) end + update_failed + return end if @order.state == "complete" || @order.completed? set_default_bill_address set_default_ship_address - flash[:success] = t(:order_processed_successfully) + ResetOrderService.new(self, current_order).call + session[:access_token] = current_order.token + + flash[:notice] = t(:order_processed_successfully) respond_to do |format| format.html do respond_with(@order, :location => order_path(@order)) end - format.js do + format.json do render json: {path: order_path(@order)}, status: 200 end end @@ -59,6 +64,13 @@ class CheckoutController < Spree::CheckoutController end end + # Clears the cached order. Required for #current_order to return a new order + # to serve as cart. See https://github.com/spree/spree/blob/1-3-stable/core/lib/spree/core/controller_helpers/order.rb#L14 + # for details. + def expire_current_order + session[:order_id] = nil + @current_order = nil + end private @@ -111,6 +123,9 @@ class CheckoutController < Spree::CheckoutController if (params[:order][:payments_attributes]) params[:order][:payments_attributes].first[:amount] = @order.total end + if params[:order][:existing_card_id] + construct_saved_card_attributes + end params[:order] end @@ -126,11 +141,12 @@ class CheckoutController < Spree::CheckoutController def update_failed clear_ship_address + restart_checkout respond_to do |format| format.html do render :edit end - format.js do + format.json do render json: {errors: @order.errors, flash: flash.to_hash}.to_json, status: 400 end end @@ -144,6 +160,15 @@ class CheckoutController < Spree::CheckoutController end end + def restart_checkout + return if @order.state == 'cart' + @order.restart_checkout! # resets state to 'cart' + @order.update_attributes!(shipping_method_id: nil) + @order.shipments.with_state(:pending).destroy_all + @order.payments.with_state(:checkout).destroy_all + @order.reload + end + def skip_state_validation? true end @@ -154,7 +179,7 @@ class CheckoutController < Spree::CheckoutController raise_insufficient_quantity and return if @order.insufficient_stock_lines.present? redirect_to main_app.shop_path and return if @order.completed? before_address - state_callback(:before) + setup_for_current_state end def before_address @@ -172,14 +197,6 @@ class CheckoutController < Spree::CheckoutController @order.ship_address ||= customer_preferred_ship_address || preferred_ship_address || last_used_ship_address || Spree::Address.default end - def after_payment - # object_params sets the payment amount to the order total, but it does this before - # the shipping method is set. This results in the customer not being charged for their - # order's shipping. To fix this, we refresh the payment amount here. - @order.update_totals - @order.payments.first.update_attribute :amount, @order.total - end - # Overriding Spree's methods def raise_insufficient_quantity respond_to do |format| @@ -202,4 +219,28 @@ class CheckoutController < Spree::CheckoutController render json: {path: spree.paypal_express_url(payment_method_id: payment_method.id)}, status: 200 true end + + def construct_saved_card_attributes + existing_card_id = params[:order].delete(:existing_card_id) + return if existing_card_id.blank? + + credit_card = Spree::CreditCard.find(existing_card_id) + if credit_card.try(:user_id).blank? || credit_card.user_id != spree_current_user.try(:id) + raise Spree::Core::GatewayError, I18n.t(:invalid_credit_card) + end + + # Not currently supported but maybe we should add it...? + credit_card.verification_value = params[:cvc_confirm] if params[:cvc_confirm].present? + + params[:order][:payments_attributes].first[:source] = credit_card + params[:order][:payments_attributes].first.delete :source_attributes + end + + def rescue_from_spree_gateway_error(error) + flash[:error] = t(:spree_gateway_error_flash_for_checkout, error: error.message) + respond_to do |format| + format.html { render :edit } + format.json { render json: { flash: flash.to_hash }, status: 400 } + end + end end diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 126e2bdeed..83c6f6e6c0 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -15,10 +15,11 @@ class EnterprisesController < BaseController respond_to :js, only: :permalink_checker def relatives + set_enterprise + respond_to do |format| format.json do - enterprise = Enterprise.find(params[:id]) - enterprises = enterprise.andand.relatives.andand.activated + enterprises = @enterprise.andand.relatives.andand.activated render(json: enterprises, each_serializer: Api::EnterpriseSerializer, data: OpenFoodNetwork::EnterpriseInjectionData.new) @@ -40,6 +41,10 @@ class EnterprisesController < BaseController private + def set_enterprise + @enterprise = Enterprise.find_by_id(params[:id]) + end + def clean_permalink params[:permalink] = params[:permalink].parameterize end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 43a3f49abe..ce72038e70 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -6,6 +6,8 @@ class GroupsController < BaseController end def show + enable_embedded_shopfront + @hide_menu = true if @shopfront_layout == 'embedded' @group = EnterpriseGroup.find_by_permalink(params[:id]) || EnterpriseGroup.find(params[:id]) end end diff --git a/app/controllers/spree/admin/line_items_controller_decorator.rb b/app/controllers/spree/admin/line_items_controller_decorator.rb index 5e95681f95..7210a6abb1 100644 --- a/app/controllers/spree/admin/line_items_controller_decorator.rb +++ b/app/controllers/spree/admin/line_items_controller_decorator.rb @@ -2,21 +2,6 @@ Spree::Admin::LineItemsController.class_eval do prepend_before_filter :load_order, except: :index around_filter :apply_enterprise_fees_with_lock, only: :update - respond_to :json - - # TODO make updating line items faster by creating a bulk update method - - def index - respond_to do |format| - format.json do - order_params = params[:q].andand.delete :order - orders = OpenFoodNetwork::Permissions.new(spree_current_user).editable_orders.ransack(order_params).result - line_items = OpenFoodNetwork::Permissions.new(spree_current_user).editable_line_items.where(order_id: orders).ransack(params[:q]) - render_as_json line_items.result.reorder('order_id ASC, id ASC') - end - end - end - def create variant = Spree::Variant.find(params[:line_item][:variant_id]) OpenFoodNetwork::ScopeVariantToHub.new(@order.distributor).scope(variant) @@ -34,9 +19,30 @@ Spree::Admin::LineItemsController.class_eval do end end + # TODO: simplify this, 3 formats per action is too much + # we need `js` format for admin/orders/edit (jquery-rails gem) + # we don't know if `html` format is needed + def update + respond_to do |format| + format.html { render_order_form } + format.js { + if @line_item.update_attributes(params[:line_item]) + render nothing: true, status: 204 # No Content, does not trigger ng resource auto-update + else + render json: { errors: @line_item.errors }, status: 412 + end + } + end + end private + def render_order_form + respond_to do |format| + format.html { render partial: 'spree/admin/orders/form', locals: {order: @order.reload} } + end + end + def load_order @order = Spree::Order.find_by_number!(params[:order_id]) authorize! :update, @order diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index 443eda0538..e99b91638b 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -4,11 +4,7 @@ Spree::Admin::OrdersController.class_eval do include OpenFoodNetwork::SpreeApiKeyLoader helper CheckoutHelper before_filter :load_spree_api_key, :only => :bulk_management - - # We need to add expections for collection actions other than :index here - # because spree_auth_devise causes load_order to be called, which results - # in an auth failure as the @order object is nil for collection actions - before_filter :check_authorization, except: [:bulk_management, :managed] + before_filter :load_order, only: %i[show edit update fire resend invoice print] before_filter :load_distribution_choices, only: [:new, :edit, :update] @@ -83,7 +79,7 @@ Spree::Admin::OrdersController.class_eval do template = if Spree::Config.invoice_style2? then "spree/admin/orders/invoice2" else "spree/admin/orders/invoice" end pdf = render_to_string pdf: "invoice-#{@order.number}.pdf", template: template, formats: [:html], encoding: "UTF-8" Spree::OrderMailer.invoice_email(@order.id, pdf).deliver - flash[:success] = t(:invoice_email_sent) + flash[:success] = t('admin.orders.invoice_email_sent') respond_with(@order) { |format| format.html { redirect_to edit_admin_order_path(@order) } } end diff --git a/app/controllers/spree/admin/payment_methods_controller_decorator.rb b/app/controllers/spree/admin/payment_methods_controller_decorator.rb index fc15324d0b..bded58fe19 100644 --- a/app/controllers/spree/admin/payment_methods_controller_decorator.rb +++ b/app/controllers/spree/admin/payment_methods_controller_decorator.rb @@ -1,6 +1,7 @@ module Spree module Admin PaymentMethodsController.class_eval do + before_filter :restrict_stripe_account_change, only: [:update] before_filter :force_environment, only: [:create, :update] skip_before_filter :load_resource, only: [:show_provider_preferences] before_filter :load_hubs, only: [:new, :edit, :update] @@ -57,12 +58,30 @@ module Spree else @providers = Gateway.providers.reject{ |p| p.name.include? "Bogus" }.sort{|p1, p2| p1.name <=> p2.name } end + @providers.reject!{ |p| p.name.ends_with? "StripeConnect" } unless show_stripe? @calculators = PaymentMethod.calculators.sort_by(&:name) end def load_hubs @hubs = Enterprise.managed_by(spree_current_user).is_distributor.sort_by!{ |d| [(@payment_method.has_distributor? d) ? 0 : 1, d.name] } end + + # Show Stripe as an option if enabled, or if the + # current payment_method is already a Stripe method + def show_stripe? + Spree::Config.stripe_connect_enabled || @payment_method.try(:type) == "Spree::Gateway::StripeConnect" + end + + def restrict_stripe_account_change + return unless @payment_method + return unless @payment_method.type == "Spree::Gateway::StripeConnect" + return unless @payment_method.preferred_enterprise_id.andand > 0 + + @stripe_account_holder = Enterprise.find(@payment_method.preferred_enterprise_id) + return if spree_current_user.enterprises.include? @stripe_account_holder + + params[:payment_method][:preferred_enterprise_id] = @stripe_account_holder.id + end end end end diff --git a/app/controllers/spree/admin/payments_controller_decorator.rb b/app/controllers/spree/admin/payments_controller_decorator.rb index 23db5eca09..92faeb093d 100644 --- a/app/controllers/spree/admin/payments_controller_decorator.rb +++ b/app/controllers/spree/admin/payments_controller_decorator.rb @@ -1,21 +1,32 @@ Spree::Admin::PaymentsController.class_eval do - # When a user fires an event, take them back to where they came from - # Responder: http://guides.spreecommerce.com/developer/logic.html#overriding-controller-action-responses - - # For some strange reason, adding PaymentsController.class_eval will cause gems/spree/app/controllers/spree/admin/payments_controller.rb:37 to error: - # payments_url not defined. - # This could be fixed by replacing line 37 with: - # respond_with(@payment, location: admin_order_payments_url) { |format| format.html { redirect_to admin_order_payments_path(@order) } } - respond_override :fire => { :html => { :success => lambda { - redirect_to request.referer # Keeps any filter and sort prefs - } } } - append_before_filter :filter_payment_methods + + # When a user fires an event, take them back to where they came from + # (we can't use respond_override because Spree no longer uses respond_with) + def fire + event = params[:e] + return unless event && @payment.payment_source + + # Because we have a transition method also called void, we do this to avoid conflicts. + event = "void_transaction" if event == "void" + if @payment.send("#{event}!") + flash[:success] = t(:payment_updated) + else + flash[:error] = t(:cannot_perform_operation) + end + rescue Spree::Core::GatewayError => ge + flash[:error] = ge.message + ensure + redirect_to request.referer + end + + + private + # Only show payments for the order's distributor def filter_payment_methods @payment_methods = @payment_methods.select{ |pm| pm.has_distributor? @order.distributor} @payment_method ||= @payment_methods.first end - end diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index 7929aa1df6..59c4cd3069 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -8,9 +8,6 @@ Spree::Admin::ProductsController.class_eval do before_filter :load_spree_api_key, :only => [:bulk_edit, :variant_overrides] before_filter :strip_new_properties, only: [:create, :update] - - respond_to :json, :only => :clone - respond_override create: { html: { success: lambda { if params[:button] == "add_another" @@ -22,7 +19,6 @@ Spree::Admin::ProductsController.class_eval do failure: lambda { render :new } } } - #respond_override :clone => { :json => {:success => lambda { redirect_to bulk_index_admin_products_url+"?q[id_eq]=#{@new.id}" } } } def product_distributions end diff --git a/app/controllers/spree/api/products_controller_decorator.rb b/app/controllers/spree/api/products_controller_decorator.rb index e809dbc23e..ee3cc9e70b 100644 --- a/app/controllers/spree/api/products_controller_decorator.rb +++ b/app/controllers/spree/api/products_controller_decorator.rb @@ -37,6 +37,17 @@ Spree::Api::ProductsController.class_eval do respond_with(@product, :status => 204) end + # POST /api/products/:product_id/clone + # + def clone + authorize! :create, Spree::Product + original_product = find_product(params[:product_id]) + authorize! :update, original_product + + @product = original_product.duplicate + + respond_with(@product, status: 201, default_template: :show) + end private diff --git a/app/controllers/spree/checkout_controller_decorator.rb b/app/controllers/spree/checkout_controller_decorator.rb index 7841c86c36..e5146394d5 100644 --- a/app/controllers/spree/checkout_controller_decorator.rb +++ b/app/controllers/spree/checkout_controller_decorator.rb @@ -32,8 +32,4 @@ Spree::CheckoutController.class_eval do @order.bill_address ||= preferred_bill_address || last_used_bill_address || Spree::Address.default @order.ship_address ||= preferred_ship_address || last_used_ship_address || nil end - - def after_complete - reset_order - end end diff --git a/app/controllers/spree/credit_cards_controller.rb b/app/controllers/spree/credit_cards_controller.rb new file mode 100644 index 0000000000..4582931dd0 --- /dev/null +++ b/app/controllers/spree/credit_cards_controller.rb @@ -0,0 +1,69 @@ +module Spree + class CreditCardsController < BaseController + def new_from_token + # A new Customer is created for every credit card (same as via ActiveMerchant) + # Note that default_source is the card represented by the token + + @customer = create_customer(params[:token]) + @credit_card = build_card_from(stored_card_attributes) + if @credit_card.save + 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: 400 + end + rescue Stripe::CardError => e + return render json: { flash: { error: I18n.t(:spree_gateway_error_flash_for_checkout, error: e.message) } }, status: 400 + end + + def destroy + @credit_card = Spree::CreditCard.find_by_id(params[:id]) + if @credit_card + authorize! :destroy, @credit_card + destroy_at_stripe + end + + # Using try because we may not have a card here + if @credit_card.try(:destroy) + flash[:success] = I18n.t(:card_has_been_removed, number: "x-#{@credit_card.last_digits}") + else + flash[:error] = I18n.t(:card_could_not_be_removed) + end + redirect_to account_path(anchor: 'cards') + rescue Stripe::CardError + flash[:error] = I18n.t(:card_could_not_be_removed) + redirect_to account_path(anchor: 'cards') + end + + private + + # Currently can only destroy the whole customer object + def destroy_at_stripe + stripe_customer = Stripe::Customer.retrieve(@credit_card.gateway_customer_profile_id) + stripe_customer.delete if stripe_customer + end + + def create_customer(token) + Stripe::Customer.create(email: spree_current_user.email, source: token) + end + + def stored_card_attributes + return {} unless @customer.try(:default_source) + { + month: params[:exp_month], + year: params[:exp_year], + last_digits: params[:last4], + gateway_payment_profile_id: @customer.default_source, + gateway_customer_profile_id: @customer.id, + cc_type: params[:cc_type] + } + end + + def build_card_from(attrs) + card = Spree::CreditCard.new(attrs) + # Can't mass assign user: + card.user_id = spree_current_user.id + card + end + end +end diff --git a/app/controllers/spree/paypal_controller_decorator.rb b/app/controllers/spree/paypal_controller_decorator.rb index 74da22557f..cc18c9fc7d 100644 --- a/app/controllers/spree/paypal_controller_decorator.rb +++ b/app/controllers/spree/paypal_controller_decorator.rb @@ -1,21 +1,44 @@ Spree::PaypalController.class_eval do - include CheckoutHelper - - after_filter :reset_order_when_complete, only: :confirm before_filter :enable_embedded_shopfront + before_filter :destroy_orphaned_paypal_payments, only: :confirm + after_filter :reset_order_when_complete, only: :confirm def cancel - flash[:notice] = t('flash.cancel', :scope => 'paypal') + flash[:notice] = Spree.t('flash.cancel', :scope => 'paypal') redirect_to main_app.checkout_path end + # Clears the cached order. Required for #current_order to return a new order + # to serve as cart. See https://github.com/spree/spree/blob/1-3-stable/core/lib/spree/core/controller_helpers/order.rb#L14 + # for details. + def expire_current_order + session[:order_id] = nil + @current_order = nil + end private def reset_order_when_complete if current_order.complete? - flash[:success] = t(:order_processed_successfully) - reset_order + flash[:notice] = t(:order_processed_successfully) + + ResetOrderService.new(self, current_order).call + session[:access_token] = current_order.token end end + + # See #1074 and #1837 for more detail on why we need this + # An 'orphaned' Spree::Payment is created for every call to CheckoutController#update + # for orders that are processed using a Spree::Gateway::PayPalExpress payment method + # These payments are 'orphaned' because they are never used by the spree_paypal_express gem + # which creates a brand new Spree::Payment from scratch in PayPalController#confirm + # However, the 'orphaned' payments are useful when applying a transaction fee, because the fees + # need to be calculated before the order details are sent to PayPal for confirmation + # This is our best hook for removing the orphaned payments at an appropriate time. ie. after + # the payment details have been confirmed, but before any payments have been processed + def destroy_orphaned_paypal_payments + return unless payment_method.is_a?(Spree::Gateway::PayPalExpress) + orphaned_payments = current_order.payments.where(payment_method_id: payment_method.id, source_id: nil) + orphaned_payments.each(&:destroy) + end end diff --git a/app/controllers/spree/users_controller_decorator.rb b/app/controllers/spree/users_controller_decorator.rb index a56c59d6fa..417086ca51 100644 --- a/app/controllers/spree/users_controller_decorator.rb +++ b/app/controllers/spree/users_controller_decorator.rb @@ -2,4 +2,14 @@ Spree::UsersController.class_eval do layout 'darkswarm' before_filter :enable_embedded_shopfront + + # Override of spree_auth_devise default + # Ignores invoice orders, only order where state: 'complete' + def show + @orders = @user.orders.where(state: 'complete').order('completed_at desc') + + return unless Spree::Config.accounts_distributor_id + + @orders = @orders.where('distributor_id != ?', Spree::Config.accounts_distributor_id) + end end diff --git a/app/controllers/stripe/callbacks_controller.rb b/app/controllers/stripe/callbacks_controller.rb new file mode 100644 index 0000000000..4db446144f --- /dev/null +++ b/app/controllers/stripe/callbacks_controller.rb @@ -0,0 +1,20 @@ +require 'stripe/account_connector' + +module Stripe + class CallbacksController < BaseController + def index + connector = Stripe::AccountConnector.new(spree_current_user, params) + + if connector.create_account + flash[:success] = t('admin.controllers.enterprises.stripe_connect_success') + elsif connector.connection_cancelled_by_user? + flash[:notice] = t('admin.controllers.enterprises.stripe_connect_cancelled') + else + flash[:error] = t('admin.controllers.enterprises.stripe_connect_fail') + end + redirect_to main_app.edit_admin_enterprise_path(connector.enterprise, anchor: 'payment_methods') + rescue Stripe::StripeError => e + render text: e.message, status: 500 + end + end +end diff --git a/app/controllers/stripe/webhooks_controller.rb b/app/controllers/stripe/webhooks_controller.rb new file mode 100644 index 0000000000..8ada0361b6 --- /dev/null +++ b/app/controllers/stripe/webhooks_controller.rb @@ -0,0 +1,38 @@ +require 'stripe/webhook_handler' + +module Stripe + class WebhooksController < BaseController + protect_from_forgery except: :create + before_filter :verify_webhook + + # POST /stripe/webhook + def create + handler = WebhookHandler.new(@event) + result = handler.handle + + render nothing: true, status: status_mappings[result] || 200 + end + + private + + def verify_webhook + payload = request.raw_post + signature = request.headers["HTTP_STRIPE_SIGNATURE"] + @event = Webhook.construct_event(payload, signature, Stripe.endpoint_secret) + rescue JSON::ParserError + render nothing: true, status: 400 + rescue Stripe::SignatureVerificationError + render nothing: true, status: 401 + end + + # Stripe interprets a 4xx or 3xx response as a failure to receive the webhook, + # and will stop sending events if too many of either of these are returned. + def status_mappings + { + success: 200, # The event was handled successfully + unknown: 202, # The event was of an unknown type + ignored: 204 # No action was taken in response to the event + } + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a9abf266dc..bec9d181c9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -20,4 +20,10 @@ module ApplicationHelper super end end + + def body_classes + classes = [] + classes << "off-canvas" unless @hide_menu + classes << @shopfront_layout + end end diff --git a/app/helpers/checkout_helper.rb b/app/helpers/checkout_helper.rb index 278422edbb..bd63882da3 100644 --- a/app/helpers/checkout_helper.rb +++ b/app/helpers/checkout_helper.rb @@ -104,20 +104,6 @@ module CheckoutHelper render "shared/validated_select", name: name, path: path, options: options, attributes: attributes end - def reset_order - distributor = current_order.distributor - token = current_order.token - - session[:order_id] = nil - @current_order = nil - current_order(true) - - current_order.set_distributor!(distributor) - current_order.tokenized_permission.token = token - current_order.tokenized_permission.save! - session[:access_token] = token - end - def payment_method_price(method, order) price = method.compute_amount(order) if price == 0 diff --git a/app/helpers/enterprises_helper.rb b/app/helpers/enterprises_helper.rb index 6b42d3bca5..7d4591da90 100644 --- a/app/helpers/enterprises_helper.rb +++ b/app/helpers/enterprises_helper.rb @@ -1,3 +1,5 @@ +require 'open_food_network/available_payment_method_filter' + module EnterprisesHelper def current_distributor @current_distributor ||= current_order(false).andand.distributor @@ -22,6 +24,9 @@ module EnterprisesHelper return [] unless current_distributor.present? payment_methods = current_distributor.payment_methods.available(:front_end).all + filter = OpenFoodNetwork::AvailablePaymentMethodFilter.new + filter.filter!(payment_methods) + applicator = OpenFoodNetwork::TagRuleApplicator.new(current_distributor, "FilterPaymentMethods", current_customer.andand.tag_list) applicator.filter!(payment_methods) diff --git a/app/helpers/i18n_helper.rb b/app/helpers/i18n_helper.rb index 527429b530..4c93bdc0bc 100644 --- a/app/helpers/i18n_helper.rb +++ b/app/helpers/i18n_helper.rb @@ -1,7 +1,16 @@ module I18nHelper - private - def set_locale - I18n.locale = params[:locale] || I18n.default_locale + # Save a given locale + if params[:locale] && Rails.application.config.i18n.available_locales.include?(params[:locale]) + spree_current_user.update_attributes!(locale: params[:locale]) if spree_current_user + cookies[:locale] = params[:locale] + end + + # After logging in, check if the user chose a locale before + if spree_current_user && spree_current_user.locale.nil? && cookies[:locale] + spree_current_user.update_attributes!(locale: params[:locale]) + end + + I18n.locale = spree_current_user.andand.locale || cookies[:locale] || I18n.default_locale end end diff --git a/app/helpers/injection_helper.rb b/app/helpers/injection_helper.rb index dad9cf11f9..b750d3f46b 100644 --- a/app/helpers/injection_helper.rb +++ b/app/helpers/injection_helper.rb @@ -64,9 +64,21 @@ module InjectionHelper render partial: "json/injection_ams", locals: {name: 'enterpriseAttributes', json: "#{@enterprise_attributes.to_json}"} end - def inject_orders_by_distributor - data_array = spree_current_user.orders_by_distributor - inject_json_ams "orders_by_distributor", data_array, Api::OrdersByDistributorSerializer + def inject_orders + inject_json_ams "orders", @orders.all, Api::OrderSerializer + end + + def inject_shops + shops = Enterprise.where(id: @orders.pluck(:distributor_id).uniq) + inject_json_ams "shops", shops.all, Api::ShopForOrdersSerializer + end + + def inject_saved_credit_cards + if spree_current_user + data = spree_current_user.credit_cards.with_payment_profile.all + end + + inject_json_ams "savedCreditCards", data, Api::CreditCardSerializer end def inject_json(name, partial, opts = {}) @@ -89,5 +101,4 @@ module InjectionHelper @enterprise_injection_data ||= OpenFoodNetwork::EnterpriseInjectionData.new {data: @enterprise_injection_data} end - end diff --git a/app/helpers/spree/admin/base_helper_decorator.rb b/app/helpers/spree/admin/base_helper_decorator.rb index 5398cee35f..99fde70676 100644 --- a/app/helpers/spree/admin/base_helper_decorator.rb +++ b/app/helpers/spree/admin/base_helper_decorator.rb @@ -22,7 +22,6 @@ module Spree link_to_with_icon('icon-trash', name, '#', html_options) + f.hidden_field(:_destroy) end - end end end diff --git a/app/models/billable_period.rb b/app/models/billable_period.rb index d3a58e3745..d2ca41b0a9 100644 --- a/app/models/billable_period.rb +++ b/app/models/billable_period.rb @@ -71,7 +71,7 @@ class BillablePeriod < ActiveRecord::Base source: self, originator: nil, # enterprise.package mandatory: true, - locked: false + state: 'closed' } end end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 9a4f2c6c80..95fb6461cc 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -42,6 +42,7 @@ class Enterprise < ActiveRecord::Base has_many :billable_periods has_many :inventory_items has_many :tag_rules + has_one :stripe_account, dependent: :destroy delegate :latitude, :longitude, :city, :state_name, :to => :address @@ -66,7 +67,6 @@ class Enterprise < ActiveRecord::Base supports_s3 :logo supports_s3 :promo_image - validates :name, presence: true validate :name_is_unique validates :sells, presence: true, inclusion: {in: SELLS} @@ -78,7 +78,6 @@ class Enterprise < ActiveRecord::Base validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? } validates_length_of :description, :maximum => 255 - before_save :confirmation_check, if: lambda { email_changed? } before_validation :initialize_permalink, if: lambda { permalink.nil? } @@ -95,7 +94,6 @@ class Enterprise < ActiveRecord::Base after_rollback :restore_permalink - scope :by_name, order('name') scope :visible, where(visible: true) scope :confirmed, where('confirmed_at IS NOT NULL') diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index ff118cf0aa..3b32ad0901 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -1,4 +1,6 @@ class EnterpriseFee < ActiveRecord::Base + include Spree::Core::CalculatedAdjustments + belongs_to :enterprise belongs_to :tax_category, class_name: 'Spree::TaxCategory', foreign_key: 'tax_category_id' @@ -14,9 +16,6 @@ class EnterpriseFee < ActiveRecord::Base # coordinator_fees and exchange_fees - calculated_adjustments - - attr_accessible :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type, :inherits_tax_category FEE_TYPES = %w(packing transport admin sales fundraising) @@ -54,19 +53,6 @@ class EnterpriseFee < ActiveRecord::Base order.adjustments.where(originator_type: 'EnterpriseFee').destroy_all end - # Create an adjustment that starts as locked. Preferable to making an adjustment and locking it since - # the unlocked adjustment tends to get hit by callbacks before we have a chance to lock it. - def create_locked_adjustment(label, target, calculable, mandatory=false) - amount = compute_amount(calculable) - return if amount == 0 && !mandatory - target.adjustments.create({ :amount => amount, - :source => calculable, - :originator => self, - :label => label, - :mandatory => mandatory, - :locked => true}, :without_protection => true) - end - private diff --git a/app/models/product_distribution.rb b/app/models/product_distribution.rb index e866d3860b..a18d9d638c 100644 --- a/app/models/product_distribution.rb +++ b/app/models/product_distribution.rb @@ -17,7 +17,7 @@ class ProductDistribution < ActiveRecord::Base end def create_adjustment_for(line_item) - a = enterprise_fee.create_locked_adjustment(adjustment_label_for(line_item), line_item.order, line_item, true) + a = enterprise_fee.create_adjustment(adjustment_label_for(line_item), line_item.order, line_item, true) AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: 'distributor' end diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 6263c4ee51..c08bd1f654 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -1,7 +1,7 @@ class AbilityDecorator include CanCan::Ability - # All abilites are allocated from this initialiser, currently in 5 chunks. + # All abilites are allocated from this initialiser. # Spree also defines other abilities. def initialize(user) add_shopping_abilities user @@ -56,9 +56,14 @@ class AbilityDecorator user == item.order.user && item.order.changes_allowed? end + can [:cancel], Spree::Order do |order| order.user == user end + + can [:destroy], Spree::CreditCard do |credit_card| + credit_card.user == user + end end # New users can create an enterprise, and gain other permissions from doing this. @@ -118,12 +123,16 @@ class AbilityDecorator can [:admin, :bulk_update], ColumnPreference do |column_preference| column_preference.user == user end + + can [:admin, :connect, :status, :destroy], StripeAccount do |stripe_account| + user.enterprises.include? stripe_account.enterprise + end end def add_product_management_abilities(user) # Enterprise User can only access products that they are a supplier for can [:create], Spree::Product - can [:admin, :read, :update, :product_distributions, :bulk_edit, :bulk_update, :clone, :delete, :destroy], Spree::Product do |product| + can [:admin, :read, :update, :product_distributions, :seo, :group_buy_options, :bulk_edit, :bulk_update, :clone, :delete, :destroy], Spree::Product do |product| OpenFoodNetwork::Permissions.new(user).managed_product_enterprises.include? product.supplier end @@ -193,6 +202,7 @@ class AbilityDecorator end can [:admin, :bulk_management, :managed], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor) can [:admin , :for_line_items], Enterprise + can [:admin, :index, :create, :update, :destroy], :line_item can [:admin, :index, :create], Spree::LineItem can [:destroy, :update], Spree::LineItem do |item| order = item.order diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb index 925106f3db..a702cecf15 100644 --- a/app/models/spree/app_configuration_decorator.rb +++ b/app/models/spree/app_configuration_decorator.rb @@ -39,4 +39,7 @@ Spree::AppConfiguration.class_eval do # Invoices & Receipts preference :invoice_style2?, :boolean, default: false preference :enable_receipt_printing?, :boolean, default: false + + # Stripe Connect + preference :stripe_connect_enabled, :boolean, default: false end diff --git a/app/models/spree/credit_card_decorator.rb b/app/models/spree/credit_card_decorator.rb new file mode 100644 index 0000000000..b9c0fca337 --- /dev/null +++ b/app/models/spree/credit_card_decorator.rb @@ -0,0 +1,28 @@ +Spree::CreditCard.class_eval do + # Allows user to submit these attributes with checkout request + # Required to be able to correctly store details for token-based charges + # Obviously can be removed once we are using strong params + attr_accessible :cc_type, :last_digits + + # For holding customer preference in memory + attr_accessible :save_requested_by_customer + attr_writer :save_requested_by_customer + + # Should be able to remove once we reach Spree v2.2.0 + # https://github.com/spree/spree/commit/411010f3975c919ab298cb63962ee492455b415c + belongs_to :payment_method + + belongs_to :user + + # Allows us to use a gateway_payment_profile_id to store Stripe Tokens + # Should be able to remove once we reach Spree v2.2.0 + # Commit: https://github.com/spree/spree/commit/5a4d690ebc64b264bf12904a70187e7a8735ef3f + # See also: https://github.com/spree/spree_gateway/issues/111 + def has_payment_profile? # rubocop:disable Style/PredicateName + gateway_customer_profile_id.present? || gateway_payment_profile_id.present? + end + + def save_requested_by_customer? + !!@save_requested_by_customer + end +end diff --git a/app/models/spree/gateway/stripe_connect.rb b/app/models/spree/gateway/stripe_connect.rb new file mode 100644 index 0000000000..6efd2f79ef --- /dev/null +++ b/app/models/spree/gateway/stripe_connect.rb @@ -0,0 +1,106 @@ +require 'stripe/profile_storer' + +module Spree + class Gateway + class StripeConnect < Gateway + preference :enterprise_id, :integer + + validate :ensure_enterprise_selected + + attr_accessible :preferred_enterprise_id + + CARD_TYPE_MAPPING = { + 'American Express' => 'american_express', + 'Diners Club' => 'diners_club', + 'Visa' => 'visa' + }.freeze + + def method_type + 'stripe' + end + + def provider_class + ActiveMerchant::Billing::StripeGateway + end + + def payment_profiles_supported? + true + end + + def stripe_account_id + StripeAccount.find_by_enterprise_id(preferred_enterprise_id).andand.stripe_user_id + end + + 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 + + def void(response_code, _creditcard, gateway_options) + gateway_options[:stripe_account] = stripe_account_id + provider.void(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 + + creditcard = token_from_card_profile_ids(creditcard) + + [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 + + return nil if token_or_card_id.blank? + + # Assume the gateway_payment_profile_id is a token generated by StripeJS + return token_or_card_id if customer.blank? + + # Assume the gateway_payment_profile_id is a Stripe card_id + # So generate a new token, using the customer_id and card_id + tokenize_instance_customer_card(customer, token_or_card_id) + end + + def tokenize_instance_customer_card(customer, card) + token = Stripe::Token.create({card: card, customer: customer}, stripe_account: stripe_account_id) + token.id + 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 > 0 + errors.add(:stripe_account_owner, I18n.t(:error_required)) + end + end + end +end diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index 9c80df8bb4..4985d059b0 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -60,7 +60,7 @@ Spree::LineItem.class_eval do end def price_with_adjustments - # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, + # EnterpriseFee#create_adjustment applies adjustments on line items to their parent order, # so line_item.adjustments returns an empty array return 0 if quantity == 0 (price + order.adjustments.where(source_id: id).sum(&:amount) / quantity).round(2) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 2eff1faa54..31c14c0225 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -37,7 +37,8 @@ Spree::Order.class_eval do end order.payment_required? } - go_to_state :confirm, :if => lambda { |order| order.confirmation_required? } + # NOTE: :confirm step was removed because we were not actually using it + # go_to_state :confirm, :if => lambda { |order| order.confirmation_required? } go_to_state :complete remove_transition :from => :delivery, :to => :confirm end @@ -181,6 +182,10 @@ Spree::Order.class_eval do end def update_distribution_charge! + # `with_lock` acquires an exclusive row lock on order so no other + # requests can update it until the transaction is commited. + # See https://github.com/rails/rails/blob/3-2-stable/activerecord/lib/active_record/locking/pessimistic.rb#L69 + # and https://www.postgresql.org/docs/current/static/sql-select.html#SQL-FOR-UPDATE-SHARE with_lock do EnterpriseFee.clear_all_adjustments_on_order self @@ -289,6 +294,24 @@ Spree::Order.class_eval do complete? && distributor.andand.allow_order_changes? && order_cycle.andand.open? end + # Override of existing Spree method. Can remove when we reach 2-0-stable + # See commit: https://github.com/spree/spree/commit/5fca58f658273451193d5711081d018c317814ed + # Allows GatewayError to show useful error messages in checkout + def process_payments! + pending_payments.each do |payment| + break if payment_total >= total + + payment.process! + + if payment.completed? + self.payment_total += payment.amount + end + end + rescue Spree::Core::GatewayError => e # This section changed + result = !!Spree::Config[:allow_checkout_on_gateway_error] + errors.add(:base, e.message) and return result + end + private def shipping_address_from_distributor @@ -344,9 +367,23 @@ Spree::Order.class_eval do end def update_adjustment!(adjustment) - locked = adjustment.locked - adjustment.locked = false + state = adjustment.state + adjustment.state = 'open' adjustment.update!(self) - adjustment.locked = locked + adjustment.state = state + end + + # object_params sets the payment amount to the order total, but it does this before + # the shipping method is set. This results in the customer not being charged for their + # order's shipping. To fix this, we refresh the payment amount here. + def charge_shipping_and_payment_fees! + update_totals + return unless payments.any? + payments.first.update_attribute :amount, total end end + +Spree::Order.state_machine.after_transition to: :payment, do: :charge_shipping_and_payment_fees! +Spree::Order.state_machine.event :restart_checkout do + transition :to => :cart, unless: :completed? +end diff --git a/app/models/spree/payment_decorator.rb b/app/models/spree/payment_decorator.rb index 98b996cce7..c9ae401541 100644 --- a/app/models/spree/payment_decorator.rb +++ b/app/models/spree/payment_decorator.rb @@ -4,13 +4,13 @@ module Spree after_save :ensure_correct_adjustment, :update_order + attr_accessible :source + def ensure_correct_adjustment - # Don't charge for invalid payments. - # PayPalExpress always creates a payment that is invalidated later. - # Unknown: What about failed payments? - if state == "invalid" - adjustment.andand.destroy - elsif adjustment + revoke_adjustment_eligibility if ['failed', 'invalid'].include?(state) + return if adjustment.try(:finalized?) + + if adjustment adjustment.originator = payment_method adjustment.label = adjustment_label adjustment.save @@ -73,6 +73,15 @@ module Spree end end + # Import from future Spree v.2.3.0 d470b31798f37 + def build_source + return if source_attributes.nil? + return unless payment_method.andand.payment_source_class + + self.source = payment_method.payment_source_class.new(source_attributes) + source.payment_method_id = payment_method.id + source.user_id = order.user_id if order + end private @@ -81,5 +90,25 @@ module Spree refund_amount.to_f end + def create_payment_profile + return unless source.is_a?(CreditCard) + return unless source.try(:save_requested_by_customer?) + return unless source.number || source.gateway_payment_profile_id + return unless source.gateway_customer_profile_id.nil? + payment_method.create_profile(self) + rescue ActiveMerchant::ConnectionError => e + gateway_error e + end + + # Don't charge fees for invalid or failed payments. + # This is called twice for failed payments, because the persistence of the 'failed' + # state is acheived through some trickery using an after_rollback callback on the + # payment model. See Spree::Payment#persist_invalid + def revoke_adjustment_eligibility + return unless adjustment.try(:reload) + return if adjustment.finalized? + adjustment.update_attribute(:eligible, false) + adjustment.finalize! + end end end diff --git a/app/models/spree/payment_method_decorator.rb b/app/models/spree/payment_method_decorator.rb index bdbbc9a8fe..86538af117 100644 --- a/app/models/spree/payment_method_decorator.rb +++ b/app/models/spree/payment_method_decorator.rb @@ -1,14 +1,15 @@ Spree::PaymentMethod.class_eval do + include Spree::Core::CalculatedAdjustments + Spree::PaymentMethod::DISPLAY = [:both, :front_end, :back_end] acts_as_taggable has_and_belongs_to_many :distributors, join_table: 'distributors_payment_methods', :class_name => 'Enterprise', association_foreign_key: 'distributor_id' + has_many :credit_cards, class_name: "Spree::CreditCard" # from Spree v.2.3.0 d470b31798f37 attr_accessible :distributor_ids, :tag_list - calculated_adjustments - after_initialize :init validates_with DistributorsValidator @@ -39,7 +40,10 @@ Spree::PaymentMethod.class_eval do } def init - self.class.calculated_adjustments unless reflections.keys.include? :calculator + unless reflections.keys.include? :calculator + self.class.include Spree::Core::CalculatedAdjustments + end + self.calculator ||= Spree::Calculator::FlatRate.new(preferred_amount: 0) end @@ -55,6 +59,8 @@ Spree::PaymentMethod.class_eval do "MasterCard Internet Gateway Service (MIGS)" when "Spree::Gateway::Pin" "Pin Payments" + when "Spree::Gateway::StripeConnect" + "Stripe" 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 29e6e2c46a..453c1fadf2 100644 --- a/app/models/spree/shipping_method_decorator.rb +++ b/app/models/spree/shipping_method_decorator.rb @@ -42,8 +42,8 @@ Spree::ShippingMethod.class_eval do ] end - def available_to_order_with_distributor_check?(order, display_on=nil) - available_to_order_without_distributor_check?(order, display_on) && + def available_to_order_with_distributor_check?(order) + available_to_order_without_distributor_check?(order) && self.distributors.include?(order.distributor) end alias_method_chain :available_to_order?, :distributor_check diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index 5007134bd6..ace0c9e165 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -11,13 +11,14 @@ Spree.user_class.class_eval do has_many :billable_periods, foreign_key: :owner_id, inverse_of: :owner has_one :cart has_many :customers + has_many :credit_cards accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true accepts_nested_attributes_for :bill_address accepts_nested_attributes_for :ship_address - attr_accessible :enterprise_ids, :enterprise_roles_attributes, :enterprise_limit, :bill_address_attributes, :ship_address_attributes + attr_accessible :enterprise_ids, :enterprise_roles_attributes, :enterprise_limit, :locale, :bill_address_attributes, :ship_address_attributes after_create :send_signup_confirmation validate :limit_owned_enterprises @@ -53,32 +54,6 @@ Spree.user_class.class_eval do owned_enterprises(:reload).size < enterprise_limit end - # Returns Enterprise IDs for distributors that the user has shopped at - def enterprises_ordered_from - enterprise_ids = orders.where(state: :complete).map(&:distributor_id).uniq - # Exclude the accounts distributor - if Spree::Config.accounts_distributor_id - enterprise_ids = enterprise_ids.keep_if { |a| a != Spree::Config.accounts_distributor_id } - end - enterprise_ids - end - - # Returns orders and their associated payments for all distributors that have been ordered from - def complete_orders_by_distributor - Enterprise - .includes(distributed_orders: { payments: :payment_method }) - .where(enterprises: { id: enterprises_ordered_from }, - spree_orders: { state: 'complete', user_id: id }) - .order('spree_orders.completed_at DESC') - end - - def orders_by_distributor - # Remove uncompleted payments as these will not be reflected in order balance - data_array = complete_orders_by_distributor.to_a - remove_payments_in_checkout(data_array) - data_array.sort! { |a, b| b.distributed_orders.length <=> a.distributed_orders.length } - end - private def limit_owned_enterprises diff --git a/app/models/stripe_account.rb b/app/models/stripe_account.rb new file mode 100644 index 0000000000..1b2840ffe6 --- /dev/null +++ b/app/models/stripe_account.rb @@ -0,0 +1,21 @@ +class StripeAccount < ActiveRecord::Base + belongs_to :enterprise + validates :stripe_user_id, :stripe_publishable_key, presence: true + validates :enterprise_id, uniqueness: true + + def deauthorize_and_destroy + accounts = StripeAccount.where(stripe_user_id: stripe_user_id) + + # Only deauthorize the user if it is not linked to multiple accounts + return destroy if accounts.count > 1 + + destroy && Stripe::OAuth.deauthorize(stripe_user_id: stripe_user_id) + rescue Stripe::OAuth::OAuthError + Bugsnag.notify( + RuntimeError.new("StripeDeauthorizeFailure"), + stripe_account: stripe_user_id, + enterprise_id: enterprise_id + ) + true + end +end diff --git a/app/models/tag_rule/discount_order.rb b/app/models/tag_rule/discount_order.rb index 5984814289..8b4a780cbd 100644 --- a/app/models/tag_rule/discount_order.rb +++ b/app/models/tag_rule/discount_order.rb @@ -1,5 +1,5 @@ class TagRule::DiscountOrder < TagRule - calculated_adjustments + include Spree::Core::CalculatedAdjustments private diff --git a/app/overrides/add_group_buy_to_admin_product_edit.rb b/app/overrides/add_group_buy_to_admin_product_edit.rb deleted file mode 100644 index eb44ca5c6c..0000000000 --- a/app/overrides/add_group_buy_to_admin_product_edit.rb +++ /dev/null @@ -1,5 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/admin/products/_form", - :insert_top => "[data-hook='admin_product_form_right']", - :partial => "spree/admin/products/group_buy_form", - :name => "add_group_buy_to_admin_product_edit", - :original => '0c0e8d714989e48ee246a8253fb2b362f108621a') diff --git a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface index aa9ead7a0f..0201278e0e 100644 --- a/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface +++ b/app/overrides/spree/admin/payment_methods/_form/replace_form_fields.html.haml.deface @@ -1,6 +1,7 @@ / replace "div[data-hook='admin_payment_method_form_fields']" = admin_inject_payment_method += admin_inject_json_ams_array "admin.paymentMethods", "shops", @hubs, Api::Admin::BasicEnterpriseSerializer %div.alpha.eleven.columns{ "ng-app" => "admin.paymentMethods", "ng-controller" => "paymentMethodCtrl" } .row .alpha.three.columns diff --git a/app/overrides/spree/admin/products/_form/add_notes_field.html.haml.deface b/app/overrides/spree/admin/products/_form/add_notes_field.html.haml.deface deleted file mode 100644 index dcc7937d6b..0000000000 --- a/app/overrides/spree/admin/products/_form/add_notes_field.html.haml.deface +++ /dev/null @@ -1,6 +0,0 @@ -/ insert_bottom "[data-hook='admin_product_form_additional_fields']" - -= f.field_container :notes do - = f.label :notes, t(:notes) - = f.text_area :notes, { :class => 'fullwidth', rows: 5 } - = f.error_message_on :notes diff --git a/app/overrides/spree/admin/products/_form/add_primary_taxon_field.html.haml.deface b/app/overrides/spree/admin/products/_form/add_primary_taxon_field.html.haml.deface index efd7ccbfaf..46afe1f94d 100644 --- a/app/overrides/spree/admin/products/_form/add_primary_taxon_field.html.haml.deface +++ b/app/overrides/spree/admin/products/_form/add_primary_taxon_field.html.haml.deface @@ -1,3 +1,3 @@ -/ insert_top "[data-hook='admin_product_form_right']" +/ insert_after "div[class='variant_units_form']" = render 'spree/admin/products/primary_taxon_form', f: f \ No newline at end of file diff --git a/app/overrides/spree/admin/products/_form/add_supplier.html.haml.deface b/app/overrides/spree/admin/products/_form/add_supplier.html.haml.deface index 1cc609bef4..a814c10b80 100644 --- a/app/overrides/spree/admin/products/_form/add_supplier.html.haml.deface +++ b/app/overrides/spree/admin/products/_form/add_supplier.html.haml.deface @@ -1,7 +1,6 @@ -/ insert_top "[data-hook='admin_product_form_right']" - +/ insert_before "code[erb-loud]:contains('f.field_container :price')" = f.field_container :supplier do - = f.label :supplier + = f.label :supplier, t(:spree_admin_supplier) %br = f.collection_select(:supplier_id, @producers, :id, :name, {:include_blank => true}, {:class => "select2"}) - = f.error_message_on :supplier + = f.error_message_on :supplier \ No newline at end of file diff --git a/app/overrides/spree/admin/products/_form/add_units_form.html.haml.deface b/app/overrides/spree/admin/products/_form/add_units_form.html.haml.deface index d20afcca61..6571fb3ab6 100644 --- a/app/overrides/spree/admin/products/_form/add_units_form.html.haml.deface +++ b/app/overrides/spree/admin/products/_form/add_units_form.html.haml.deface @@ -3,7 +3,7 @@ .variant_units_form{ 'ng-app' => 'admin.products', 'ng-controller' => 'editUnitsCtrl' } = f.field_container :units do - = f.label :variant_unit_with_scale, :units + = f.label :variant_unit_with_scale, t(:spree_admin_variant_unit_scale) %select.select2.fullwidth{ id: 'product_variant_unit_with_scale', 'ng-model' => 'variant_unit_with_scale', 'ng-change' => 'setFields()', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' } %option{'value' => ''} diff --git a/app/overrides/spree/admin/products/_form/remove_available_on.deface b/app/overrides/spree/admin/products/_form/remove_available_on.deface new file mode 100644 index 0000000000..645fb76bde --- /dev/null +++ b/app/overrides/spree/admin/products/_form/remove_available_on.deface @@ -0,0 +1,2 @@ +remove "code[erb-loud]:contains('f.label :available_on')" +closing_selector("code[erb-loud]:contains('f.text_field :available_on')") \ No newline at end of file diff --git a/app/overrides/spree/admin/products/_form/remove_cost_currency.deface b/app/overrides/spree/admin/products/_form/remove_cost_currency.deface new file mode 100644 index 0000000000..a194a5e03a --- /dev/null +++ b/app/overrides/spree/admin/products/_form/remove_cost_currency.deface @@ -0,0 +1,2 @@ +remove "code[erb-loud]:contains('f.field_container :cost_currency')" +closing_selector("code[erb-silent]:contains('end')") \ No newline at end of file diff --git a/app/overrides/spree/admin/products/_form/remove_cost_price.deface b/app/overrides/spree/admin/products/_form/remove_cost_price.deface new file mode 100644 index 0000000000..9f9cd8ec3f --- /dev/null +++ b/app/overrides/spree/admin/products/_form/remove_cost_price.deface @@ -0,0 +1,2 @@ +remove "code[erb-loud]:contains('f.field_container :cost_price')" +closing_selector("code[erb-silent]:contains('end')") \ No newline at end of file diff --git a/app/overrides/spree/admin/products/_form/remove_meta_description.deface b/app/overrides/spree/admin/products/_form/remove_meta_description.deface new file mode 100644 index 0000000000..15a00a5df0 --- /dev/null +++ b/app/overrides/spree/admin/products/_form/remove_meta_description.deface @@ -0,0 +1 @@ +remove "div[data-hook='admin_product_form_meta']" \ No newline at end of file diff --git a/app/overrides/spree/admin/products/_form/remove_option_types_and_taxons.deface b/app/overrides/spree/admin/products/_form/remove_option_types_and_taxons.deface new file mode 100644 index 0000000000..eeccddc25c --- /dev/null +++ b/app/overrides/spree/admin/products/_form/remove_option_types_and_taxons.deface @@ -0,0 +1 @@ +remove "div[class='twelve columns alpha omega']" \ No newline at end of file diff --git a/app/overrides/spree/admin/products/_form/replace_master_price_label.html.haml.deface b/app/overrides/spree/admin/products/_form/replace_master_price_label.html.haml.deface new file mode 100644 index 0000000000..02152cb521 --- /dev/null +++ b/app/overrides/spree/admin/products/_form/replace_master_price_label.html.haml.deface @@ -0,0 +1,2 @@ +/ replace "[data-hook=admin_product_form_right] code[erb-loud]:contains('f.label :price')" += f.label :price, raw(t(:price) + content_tag(:span, ' *', :class => 'required')) \ No newline at end of file diff --git a/app/overrides/spree/admin/products/_form/replace_taxons_div.html.haml.deface b/app/overrides/spree/admin/products/_form/replace_taxons_div.html.haml.deface new file mode 100644 index 0000000000..697255ab70 --- /dev/null +++ b/app/overrides/spree/admin/products/_form/replace_taxons_div.html.haml.deface @@ -0,0 +1,5 @@ +/ insert_bottom "[data-hook=admin_product_form_left]" += f.field_container :taxons do + = f.label :taxon_ids, t(:taxons) + %br + = f.hidden_field :taxon_ids, :value => @product.taxon_ids.join(',') \ No newline at end of file diff --git a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface index e9dc6f13c5..efcd0f1d3d 100644 --- a/app/overrides/spree/admin/products/new/replace_form.html.haml.deface +++ b/app/overrides/spree/admin/products/new/replace_form.html.haml.deface @@ -19,14 +19,14 @@ .twelve.columns.alpha{ 'ng-controller' => 'unitsCtrl' } .six.columns.alpha = f.field_container :units do - = f.label :variant_unit_with_scale, :units + = f.label :variant_unit_with_scale, t(:units) %select.select2.fullwidth{ id: 'product_variant_unit_with_scale', 'ng-model' => 'product.variant_unit_with_scale', 'ng-options' => 'unit[1] as unit[0] for unit in variant_unit_options' } %option{'value' => '', 'ng-hide' => "hasUnit(product)"} %input{ type: 'hidden', 'ng-value' => 'product.variant_unit', name: 'product[variant_unit]' } %input{ type: 'hidden', 'ng-value' => 'product.variant_unit_scale', name: 'product[variant_unit_scale]' } .three.columns = f.field_container :unit_value do - = f.label :product_unit_value_with_description, :value, 'ng-disabled' => "!hasUnit(product)" + = f.label :product_unit_value_with_description, t(:value), 'ng-disabled' => "!hasUnit(product)" %input.fullwidth{ id: 'product_unit_value_with_description', 'ng-model' => 'product.master.unit_value_with_description', :type => 'text', placeholder: "eg. 2", 'ng-disabled' => "!hasUnit(product)" } %input{ type: 'hidden', 'ng-value' => 'product.master.unit_value', name: 'product[unit_value]' } %input{ type: 'hidden', 'ng-value' => 'product.master.unit_description', name: 'product[unit_description]' } @@ -87,7 +87,7 @@ = button t('actions.create'), 'icon-ok', :submit, value: "create" %span.or = t(:or) - = button "Create And Add Another", 'icon-repeat', :submit, value: 'add_another' + = button t('actions.create_and_add_another'), 'icon-repeat', :submit, value: 'add_another' %span.or = t(:or) = link_to_with_icon 'icon-remove', t('actions.cancel'), bulk_edit_admin_products_path, :class => 'button' diff --git a/app/overrides/spree/admin/shared/_configuration_menu/add_stripe_connect_settings.html.haml.deface b/app/overrides/spree/admin/shared/_configuration_menu/add_stripe_connect_settings.html.haml.deface new file mode 100644 index 0000000000..c7b3f4789a --- /dev/null +++ b/app/overrides/spree/admin/shared/_configuration_menu/add_stripe_connect_settings.html.haml.deface @@ -0,0 +1,4 @@ +// insert_bottom "[data-hook='admin_configurations_sidebar_menu']" + +%li + = link_to t('.stripe_connect'), main_app.edit_admin_stripe_connect_settings_path diff --git a/app/overrides/spree/admin/shared/_product_tabs/add_group_buy.html.haml.deface b/app/overrides/spree/admin/shared/_product_tabs/add_group_buy.html.haml.deface new file mode 100644 index 0000000000..ab62e4725a --- /dev/null +++ b/app/overrides/spree/admin/shared/_product_tabs/add_group_buy.html.haml.deface @@ -0,0 +1,5 @@ +/ insert_bottom "[data-hook='admin_product_tabs']" + +- klass = current == 'Group Buy Options' ? 'active' : '' +%li{:class => klass} + = link_to_with_icon 'icon-tasks', 'Group Buy Options', group_buy_options_admin_product_url(@product) diff --git a/app/overrides/spree/admin/shared/_product_tabs/add_seo.html.haml.deface b/app/overrides/spree/admin/shared/_product_tabs/add_seo.html.haml.deface new file mode 100644 index 0000000000..98023bf7a1 --- /dev/null +++ b/app/overrides/spree/admin/shared/_product_tabs/add_seo.html.haml.deface @@ -0,0 +1,5 @@ +/ insert_bottom "[data-hook='admin_product_tabs']" + +- klass = current == 'SEO' ? 'active' : '' +%li{:class => klass} + = link_to_with_icon 'icon-tasks', 'SEO', seo_admin_product_url(@product) diff --git a/app/overrides/spree/orders/edit/promo_cart_coupon_code_field.html.haml.deface b/app/overrides/spree/orders/edit/promo_cart_coupon_code_field.html.haml.deface deleted file mode 100644 index dca5725b49..0000000000 --- a/app/overrides/spree/orders/edit/promo_cart_coupon_code_field.html.haml.deface +++ /dev/null @@ -1 +0,0 @@ -/ disabled diff --git a/app/serializers/api/admin/basic_enterprise_serializer.rb b/app/serializers/api/admin/basic_enterprise_serializer.rb index f060dc4166..854b9b4019 100644 --- a/app/serializers/api/admin/basic_enterprise_serializer.rb +++ b/app/serializers/api/admin/basic_enterprise_serializer.rb @@ -1,4 +1,4 @@ class Api::Admin::BasicEnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids - attributes :producer_profile_only + attributes :producer_profile_only, :permalink end diff --git a/app/serializers/api/admin/payment_method/base_serializer.rb b/app/serializers/api/admin/payment_method/base_serializer.rb new file mode 100644 index 0000000000..cc915ace40 --- /dev/null +++ b/app/serializers/api/admin/payment_method/base_serializer.rb @@ -0,0 +1,13 @@ +module Api::Admin::PaymentMethod + class BaseSerializer < ActiveModel::Serializer + attributes :id, :name, :type, :tag_list, :tags + + def tag_list + object.tag_list.join(",") + end + + def tags + object.tag_list.map{ |t| { text: t } } + end + end +end diff --git a/app/serializers/api/admin/payment_method/stripe_serializer.rb b/app/serializers/api/admin/payment_method/stripe_serializer.rb new file mode 100644 index 0000000000..e679547e16 --- /dev/null +++ b/app/serializers/api/admin/payment_method/stripe_serializer.rb @@ -0,0 +1,5 @@ +module Api::Admin::PaymentMethod + class StripeSerializer < BaseSerializer + attributes :preferred_enterprise_id + end +end diff --git a/app/serializers/api/admin/payment_method_serializer.rb b/app/serializers/api/admin/payment_method_serializer.rb index 42c11a6953..3d66ddbc03 100644 --- a/app/serializers/api/admin/payment_method_serializer.rb +++ b/app/serializers/api/admin/payment_method_serializer.rb @@ -1,11 +1,15 @@ -class Api::Admin::PaymentMethodSerializer < ActiveModel::Serializer - attributes :id, :name, :type, :tag_list, :tags +module Api + module Admin + class PaymentMethodSerializer < ActiveModel::Serializer + delegate :serializable_hash, to: :method_serializer - def tag_list - object.tag_list.join(",") - end - - def tags - object.tag_list.map{ |t| { text: t } } + def method_serializer + if object.type == 'Spree::Gateway::StripeConnect' + Api::Admin::PaymentMethod::StripeSerializer.new(object) + else + Api::Admin::PaymentMethod::BaseSerializer.new(object) + end + end + end end end diff --git a/app/serializers/api/credit_card_serializer.rb b/app/serializers/api/credit_card_serializer.rb new file mode 100644 index 0000000000..1ea2da07d5 --- /dev/null +++ b/app/serializers/api/credit_card_serializer.rb @@ -0,0 +1,27 @@ +module Api + class CreditCardSerializer < ActiveModel::Serializer + attributes :id, :brand, :number, :expiry, :formatted, :delete_link + + def brand + object.cc_type.capitalize + end + + def number + "x-#{object.last_digits}" + end + + def expiry + m = object.month.to_i + m = m < 10 ? "0#{m}" : m.to_s + "#{m}/#{object.year}" + end + + def formatted + "#{brand} #{number} #{I18n.t(:card_expiry_abbreviation)}:#{expiry}" + end + + def delete_link + Spree::Core::Engine.routes.url_helpers.credit_card_path(object.id) + end + end +end diff --git a/app/serializers/api/order_serializer.rb b/app/serializers/api/order_serializer.rb index 7aa53a0d6b..481d8ec213 100644 --- a/app/serializers/api/order_serializer.rb +++ b/app/serializers/api/order_serializer.rb @@ -1,13 +1,18 @@ module Api class OrderSerializer < ActiveModel::Serializer attributes :number, :completed_at, :total, :state, :shipment_state, :payment_state - attributes :outstanding_balance, :payments, :path, :cancel_path, :changes_allowed, :changes_allowed_until - attributes :shop_name, :item_count + attributes :outstanding_balance, :payments, :path, :cancel_path + attributes :changes_allowed, :changes_allowed_until, :item_count + attributes :shop_id has_many :payments, serializer: Api::PaymentSerializer - def shop_name - object.distributor.andand.name + def payments + object.payments.joins(:payment_method).completed + end + + def shop_id + object.distributor_id end def item_count @@ -23,10 +28,6 @@ module Api I18n.l(object.order_cycle.andand.orders_close_at, format: "%b %d, %Y %H:%M") end - def total - object.total.to_money.to_s - end - def shipment_state object.shipment_state ? object.shipment_state : nil end diff --git a/app/serializers/api/orders_by_distributor_serializer.rb b/app/serializers/api/orders_by_distributor_serializer.rb index b920272df2..df1cefffc5 100644 --- a/app/serializers/api/orders_by_distributor_serializer.rb +++ b/app/serializers/api/orders_by_distributor_serializer.rb @@ -4,7 +4,7 @@ module Api has_many :distributed_orders, serializer: Api::OrderSerializer def balance - object.distributed_orders.map(&:outstanding_balance).reduce(:+).to_money.to_s + object.distributed_orders.map(&:outstanding_balance).reduce(:+) end def hash diff --git a/app/serializers/api/payment_serializer.rb b/app/serializers/api/payment_serializer.rb index 8465998db6..8b956ecb1f 100644 --- a/app/serializers/api/payment_serializer.rb +++ b/app/serializers/api/payment_serializer.rb @@ -2,15 +2,11 @@ module Api class PaymentSerializer < ActiveModel::Serializer attributes :amount, :updated_at, :payment_method, :state def payment_method - object.payment_method.name - end - - def amount - object.amount.to_money.to_s + object.payment_method.try(:name) end def updated_at - I18n.l(object.updated_at, format: :long) + I18n.l(object.updated_at, format: "%b %d, %Y %H:%M") end end end diff --git a/app/serializers/api/shop_for_orders_serializer.rb b/app/serializers/api/shop_for_orders_serializer.rb new file mode 100644 index 0000000000..1b8fa51b85 --- /dev/null +++ b/app/serializers/api/shop_for_orders_serializer.rb @@ -0,0 +1,13 @@ +module Api + class ShopForOrdersSerializer < ActiveModel::Serializer + attributes :id, :name, :hash, :logo + + def hash + object.to_param + end + + def logo + object.logo(:small) if object.logo? + end + end +end diff --git a/app/services/reset_order_service.rb b/app/services/reset_order_service.rb new file mode 100644 index 0000000000..402c377eb7 --- /dev/null +++ b/app/services/reset_order_service.rb @@ -0,0 +1,31 @@ +# Builds a new order based on the one specified. This implements the "continue +# shopping" feature once an order is completed. +class ResetOrderService + # Constructor + # + # @param controller [#expire_current_order, #current_order] + # @param order [Spree::Order] + def initialize(controller, order) + @controller = controller + @distributor = order.distributor + @token = order.token + end + + # Expires the order currently in use and builds a new one based on it + def call + controller.expire_current_order + build_new_order + end + + private + + attr_reader :controller, :distributor, :token + + # Builds an order setting the token and distributor of the one specified + def build_new_order + new_order = controller.current_order(true) + new_order.set_distributor!(distributor) + new_order.tokenized_permission.token = token + new_order.tokenized_permission.save! + end +end diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 7d645b9860..f635ad95c4 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -4,7 +4,7 @@ = form_for [main_app, :admin, @enterprise], html: { name: "enterprise_form", "ng-controller" => 'enterpriseCtrl', 'onchange' => 'angular.element(enterprise_form).scope().setFormDirty()', - } do |f| + "ng-cloak" => true } do |f| %save-bar{ dirty: "enterprise_form.$dirty", persist: "true" } %input.red{ type: "button", value: t(:update), ng: { click: "submit()", disabled: "!enterprise_form.$dirty" } } diff --git a/app/views/admin/enterprises/form/_payment_methods.html.haml b/app/views/admin/enterprises/form/_payment_methods.html.haml index efcde16dc0..30a5852816 100644 --- a/app/views/admin/enterprises/form/_payment_methods.html.haml +++ b/app/views/admin/enterprises/form/_payment_methods.html.haml @@ -1,3 +1,6 @@ +- if Spree::Config.stripe_connect_enabled || @enterprise.stripe_account + = render 'admin/enterprises/form/stripe_connect' + - if @payment_methods.count > 0 %table %thead diff --git a/app/views/admin/enterprises/form/_stripe_connect.html.haml b/app/views/admin/enterprises/form/_stripe_connect.html.haml new file mode 100644 index 0000000000..be0f4dd157 --- /dev/null +++ b/app/views/admin/enterprises/form/_stripe_connect.html.haml @@ -0,0 +1,15 @@ += render 'admin/enterprises/form/stripe_connect/confirm_modal' + +- if @stripe_account = @enterprise.stripe_account + .stripe-info + .row + = t('.stripe_account_connected') + .row + =link_to t('.disconnect'), main_app.admin_stripe_account_path(@stripe_account), method: :delete, class: 'button' +- else + .row.stripe-info + .six.columns.alpha + =t('.stripe_connect_intro') + .five.columns.omega.text-right + %a.stripe-connect.help-modal{ template: 'admin/modals/stripe_connect_confirm.html' } + %span= t('.connect_with_stripe') diff --git a/app/views/admin/enterprises/form/stripe_connect/_confirm_modal.html.haml b/app/views/admin/enterprises/form/stripe_connect/_confirm_modal.html.haml new file mode 100644 index 0000000000..ac16e619a5 --- /dev/null +++ b/app/views/admin/enterprises/form/stripe_connect/_confirm_modal.html.haml @@ -0,0 +1,27 @@ +%script{ type: "text/ng-template", id: "admin/modals/stripe_connect_confirm.html" } + -# Recommended info to impart (from Stripe Connect docs): + -# indicate to the user what you’re responsible for and what they’ll be expected to do. It’s particularly important to communicate: + -# That they’ll need to create and maintain their Stripe account. + -# That they’ll need to handle chargebacks and all customer service issues. + -# Who is responsible for paying the Stripe fees. + -# What, if any, fees the platform charges. + + #stripe-connect-confirm + .margin-bottom-30.text-center + .text-big + = t('.title') + + .margin-bottom-30 + %p= t('.part1') + + .margin-bottom-30 + %p= t('.part2') + + .margin-bottom-30 + %p= t('.part3') + + .text-center + %a.button.icon-ok{ href: main_app.connect_admin_stripe_accounts_path(enterprise_id: @enterprise) } + = t('.i_agree') + %a.button.red.icon-remove{ href: 'javascript:void(0)', ng: { click: 'close()' } } + = t('.cancel') diff --git a/app/views/admin/order_cycles/_add_exchange_form.html.haml b/app/views/admin/order_cycles/_add_exchange_form.html.haml index f9f64ab631..36c8e804af 100644 --- a/app/views/admin/order_cycles/_add_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_add_exchange_form.html.haml @@ -3,4 +3,4 @@ {{ enterprise.name }} = "{{ enterprise.issues_summary_#{type} ? '('+enterprise.issues_summary_#{type}+')' : '' }}" -= f.submit "Add #{type}", 'ng-click' => "add#{type.capitalize}($event)", 'ng-disabled' => "!new_#{type}_id || !OrderCycle.novel#{type.capitalize}(new_#{type}_id)" += f.submit t(".add_#{type}"), 'ng-click' => "add#{type.capitalize}($event)", 'ng-disabled' => "!new_#{type}_id || !OrderCycle.novel#{type.capitalize}(new_#{type}_id)" diff --git a/app/views/admin/order_cycles/_exchange_form.html.haml b/app/views/admin/order_cycles/_exchange_form.html.haml index 1275103a50..9209c4cf77 100644 --- a/app/views/admin/order_cycles/_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_form.html.haml @@ -6,7 +6,7 @@ {{ enterpriseTotalVariants(enterprises[exchange.enterprise_id]) }} - else {{ (incomingExchangeVariantsFor(exchange.enterprise_id)).length }} - selected + = t('.selected') - if type == 'supplier' %td.receival-details = text_field_tag 'order_cycle_incoming_exchange_{{ $index }}_receival_instructions', '', 'id' => 'order_cycle_incoming_exchange_{{ $index }}_receival_instructions', 'placeholder' => t('.receival_instructions_placeholder'), 'ng-model' => 'exchange.receival_instructions' @@ -15,10 +15,10 @@ {{ exchange.tags.length }} %td.collection-details = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', 'required' => 'required', 'placeholder' => t('.pickup_time_placeholder'), 'ng-model' => 'exchange.pickup_time', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator', 'maxlength' => 35 - %span.icon-question-sign{'ofn-with-tip' => t('admin.order_cycles.edit.pickup_time_tip')} + %span.icon-question-sign{'ofn-with-tip' => t('.pickup_time_tip')} %br/ = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', 'placeholder' => t('.pickup_instructions_placeholder'), 'ng-model' => 'exchange.pickup_instructions', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' - %span.icon-question-sign{'ofn-with-tip' => t('admin.order_cycles.edit.pickup_instructions_tip')} + %span.icon-question-sign{'ofn-with-tip' => t('.pickup_instructions_tip')} %td.fees %ol{ ng: { show: 'enterprises[exchange.enterprise_id].managed || order_cycle.viewing_as_coordinator' } } %li{'ng-repeat' => 'enterprise_fee in exchange.enterprise_fees'} @@ -28,7 +28,7 @@ = link_to 'Remove', '#', {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_remove', 'ng-click' => 'removeExchangeFee($event, exchange, $index)'} - = f.submit 'Add fee', 'ng-click' => 'addExchangeFee($event, exchange)', 'ng-hide' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' + = f.submit t('.add_fee'), 'ng-click' => 'addExchangeFee($event, exchange)', 'ng-hide' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' %td.actions %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text remove-exchange"} diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index ddfd10b38a..ed7b93571a 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -12,7 +12,7 @@ - if can? :notify_producers, @order_cycle %li - = button_to "Notify producers", main_app.notify_producers_admin_order_cycle_path, :id => 'admin_notify_producers', :confirm => t(:are_you_sure) + = button_to t(:notify_producers), main_app.notify_producers_admin_order_cycle_path, :id => 'admin_notify_producers', :confirm => t(:are_you_sure) %li %button#toggle_settings{ onClick: 'toggleSettings()' } = t('.advanced_settings') @@ -30,7 +30,7 @@ %save-bar{ dirty: "order_cycle_form.$dirty", persist: "true" } %input.red{ type: "button", value: t(:update), ng: { click: "submit($event, null)", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } } %input.red{ type: "button", value: t('.update_and_close'), ng: { click: "submit($event, '#{main_app.admin_order_cycles_path}')", disabled: "!order_cycle_form.$dirty || order_cycle_form.$invalid" } } - %input{ type: "button", ng: { value: "order_cycle_form.$dirty ? 'Cancel' : 'Close'", click: "cancel('#{main_app.admin_order_cycles_path}')" } } + %input{ type: "button", ng: { value: "order_cycle_form.$dirty ? '#{t(:cancel)}' : '#{t(:close)}'", click: "cancel('#{main_app.admin_order_cycles_path}')" } } - if order_cycles_simple_form = render 'simple_form', f: f diff --git a/app/views/admin/stripe_connect_settings/edit.html.haml b/app/views/admin/stripe_connect_settings/edit.html.haml new file mode 100644 index 0000000000..43d90b4574 --- /dev/null +++ b/app/views/admin/stripe_connect_settings/edit.html.haml @@ -0,0 +1,53 @@ += render :partial => 'spree/admin/shared/configuration_menu' + +- content_for :page_title do + = t('.title') + +%fieldset.no-border-bottom + %legend + = t('.settings') + .alert-box.warning + = t(".stripe_beta_warning") + = form_for @settings, as: :settings, url: main_app.admin_stripe_connect_settings_path, :method => :put do |f| + .row + .twelve.columns.alpha.omega + .field + - disabled = !@settings.stripe_connect_enabled && @stripe_account[:status] != :ok + = f.label :stripe_connect_enabled, t('.stripe_connect_enabled') + = f.check_box :stripe_connect_enabled, disabled: disabled + .row + .twelve.columns.alpha.omega.form-buttons{"data-hook" => "buttons"} + = button t(:update), 'icon-refresh', value: "update" + +%fieldset.no-border-bottom + %legend= t('.status') + %strong= t('.configuration_explanation_html') + - if @stripe_account[:status] == :ok + .alert-box.ok + .status + %strong= t(".status") + ":" + = t(".ok") + %i.icon-ok + .business_name + %strong= t(".business_name") + ":" + = @stripe_account[:business_name] + .charges_enabled + %strong + - enabled_text = t(@stripe_account[:charges_enabled] ? :say_yes : :say_no) + = t(".charges_enabled") + ":" + = enabled_text + .account_id + %strong= t(".account_id") + ":" + = @stripe_account[:id] + .secret_key + %strong= t('.instance_secret_key') + ":" + = @obfuscated_secret_key + .publishable_key + %strong= t('.instance_publishable_key') + ":" + = Stripe.publishable_key + - if !@stripe_account[:charges_enabled] + .alert-box.warning + = t(".charges_enabled_warning") + - else + .alert-box.error + = t(".#{@stripe_account[:status]}_error") diff --git a/app/views/checkout/_form.html.haml b/app/views/checkout/_form.html.haml index 42f31f4a35..0fdfe267b8 100644 --- a/app/views/checkout/_form.html.haml +++ b/app/views/checkout/_form.html.haml @@ -6,6 +6,7 @@ = inject_available_shipping_methods = inject_available_payment_methods + = inject_saved_credit_cards = render "checkout/details", f: f = render "checkout/billing", f: f diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index 928578158f..ab3ad309be 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -1,4 +1,8 @@ %fieldset#payment + - if Stripe.publishable_key + :javascript + angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}")) + %ng-form{"ng-controller" => "PaymentCtrl", name: "payment"} %h5{"ng-class" => "{valid: payment.$valid, dirty: payment.$dirty || submitted}"} diff --git a/app/views/json/_flash.rabl b/app/views/json/_flash.rabl index 2900b2b948..5a90e00f02 100644 --- a/app/views/json/_flash.rabl +++ b/app/views/json/_flash.rabl @@ -1,2 +1,2 @@ object OpenStruct.new(flash.to_hash) -attributes :info, :success, :error +attributes :info, :success, :error, :notice diff --git a/app/views/layouts/darkswarm.html.haml b/app/views/layouts/darkswarm.html.haml index 298083425c..c702848e89 100644 --- a/app/views/layouts/darkswarm.html.haml +++ b/app/views/layouts/darkswarm.html.haml @@ -13,6 +13,7 @@ %link{href: "https://fonts.googleapis.com/css?family=Roboto:400,300italic,400italic,300,700,700italic|Oswald:300,400,700", rel: "stylesheet", type: "text/css"} = yield :scripts + %script{:src => "https://js.stripe.com/v3/", :type => "text/javascript"} %script{src: "//maps.googleapis.com/maps/api/js?libraries=places,geometry#{ ENV['GOOGLE_MAPS_API_KEY'] ? '&key=' + ENV['GOOGLE_MAPS_API_KEY'] : ''} "} = split_stylesheet_link_tag "darkswarm/all" = javascript_include_tag "darkswarm/all" @@ -21,7 +22,7 @@ = render "layouts/bugherd_script" = csrf_meta_tags - %body.off-canvas{class: @shopfront_layout, ng: {app: "Darkswarm"}} + %body{class: body_classes, ng: {app: "Darkswarm"}} / [if lte IE 8] = render partial: "shared/ie_warning" = javascript_include_tag "iehack" @@ -38,7 +39,7 @@ .off-canvas-wrap{offcanvas: true} .inner-wrap - = render "shared/menu/menu" + = render "shared/menu/menu" unless @hide_menu %section{ role: "main" } = yield diff --git a/app/views/registration/steps/_contact.html.haml b/app/views/registration/steps/_contact.html.haml index 3b7d43ae72..dd282f4ca3 100644 --- a/app/views/registration/steps/_contact.html.haml +++ b/app/views/registration/steps/_contact.html.haml @@ -12,18 +12,18 @@ .small-12.medium-12.large-7.columns .row .small-12.columns.field - %label{ for: 'enterprise_contact' } {{'enterprise_contact' | t}}: - %input.chunky.small-12.columns{ id: 'enterprise_contact', name: 'contact', required: true, placeholder: "Contact Name", ng: { model: 'enterprise.contact' } } + %label{ for: 'enterprise_contact' } {{'enterprise.registration.modal.steps.contact.contact_field' | t}}: + %input.chunky.small-12.columns{ id: 'enterprise_contact', name: 'contact', required: true, placeholder: "{{'enterprise.registration.modal.steps.contact.contact_field_placeholder' | t}}", ng: { model: 'enterprise.contact' } } %span.error.small-12.columns{ ng: { show: "contact.contact.$error.required && submitted" } } - {{'enterprise_contact_required' | t}} + {{'enterprise.registration.modal.steps.contact.contact_field_required' | t}} .row .small-12.columns.field - %label{ for: 'enterprise_email_address' } {{'enterprise_email_address' | t}}: - %input.chunky.small-12.columns{ id: 'enterprise_email_address', name: 'email_address', type: 'email', placeholder: "eg. charlie@thefarm.com", ng: { model: 'enterprise.email_address' } } + %label{ for: 'enterprise_email_address' } {{'enterprise.registration.modal.steps.contact.email_field' | t}}: + %input.chunky.small-12.columns{ id: 'enterprise_email_address', name: 'email_address', type: 'email', placeholder: "{{'enterprise.registration.modal.steps.contact.email_field_placeholder' | t}}", ng: { model: 'enterprise.email_address' } } .row .small-12.columns.field - %label{ for: 'enterprise_phone' } {{'enterprise_phone' | t}}: - %input.chunky.small-12.columns{ id: 'enterprise_phone', name: 'phone', placeholder: "eg. (03) 1234 5678", ng: { model: 'enterprise.phone' } } + %label{ for: 'enterprise_phone' } {{'enterprise.registration.modal.steps.contact.phone_field' | t}}: + %input.chunky.small-12.columns{ id: 'enterprise_phone', name: 'phone', placeholder: "{{'enterprise.registration.modal.steps.contact.phone_field_placeholder' | t}}", ng: { model: 'enterprise.phone' } } .small-12.medium-12.large-5.hide-for-small-only .row.buttons diff --git a/app/views/registration/steps/_details.html.haml b/app/views/registration/steps/_details.html.haml index 4a01d92385..4fce6a4319 100644 --- a/app/views/registration/steps/_details.html.haml +++ b/app/views/registration/steps/_details.html.haml @@ -4,59 +4,59 @@ .row .small-12.columns %header - %h2 {{'registration_detail_headline' | t}} - %h5{ ng: { if: "::enterprise.type != 'own'" } } {{'registration_detail_enterprise' | t}} - %h5{ ng: { if: "::enterprise.type == 'own'" } } {{'registration_detail_producer' | t}} + %h2 {{'enterprise.registration.modal.steps.details.headline' | t}} + %h5{ ng: { if: "::enterprise.type != 'own'" } } {{'enterprise.registration.modal.steps.details.enterprise' | t}} + %h5{ ng: { if: "::enterprise.type == 'own'" } } {{'enterprise.registration.modal.steps.details.producer' | t}} %form{ name: 'details', novalidate: true, ng: { controller: "RegistrationFormCtrl", submit: "selectIfValid('contact',details)" } } .row .small-12.medium-9.large-12.columns.end .field - %label{ for: 'enterprise_name', ng: { if: "::enterprise.type != 'own'" } } {{'registration_detail_name_enterprise' | t}} - %label{ for: 'enterprise_name', ng: { if: "::enterprise.type == 'own'" } } {{'registration_detail_name_producer' | t}} - %input.chunky{ id: 'enterprise_name', name: 'name', placeholder: "{{'registration_detail_name_placeholder' | t}}", required: true, ng: { model: 'enterprise.name' } } + %label{ for: 'enterprise_name', ng: { if: "::enterprise.type != 'own'" } } {{'enterprise.registration.modal.steps.details.enterprise_name_field' | t}} + %label{ for: 'enterprise_name', ng: { if: "::enterprise.type == 'own'" } } {{'enterprise.registration.modal.steps.details.producer_name_field' | t}} + %input.chunky{ id: 'enterprise_name', name: 'name', placeholder: "{{'enterprise.registration.modal.steps.details.producer_name_field_placeholder' | t}}", required: true, ng: { model: 'enterprise.name' } } %span.error{ ng: { show: "details.name.$error.required && submitted" } } - {{'registration_detail_name_error' | t}} + {{'enterprise.registration.modal.steps.details.producer_name_field_error' | t}} .row .small-12.medium-9.large-6.columns .field - %label{ for: 'enterprise_address' } {{'registration_detail_address1' | t}} - %input.chunky{ id: 'enterprise_address', name: 'address1', required: true, placeholder: "{{'registration_detail_address1_placeholder' | t}}", required: true, ng: { model: 'enterprise.address.address1' } } + %label{ for: 'enterprise_address' } {{'enterprise.registration.modal.steps.details.address1_field' | t}} + %input.chunky{ id: 'enterprise_address', name: 'address1', required: true, placeholder: "{{'enterprise.registration.modal.steps.details.address1_field_placeholder' | t}}", required: true, ng: { model: 'enterprise.address.address1' } } %span.error{ ng: { show: "details.address1.$error.required && submitted" } } - {{'registration_detail_address1_error' | t}} + {{'enterprise.registration.modal.steps.details.address1_field_error' | t}} .field - %label{ for: 'enterprise_address2' } {{'registration_detail_address2' | t}} + %label{ for: 'enterprise_address2' } {{'enterprise.registration.modal.steps.details.address2_field' | t}} %input.chunky{ id: 'enterprise_address2', name: 'address2', required: false, placeholder: "", required: false, ng: { model: 'enterprise.address.address2' } } .small-12.medium-9.large-6.columns.end .row .small-12.medium-8.large-8.columns .field - %label{ for: 'enterprise_city' } {{'registration_detail_suburb' | t}} - %input.chunky{ id: 'enterprise_city', name: 'city', required: true, placeholder: "{{'registration_detail_suburb_placeholder' | t}}", ng: { model: 'enterprise.address.city' } } + %label{ for: 'enterprise_city' } {{'enterprise.registration.modal.steps.details.suburb_field' | t}} + %input.chunky{ id: 'enterprise_city', name: 'city', required: true, placeholder: "{{'enterprise.registration.modal.steps.details.suburb_field_placeholder' | t}}", ng: { model: 'enterprise.address.city' } } %span.error{ ng: { show: "details.city.$error.required && submitted" } } - {{'registration_detail_suburb_error' | t}} + {{'enterprise.registration.modal.steps.details.suburb_field_error' | t}} .small-12.medium-4.large-4.columns .field - %label{ for: 'enterprise_zipcode' } {{'registration_detail_postcode' | t}} - %input.chunky{ id: 'enterprise_zipcode', name: 'zipcode', required: true, placeholder: "{{'registration_detail_postcode_placeholder' | t}}", ng: { model: 'enterprise.address.zipcode' } } + %label{ for: 'enterprise_zipcode' } {{'enterprise.registration.modal.steps.details.postcode_field' | t}} + %input.chunky{ id: 'enterprise_zipcode', name: 'zipcode', required: true, placeholder: "{{'enterprise.registration.modal.steps.details.postcode_field_placeholder' | t}}", ng: { model: 'enterprise.address.zipcode' } } %span.error{ ng: { show: "details.zipcode.$error.required && submitted" } } - {{'registration_detail_postcode_error' | t}} + {{'enterprise.registration.modal.steps.details.postcode_field_error' | t}} .row .small-12.medium-4.large-4.columns .field - %label{ for: 'enterprise_state' } {{'registration_detail_state' | t}} + %label{ for: 'enterprise_state' } {{'enterprise.registration.modal.steps.details.state_field' | t}} %select.chunky{ id: 'enterprise_state', name: 'state', ng: { model: 'enterprise.address.state_id', options: 's.id as s.abbr for s in enterprise.country.states', show: 'countryHasStates()', required: 'countryHasStates()' } } %span.error{ ng: { show: "details.state.$error.required && submitted" } } - {{'registration_detail_state_error' | t}} + {{'enterprise.registration.modal.steps.details.state_field_error' | t}} .small-12.medium-8.large-8.columns .field - %label{ for: 'enterprise_country' } {{'registration_detail_country' | t}} + %label{ for: 'enterprise_country' } {{'enterprise.registration.modal.steps.details.country_field' | t}} %select.chunky{ id: 'enterprise_country', name: 'country', required: true, ng: { init: "setDefaultCountry(#{Spree::Config[:default_country_id]})", model: 'enterprise.country', options: 'c as c.name for c in countries' } } %span.error{ ng: { show: "details.country.$error.required && submitted" } } - {{'registration_detail_country_error' | t}} + {{'enterprise.registration.modal.steps.details.country_field_error' | t}} .row.buttons diff --git a/app/views/registration/steps/_steps.html.haml b/app/views/registration/steps/_steps.html.haml index 7ea0412929..3538d5ff48 100644 --- a/app/views/registration/steps/_steps.html.haml +++ b/app/views/registration/steps/_steps.html.haml @@ -1,4 +1,5 @@ %script{ type: "text/ng-template", id: "registration/steps.html" } .row#progress-bar .small-12.medium-2.columns.item{ ng: { repeat: 'step in steps', class: "{active: (currentStep() == step),'show-for-medium-up': (currentStep() != step)}" } } - {{ $index+1 + ". " + step }} + {{ $index+1 + ". " + ('enterprise.registration.modal.steps.' + step + '.title' | t) }} + diff --git a/app/views/registration/steps/_type.html.haml b/app/views/registration/steps/_type.html.haml index 4c93c441f4..9276a5731a 100644 --- a/app/views/registration/steps/_type.html.haml +++ b/app/views/registration/steps/_type.html.haml @@ -6,9 +6,9 @@ .row .small-12.columns %header - %h2{ "ng-bind" => "'registration_type_headline' | t:{enterprise: enterprise.name}" } + %h2{ "ng-bind" => "'enterprise.registration.modal.steps.type.headline' | t:{enterprise: enterprise.name}" } %h4 - {{'registration_type_question' | t}} + {{'enterprise.registration.modal.steps.type.question' | t}} %form{ name: 'type', novalidate: true, ng: { controller: "RegistrationFormCtrl", submit: "create(type)" } } .row#enterprise-types{ 'data-equalizer' => true, ng: { if: "::enterprise.type != 'own'" } } @@ -17,32 +17,32 @@ .small-12.medium-6.large-6.columns{ 'data-equalizer-watch' => true } %a.btnpanel#producer-panel{ href: "#", ng: { click: "enterprise.is_primary_producer = true", class: "{selected: enterprise.is_primary_producer}" } } %i.ofn-i_059-producer - %h4 {{'registration_type_producer' | t}} + %h4 {{'enterprise.registration.modal.steps.type.yes_producer' | t}} .small-12.medium-6.large-6.columns{ 'data-equalizer-watch' => true } %a.btnpanel#hub-panel{ href: "#", ng: { click: "enterprise.is_primary_producer = false", class: "{selected: enterprise.is_primary_producer == false}" } } %i.ofn-i_063-hub - %h4 {{'registration_type_no_producer' | t}} + %h4 {{'enterprise.registration.modal.steps.type.no_producer' | t}} .row .small-12.columns %input.chunky{ id: 'enterprise_is_primary_producer', name: 'is_primary_producer', hidden: true, required: true, ng: { model: 'enterprise.is_primary_producer' } } %span.error{ ng: { show: "type.is_primary_producer.$error.required && submitted" } } - {{'registration_type_error' | t}} + {{'enterprise.registration.modal.steps.type.producer_field_error' | t}} .row .small-12.columns .panel.callout .left %i.ofn-i_013-help   - %p {{'registration_type_producer_help' | t}} + %p {{'enterprise.registration.modal.steps.type.yes_producer_help' | t}} .panel.callout .left %i.ofn-i_013-help   - %p {{'registration_type_no_producer_help' | t}} + %p {{'enterprise.registration.modal.steps.type.no_producer_help' | t}} .row.buttons .small-12.columns %input.button.secondary{ type: "button", value: "{{'back' | t}}", ng: { click: "select('contact')" } } - %input.button.primary.right{ type: "submit", value: "{{'create_profile' | t}}" } + %input.button.primary.right{ ng: { disabled: 'isDisabled' }, type: "submit", value: "{{'create_profile' | t}}" } diff --git a/app/views/shops/_filters.html.haml b/app/views/shops/_filters.html.haml index 42e2b2ec28..dedc473808 100644 --- a/app/views/shops/_filters.html.haml +++ b/app/views/shops/_filters.html.haml @@ -1,5 +1,5 @@ - resource ||= "visibleMatches" -- property_filters ||= "| filter:filterExpression | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles" +- property_filters ||= "| closedShops:show_closed | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles" .row = render 'shared/components/filter_controls' diff --git a/app/views/shops/_hubs.html.haml b/app/views/shops/_hubs.html.haml index 6087d2330c..3e477b9551 100644 --- a/app/views/shops/_hubs.html.haml +++ b/app/views/shops/_hubs.html.haml @@ -28,8 +28,8 @@ %a{href: "", "ng-click" => "showDistanceMatches()"} = t :hubs_distance_filter, location: "{{ nameMatchesFiltered[0].name }}" .more-controls - %a.button{href: "", ng: {click: "showClosedShops()", show: "filterExpression.active"}} + %a.button{href: "", ng: {click: "showClosedShops()", show: "!show_closed"}} = t '.show_closed_shops' - %a.button{href: "", ng: {click: "hideClosedShops()", show: "!filterExpression.active"}} + %a.button{href: "", ng: {click: "hideClosedShops()", show: "show_closed"}} = t '.hide_closed_shops' %a.button{href: main_app.map_path}= t '.show_on_map' diff --git a/app/views/shops/_hubs_table.html.haml b/app/views/shops/_hubs_table.html.haml index 6d5803b477..0c0b1a4442 100644 --- a/app/views/shops/_hubs_table.html.haml +++ b/app/views/shops/_hubs_table.html.haml @@ -1,5 +1,5 @@ .active_table - %hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | filter:filterExpression | taxons:activeTaxons | properties:activeProperties:'distributed_properties' | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+distance', '+orders_close_at'])", + %hub.active_table_node.row{"ng-repeat" => "hub in #{enterprises}Filtered = (#{enterprises} | closedShops:show_closed | taxons:activeTaxons | properties:activeProperties:'distributed_properties' | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+distance', '+orders_close_at'])", "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", "ng-controller" => "HubNodeCtrl", id: "{{hub.hash}}"} 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 e227180fc3..c64ad9f1d2 100644 --- a/app/views/spree/admin/payment_methods/_provider_settings.html.haml +++ b/app/views/spree/admin/payment_methods/_provider_settings.html.haml @@ -1,7 +1,11 @@ -- if @payment_method.preferences.present? - %fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields - %legend{ align: "center"} - = t(:provider_settings) - .preference-settings - = fields_for :payment_method, @payment_method do |payment_method_form| - = preference_fields(@payment_method, payment_method_form) \ No newline at end of file +- case @payment_method +- when Spree::Gateway::StripeConnect + = render 'stripe_connect' +- else + - if @payment_method.preferences.present? + %fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields + %legend{ align: "center"} + = t(:provider_settings) + .preference-settings + = fields_for :payment_method, @payment_method do |payment_method_form| + = preference_fields(@payment_method, payment_method_form) diff --git a/app/views/spree/admin/payment_methods/_stripe_connect.html.haml b/app/views/spree/admin/payment_methods/_stripe_connect.html.haml new file mode 100644 index 0000000000..0e9ff2f614 --- /dev/null +++ b/app/views/spree/admin/payment_methods/_stripe_connect.html.haml @@ -0,0 +1,48 @@ +%fieldset.alpha.eleven.columns.no-border-bottom#gateway_fields + %legend{ align: "center"} + = t(:provider_settings) + .preference-settings{ ng: { controller: "StripeController" } } + = fields_for :payment_method, @payment_method do |payment_method_form| + = payment_method_form.label :stripe_account_owner + %br + - if @stripe_account_holder.nil? || spree_current_user.enterprises.include?(@stripe_account_holder) + %input.ofn-select2.fullwidth#payment_method_preferred_enterprise_id{ type: 'number', + name: 'payment_method[preferred_enterprise_id]', + placeholder: t(".enterprise_select_placeholder"), + data: 'shops', ng: { model: 'paymentMethod.preferred_enterprise_id' } } + - else + %strong= Enterprise.find_by_id(@payment_method).andand.name + + #stripe-account-status{ ng: { show: "paymentMethod.preferred_enterprise_id" } } + .alert-box.warning{ ng: { hide: "stripe_account.status" } } + = t(".loading_account_information_msg") + + .alert-box.error{ ng: { show: "stripe_account.status == 'stripe_disabled'" } } + = t(".stripe_disabled_msg") + + .alert-box.error{ ng: { show: "stripe_account.status == 'request_failed'" } } + = t(".request_failed_msg") + + .alert-box.error{ ng: { show: "stripe_account.status == 'account_missing'" } } + = t(".account_missing_msg") + %a.button{ ng: { href: "{{current_enterprise_stripe_path()}}" }, target: 'blank' } + = t(".connect_one") + %i.icon-chevron-right + + .alert-box.error{ ng: { show: "stripe_account.status == 'access_revoked'" } } + = t(".access_revoked_msg") + + .alert-box.ok{ ng: { show: "stripe_account.status == 'connected'" } } + .status + %strong= t(".status") + ":" + = t(".connected") + %i.icon-ok + .account_id + %strong= t(".account_id") + ":" + {{ stripe_account.id }} + .business_name + %strong= t(".business_name") + ":" + {{ stripe_account.business_name }} + .charges_enabled + %strong= t(".charges_enabled") + ":" + {{ stripe_account.charges_enabled ? "say_yes" : "say_no" | t }} diff --git a/app/views/spree/admin/payments/_form.html.erb b/app/views/spree/admin/payments/_form.html.erb new file mode 100644 index 0000000000..fe019007b1 --- /dev/null +++ b/app/views/spree/admin/payments/_form.html.erb @@ -0,0 +1,34 @@ +
+
+
+ <%= f.label :amount, t(:amount) %> + <%= f.text_field :amount, :value => @order.outstanding_balance, :class => 'fullwidth' %> +
+
+
+
+ +
    + <% @payment_methods.each do |method| %> +
  • + +
  • + <% end %> +
+ +
+ <% @payment_methods.each do |method| %> +
+ <% if method.source_required? %> +
+ <%= render :partial => "spree/admin/payments/source_forms/#{method.method_type}", :locals => { :payment_method => method } %> + <% end %> +
+ <% end %> +
+
+
+
diff --git a/app/views/spree/admin/payments/source_forms/_gateway.html.erb b/app/views/spree/admin/payments/source_forms/_gateway.html.erb new file mode 100644 index 0000000000..8cf4b914cc --- /dev/null +++ b/app/views/spree/admin/payments/source_forms/_gateway.html.erb @@ -0,0 +1,58 @@ +
+
+ <% @previous_cards.each do |card| %> + + <% end %> + +
+ +
+ <% param_prefix = "payment_source[#{payment_method.id}]" %> + +
+ +
+
+
+ <%= hidden_field_tag "#{param_prefix}[cc_type]", '', {class: 'ccType'} %> + <%= label_tag "card_number#{payment_method.id}", raw(t(:card_number) + content_tag(:span, ' *', :class => 'required')) %> + <%= text_field_tag "#{param_prefix}[number]", '', :class => 'required fullwidth cardNumber', :id => "card_number#{payment_method.id}", :maxlength => 19 %> + +
+
+
+
+
+
+ <%= label_tag "card_name#{payment_method.id}", raw(t(:name) + content_tag(:span, ' *', :class => 'required')) %> + <%= text_field_tag "#{param_prefix}[name]", '', id: "card_name#{payment_method.id}", class: 'required fullwidth', maxlength: 19 %> +
+
+
+
+
+ <%= label_tag "card_expiry#{payment_method.id}", raw(t(:expiration) + content_tag(:span, ' *', :class => 'required')) %>
+ <%= text_field_tag "#{param_prefix}[expiry]", '', id: "card_expiry#{payment_method.id}", class: "required cardExpiry", placeholder: "MM / YY" %> +
+
+
+
+ <%= label_tag "card_code#{payment_method.id}", raw(t(:card_code) + content_tag(:span, ' *', :class => "required")) %> + <%= text_field_tag "#{param_prefix}[verification_value]", '', id: "card_code#{payment_method.id}", class: 'required fullwidth cardCode', size: 5 %> + + (<%= t(:what_is_this) %>) + +
+
+ +
+ <%= image_tag 'credit_cards/credit_card.gif', :class => 'credit-card-image' %> +
+ +
+
+
diff --git a/app/views/spree/admin/payments/source_forms/_paypal.html.haml b/app/views/spree/admin/payments/source_forms/_paypal.html.haml new file mode 100644 index 0000000000..e26db66846 --- /dev/null +++ b/app/views/spree/admin/payments/source_forms/_paypal.html.haml @@ -0,0 +1,8 @@ +-# We can remove this file as soon as we have a version of better_spree_paypal_express that includes: +-# https://github.com/spree-contrib/better_spree_paypal_express/commit/4360a1fb82d30d7601bc6a98e7b74819f0b377f0 + +-# The selectors in app/assets/javascripts/spree/backend/paypal_express.js don't work with the version +-# of the views we are using, so the warning below wasn't displaying without this override. + +#paypal-warning + %strong= t('no_payment_via_admin_backend', :scope => 'paypal') diff --git a/app/views/spree/admin/payments/source_forms/_stripe.html.haml b/app/views/spree/admin/payments/source_forms/_stripe.html.haml new file mode 100644 index 0000000000..24bde74a31 --- /dev/null +++ b/app/views/spree/admin/payments/source_forms/_stripe.html.haml @@ -0,0 +1,4 @@ +-# = render "spree/admin/payments/source_forms/gateway", payment_method: payment_method + +%strong + = t('.no_payment_via_admin_backend') diff --git a/app/views/spree/admin/payments/source_views/_stripe.html.haml b/app/views/spree/admin/payments/source_views/_stripe.html.haml new file mode 100644 index 0000000000..64af07bd3b --- /dev/null +++ b/app/views/spree/admin/payments/source_views/_stripe.html.haml @@ -0,0 +1 @@ += render "spree/admin/payments/source_views/gateway", payment: payment diff --git a/app/views/spree/admin/products/_display_as.html.haml b/app/views/spree/admin/products/_display_as.html.haml index 33beec2b86..04af12bdce 100644 --- a/app/views/spree/admin/products/_display_as.html.haml +++ b/app/views/spree/admin/products/_display_as.html.haml @@ -1,4 +1,4 @@ .three.columns.omega{ "ng-if" => "product.variant_unit_with_scale != 'items'" } = f.field_container :display_as do - = f.label :product_display_as, t(:display_as) - %input#product_display_as.fullwidth{name: "product[display_as]", placeholder: "{{ placeholder_text }}", type: "text"} \ No newline at end of file + = f.label :product_display_as, t('.display_as') + %input#product_display_as.fullwidth{name: "product[display_as]", placeholder: "{{ placeholder_text }}", type: "text"} diff --git a/app/views/spree/admin/products/_group_buy_form.html.haml b/app/views/spree/admin/products/_group_buy_form.html.haml index 8f1de2a884..6ccf23a024 100644 --- a/app/views/spree/admin/products/_group_buy_form.html.haml +++ b/app/views/spree/admin/products/_group_buy_form.html.haml @@ -1,14 +1,14 @@ = f.field_container :group_buy do - = f.label :group_buy, 'Group buy?' + = f.label :group_buy, t('.group_buy') %br .alpha.two.columns - = f.radio_button :group_buy, '1', :checked => f.object.group_buy - = f.label :group_buy_1, 'Yes' + = f.label :group_buy_1, t(:yes) + = f.radio_button :group_buy, '1', checked: f.object.group_buy .alpha.two.columns - = f.radio_button :group_buy, '0', :checked => !f.object.group_buy - = f.label :group_buy_0, 'No' -%br.clear -= f.field_container :group_buy_unit_size do - = f.label :group_buy_unit_size, "Bulk unit size" - %br - = f.text_field :group_buy_unit_size + = f.label :group_buy_0, t(:no) + = f.radio_button :group_buy, '0', checked: !f.object.group_buy + %br.clear + = f.field_container :group_buy_unit_size do + = f.label :group_buy_unit_size, t('.bulk_unit_size') + %br + = f.text_field :group_buy_unit_size diff --git a/app/views/spree/admin/products/_primary_taxon_form.html.haml b/app/views/spree/admin/products/_primary_taxon_form.html.haml index 8ffe5887a7..51da47f0f4 100644 --- a/app/views/spree/admin/products/_primary_taxon_form.html.haml +++ b/app/views/spree/admin/products/_primary_taxon_form.html.haml @@ -1,5 +1,5 @@ = f.field_container :primary_taxon_id do - = f.label :primary_taxon_id, t(:product_category) + = f.label :primary_taxon_id, t('.product_category') %span.required * %br = f.collection_select(:primary_taxon_id, Spree::Taxon.all, :id, :name, {:include_blank => true}, {:class => "select2 fullwidth"}) diff --git a/app/views/spree/admin/products/_seo_form.html.haml b/app/views/spree/admin/products/_seo_form.html.haml new file mode 100644 index 0000000000..bbc5bdb4f0 --- /dev/null +++ b/app/views/spree/admin/products/_seo_form.html.haml @@ -0,0 +1,15 @@ +.row{"data-hook" => "admin_product_meta_form"} + .alpha.eleven.columns + = f.field_container :meta_description do + = f.label :meta_keywords, t(:meta_keywords) + %br/ + = f.text_field :meta_keywords, :class => 'fullwidth', :rows => 6 + = f.field_container :meta_description do + = f.label :meta_description, t(:meta_description) + %br/ + = f.text_field :meta_description, :class => 'fullwidth', :rows => 6 + .alpha.eleven.columns + = f.field_container :notes do + = f.label :notes, t(:notes) + = f.text_area :notes, { :class => 'fullwidth', rows: 5 } + = f.error_message_on :notes \ No newline at end of file diff --git a/app/views/spree/admin/products/_tax_category_form.html.haml b/app/views/spree/admin/products/_tax_category_form.html.haml index 3d529e6562..250ba8c832 100644 --- a/app/views/spree/admin/products/_tax_category_form.html.haml +++ b/app/views/spree/admin/products/_tax_category_form.html.haml @@ -2,5 +2,5 @@ = f.label :tax_category_id, t(:tax_category) %span.required * %br - = f.collection_select(:tax_category_id, Spree::TaxCategory.all, :id, :name, {:include_blank => Spree::Config.products_require_tax_category ? false : 'None'}, {:class => "select2 fullwidth"}) + = f.collection_select(:tax_category_id, Spree::TaxCategory.all, :id, :name, {:include_blank => Spree::Config.products_require_tax_category ? false : t(:none)}, {:class => "select2 fullwidth"}) = f.error_message_on :tax_category_id diff --git a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml index 9a8f7ad86a..5bd4418ac2 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_head.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_head.html.haml @@ -19,7 +19,7 @@ %thead %tr{ ng: { controller: "ColumnsCtrl" } } %th.left-actions - %a{ 'ng-click' => 'toggleShowAllVariants()', :style => 'color: red' } + %a{ 'ng-click' => 'toggleShowAllVariants()', :style => 'color: red; cursor: pointer' } = t(:expand_all) %th.producer{ 'ng-show' => 'columns.producer.visible' }=t('admin.producer') %th.sku{ 'ng-show' => 'columns.sku.visible' }=t('admin.sku') diff --git a/app/views/spree/admin/products/group_buy_options.html.haml b/app/views/spree/admin/products/group_buy_options.html.haml new file mode 100644 index 0000000000..9261ddbfc6 --- /dev/null +++ b/app/views/spree/admin/products/group_buy_options.html.haml @@ -0,0 +1,8 @@ += render :partial => 'spree/admin/shared/product_sub_menu' += render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Group Buy Options' } += render :partial => 'spree/shared/error_messages', :locals => { :target => @product } + += form_for [:admin, @product], :method => :put, :html => { :multipart => true } do |f| + %fieldset.no-border-top + = render :partial => 'group_buy_form', :locals => { :f => f } + = render :partial => 'spree/admin/shared/edit_resource_links' diff --git a/app/views/spree/admin/products/new.js.erb b/app/views/spree/admin/products/new.js.erb new file mode 100644 index 0000000000..0a3711620d --- /dev/null +++ b/app/views/spree/admin/products/new.js.erb @@ -0,0 +1,12 @@ +<%# This chunk is just a copy of Spree's core/app/views/spree/admin/products/new.js.erb %> +$("#new_product").html('<%= escape_javascript(render :template => "spree/admin/products/new", :formats => [:html], :handlers => [:erb]) %>'); +handle_date_picker_fields(); +<% unless Rails.env.test? %> + $('.select2').select2(); +<% end %> +$("#table-filter").hide(); +$("#admin_new_product").parent().hide(); + +<%# We need to replace the page's title as well. We're navigating to a new page + although through ajax %> +$('#content-header .page-title').html('<%= t('.title') %>'); diff --git a/app/views/spree/admin/products/seo.html.haml b/app/views/spree/admin/products/seo.html.haml new file mode 100644 index 0000000000..ddcd00402f --- /dev/null +++ b/app/views/spree/admin/products/seo.html.haml @@ -0,0 +1,8 @@ += render :partial => 'spree/admin/shared/product_sub_menu' += render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'SEO' } += render :partial => 'spree/shared/error_messages', :locals => { :target => @product } + += form_for [:admin, @product], :method => :put, :html => { :multipart => true } do |f| + %fieldset.no-border-top + = render :partial => 'seo_form', :locals => { :f => f } + = render :partial => 'spree/admin/shared/edit_resource_links' diff --git a/app/views/spree/admin/shared/_routes.html.erb b/app/views/spree/admin/shared/_routes.html.erb new file mode 100644 index 0000000000..27957a3466 --- /dev/null +++ b/app/views/spree/admin/shared/_routes.html.erb @@ -0,0 +1,10 @@ + diff --git a/app/views/spree/checkout/payment/_gateway.html.haml b/app/views/spree/checkout/payment/_gateway.html.haml index 33dcbda8fc..0bcf038cb9 100644 --- a/app/views/spree/checkout/payment/_gateway.html.haml +++ b/app/views/spree/checkout/payment/_gateway.html.haml @@ -2,17 +2,18 @@ .small-6.columns %label = t :first_name - %input{type: :text, disabled: true, "ng-value" => "order.bill_address.firstname"} + -# Changing name not permitted by default (in checkout) - can be enabled by setting an allow_name_change variable in $scope + %input{type: :text, "ng-disabled" => "!allow_name_change", "ng-value" => "order.bill_address.firstname"} .small-6.columns %label = t :last_name - %input{type: :text, disabled: true, "ng-value" => "order.bill_address.lastname"} + %input{type: :text, "ng-disabled" => "!allow_name_change", "ng-value" => "order.bill_address.lastname"} .small-6.columns - = validated_input t(:card_number), "secrets.card_number", required: true, maxlength: 19, autocomplete: "off" + = validated_input t(:card_number), "secrets.card_number", maxlength: 19, autocomplete: "off" .small-6.columns - = validated_input t(:card_securitycode), "secrets.card_verification_value", required: true + = validated_input t(:card_securitycode), "secrets.card_verification_value" .row .small-12.columns @@ -21,6 +22,6 @@ .row .small-6.columns - %select{"ng-model" => "secrets.card_month", "ng-options" => "currMonth.value as currMonth.key for currMonth in months", name: "secrets.card_month", required: true} + %select{"ng-model" => "secrets.card_month", "ng-options" => "currMonth.value as currMonth.key for currMonth in months", name: "secrets.card_month"} .small-6.columns - %select{"ng-model" => "secrets.card_year", "ng-options" => "year for year in years", name: "secrets.card_year", required: true} + %select{"ng-model" => "secrets.card_year", "ng-options" => "year for year in years", name: "secrets.card_year"} diff --git a/app/views/spree/checkout/payment/_stripe.html.haml b/app/views/spree/checkout/payment/_stripe.html.haml new file mode 100644 index 0000000000..3c1bc0c35e --- /dev/null +++ b/app/views/spree/checkout/payment/_stripe.html.haml @@ -0,0 +1,17 @@ +.row{ "ng-show" => "savedCreditCards != null && 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/users/_cards.html.haml b/app/views/spree/users/_cards.html.haml new file mode 100644 index 0000000000..80069c282f --- /dev/null +++ b/app/views/spree/users/_cards.html.haml @@ -0,0 +1,15 @@ +%script{ type: "text/ng-template", id: "account/cards.html" } + .credit_cards{"ng-controller" => "CreditCardsCtrl"} + .row + .small-12.medium-6.columns + %h3= t(:saved_cards) + .saved_cards{ ng: { show: 'savedCreditCards.length > 0' } } + = render 'saved_cards' + .no_cards{ ng: { hide: 'savedCreditCards.length > 0' } } + = t(:you_have_no_saved_cards) + %button.button.primary{ ng: { click: 'showForm()', hide: 'CreditCard.visible' } } + = t(:add_a_card) + + .small-12.medium-6.columns.new_card{ ng: { show: 'CreditCard.visible', class: '{visible: CreditCard.visible}' } } + %h3= t(:add_a_new_card) + = render 'new_card_form' diff --git a/app/views/spree/users/_fat.html.haml b/app/views/spree/users/_fat.html.haml index 4ce051e5b8..1a4c74ed3e 100644 --- a/app/views/spree/users/_fat.html.haml +++ b/app/views/spree/users/_fat.html.haml @@ -9,16 +9,7 @@ %th.order5.text-right= t :value %th.order6.text-right.show-for-large-up= t :outstanding_balance %th.order7.text-right= t :running_balance - %tbody.transaction-group{"ng-repeat" => "order in distributor.distributed_orders", "ng-class-odd"=>"'odd'", "ng-class-even"=>"'even'"} - %tr.order-row - %td.order1 - %a{"ng-href" => "{{::order.path}}", "ng-bind" => "::('order' | t )+ ' ' + order.number"} - %td.order2{"ng-bind" => "::order.completed_at"} - %td.order3.show-for-large-up{"ng-bind" => "::'spree.payment_states.' + order.payment_state | t | capitalize"} - %td.order4.show-for-large-up{"ng-bind" => "::'spree.shipment_states.' + order.shipment_state | t | capitalize"} - %td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","ng-bind" => "::order.total | localizeCurrency"} - %td.order6.text-right.show-for-large-up{"ng-class" => "{'credit' : order.outstanding_balance < 0, 'debit' : order.outstanding_balance > 0, 'paid' : order.outstanding_balance == 0}", "ng-bind" => "::order.outstanding_balance | localizeCurrency"} - %td.order7.text-right{"ng-class" => "{'credit' : order.running_balance < 0, 'debit' : order.running_balance > 0, 'paid' : order.running_balance == 0}", "ng-bind" => "::order.running_balance | localizeCurrency"} + %tbody.transaction-group{"ng-repeat" => "order in shop.orders", "ng-class-odd"=>"'odd'", "ng-class-even"=>"'even'"} %tr.payment-row{"ng-repeat" => "payment in order.payments", "ng-class" => "{'invalid': payment.state != 'completed'}"} %td.order1{"ng-bind" => "::payment.payment_method"} %td.order2{"ng-bind" => "::payment.updated_at"} @@ -29,3 +20,12 @@ %td.order5.text-right{"ng-class" => "{'credit' : payment.amount > 0, 'debit' : payment.amount < 0, 'paid' : payment.amount == 0}","ng-bind" => "::payment.amount | localizeCurrency"} %td.order6.show-for-large-up %td.order7 + %tr.order-row + %td.order1 + %a{"ng-href" => "{{::order.path}}", "ng-bind" => "::('order' | t )+ ' ' + order.number"} + %td.order2{"ng-bind" => "::order.completed_at"} + %td.order3.show-for-large-up{"ng-bind" => "::'spree.payment_states.' + order.payment_state | t | capitalize"} + %td.order4.show-for-large-up{"ng-bind" => "::'spree.shipment_states.' + order.shipment_state | t | capitalize"} + %td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","ng-bind" => "::order.total | localizeCurrency"} + %td.order6.text-right.show-for-large-up{"ng-class" => "{'credit' : order.outstanding_balance < 0, 'debit' : order.outstanding_balance > 0, 'paid' : order.outstanding_balance == 0}", "ng-bind" => "::order.outstanding_balance | localizeCurrency"} + %td.order7.text-right{"ng-class" => "{'credit' : order.runningBalance < 0, 'debit' : order.runningBalance > 0, 'paid' : order.runningBalance == 0}", "ng-bind" => "::order.runningBalance | localizeCurrency"} diff --git a/app/views/spree/users/_form.html.haml b/app/views/spree/users/_form.html.haml new file mode 100644 index 0000000000..61f6962abb --- /dev/null +++ b/app/views/spree/users/_form.html.haml @@ -0,0 +1,6 @@ += render :partial => 'spree/shared/error_messages', :locals => { :target => @user } +%h3= t('.account_settings') += form_for Spree::User.new, :as => @user, :url => spree.user_path(@user), :method => :put do |f| + = render :partial => 'spree/shared/user_form', :locals => { :f => f } + %p + = f.submit t(:update), :class => 'button primary' diff --git a/app/views/spree/users/_new_card_form.html.haml b/app/views/spree/users/_new_card_form.html.haml new file mode 100644 index 0000000000..3d17afbf6a --- /dev/null +++ b/app/views/spree/users/_new_card_form.html.haml @@ -0,0 +1,35 @@ +%form{ novalidate: true, name: 'new_card_form', "ng-submit" => "storeCard()" } + .row + .small-6.columns + %label + = t(:first_name) + -# Changing name not permitted by default (in checkout) - can be enabled by setting an allow_name_change variable in $scope + %input#first_name{ type: :text, + name: 'first_name', + required: true, + ng: { model: "secrets.first_name", + disabled: "!allow_name_change", + value: "order.bill_address.firstname"} } + %small.error{ ng: { show: 'new_card_form.$submitted && new_card_form.first_name.$error.required' } }= t(:error_required) + + .small-6.columns + %label + = t(:last_name) + %input#last_name{type: :text, + name: "last_name", + required: true, + ng: { model: "secrets.last_name", + disabled: "!allow_name_change", + value: "order.bill_address.lastname" } } + %small.error{ ng: { show: 'new_card_form.$submitted && new_card_form.last_name.$error.required' } }= t(:error_required) + + .row + .small-12.columns + %label + = t(:card_details) + %stripe-elements + .row + .small-4.columns + %p + %button.button.primary{type: :submit} + = t(:add_card) diff --git a/app/views/spree/users/_open_orders.html.haml b/app/views/spree/users/_open_orders.html.haml index 5f78738e34..ed5e43b9b5 100644 --- a/app/views/spree/users/_open_orders.html.haml +++ b/app/views/spree/users/_open_orders.html.haml @@ -9,12 +9,12 @@ %th.order5.text-right= t('.total') %th.order6.text-right.show-for-large-up= t('.edit') %th.order7.text-right= t('.cancel') - %tbody.transaction-group{"ng-repeat" => "order in Orders.changeable_orders", "ng-class-odd"=>"'odd'", "ng-class-even"=>"'even'"} + %tbody.transaction-group{"ng-repeat" => "order in Orders.changeable", "ng-class-odd"=>"'odd'", "ng-class-even"=>"'even'"} %tr.order-row %td.order1 %a{"ng-href" => "{{::order.path}}", "ng-bind" => "::order.number"} - %td.order2{"ng-bind" => "::order.shop_name"} - %td.order3.show-for-large-up{"ng-bind" => "order.changes_allowed_until"} + %td.order2{"ng-bind" => "::Orders.shopsByID[order.shop_id].name"} + %td.order3.show-for-large-up{"ng-bind" => "::order.changes_allowed_until"} %td.order4.show-for-large-up{"ng-bind" => "::order.item_count"} %td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","ng-bind" => "::order.total | localizeCurrency"} %td.order6.text-right.show-for-large-up.brick diff --git a/app/views/spree/users/_orders.html.haml b/app/views/spree/users/_orders.html.haml new file mode 100644 index 0000000000..a8c34b22f1 --- /dev/null +++ b/app/views/spree/users/_orders.html.haml @@ -0,0 +1,10 @@ +%script{ type: "text/ng-template", id: "account/orders.html" } + .orders{"ng-controller" => "OrdersCtrl", "ng-cloak" => true} + .my-open-orders{ ng: { show: 'Orders.changeable.length > 0' } } + %h3= t('.open_orders') + = render 'open_orders' + + .past-orders{ ng: { show: 'pastOrders.length > 0' } } + %h3= t('.past_orders') + = render 'past_orders' + .message{"ng-if" => "Orders.all.length == 0", "ng-bind" => "::'you_have_no_orders_yet' | t"} diff --git a/app/views/spree/users/_past_orders.html.haml b/app/views/spree/users/_past_orders.html.haml new file mode 100644 index 0000000000..2abf5a273e --- /dev/null +++ b/app/views/spree/users/_past_orders.html.haml @@ -0,0 +1,22 @@ +.row + .small-12.columns + %table + %tr + %th.order1= t('.order') + %th.order2= t('.shop') + %th.order3.show-for-large-up= t('.completed_at') + %th.order4.show-for-large-up= t('.items') + %th.order5.text-right= t('.total') + %th.order6.text-right.show-for-large-up= t('.paid?') + %th.order7.text-right= t('.view') + %tbody.transaction-group{"ng-repeat" => "order in Orders.all | filter:{changes_allowed:false} as pastOrders", "ng-class-odd"=>"'odd'", "ng-class-even"=>"'even'"} + %tr.order-row + %td.order1 + %a{"ng-href" => "{{::order.path}}", "ng-bind" => "::order.number"} + %td.order2{"ng-bind" => "::Orders.shopsByID[order.shop_id].name"} + %td.order3.show-for-large-up{"ng-bind" => "::order.completed_at"} + %td.order4.show-for-large-up{"ng-bind" => "::order.item_count"} + %td.order5.text-right{"ng-class" => "{'debit': order.payment_state != 'paid', 'credit': order.payment_state == 'paid'}","ng-bind" => "::order.total | localizeCurrency"} + %td.order6.text-right.show-for-large-up{"ng-class" => "{'debit': order.payment_state != 'paid', 'credit': order.payment_state == 'paid'}", "ng-bind" => "::(order.payment_state == 'paid' ? 'say_yes' : 'say_no') | t"} + %td.order7.text-right + %a{"ng-href" => "{{::order.path}}" }= t('.view') diff --git a/app/views/spree/users/_saved_cards.html.haml b/app/views/spree/users/_saved_cards.html.haml new file mode 100644 index 0000000000..d8c710607b --- /dev/null +++ b/app/views/spree/users/_saved_cards.html.haml @@ -0,0 +1,13 @@ +%table + %tr + %th= t(:card_type) + %th= t(:card_number) + %th= t(:card_expiry_date) + %th= t(:delete?) + %tr.card{ id: "card{{ card.id }}", ng: { repeat: "card in savedCreditCards" } } + %td.brand{ ng: { bind: '::card.brand' } } + %td.number{ ng: { bind: '::card.number' } } + %td.expiry{ ng: { bind: '::card.expiry' } } + %td.actions + %a{"rel" => "nofollow", "data-method" => "delete", "ng-href" => "{{card.delete_link}}" } + = t(:delete) diff --git a/app/views/spree/users/_settings.html.haml b/app/views/spree/users/_settings.html.haml new file mode 100644 index 0000000000..6107172190 --- /dev/null +++ b/app/views/spree/users/_settings.html.haml @@ -0,0 +1,2 @@ +%script{ type: "text/ng-template", id: "account/settings.html" } + = render 'form' diff --git a/app/views/spree/users/_skinny.html.haml b/app/views/spree/users/_skinny.html.haml index 70ba39d335..43bb039c46 100644 --- a/app/views/spree/users/_skinny.html.haml +++ b/app/views/spree/users/_skinny.html.haml @@ -1,10 +1,10 @@ .row.active_table_row.skinny-head.margin-top{"ng-click" => "toggle($event)", "ng-class" => "{'closed' : !open()}"} .columns.small-2 - %img.margin-top.account-logo{"logo-fallback" => true, "ng-src" => "{{distributor.logo}}"} + %img.margin-top.account-logo{"logo-fallback" => true, "ng-src" => "{{shop.logo}}"} .columns.small-5 - %h3.margin-top{"ng-bind" => "::distributor.name"} + %h3.margin-top{"ng-bind" => "::shop.name"} .columns.small-4.text-right - %h3.margin-top.distributor-balance{"ng-bind" => "::distributor.balance | formatBalance", "ng-class" => "{'credit' : distributor.balance < 0, 'debit' : distributor.balance > 0, 'paid' : distributor.balance == 0}" } + %h3.margin-top.distributor-balance{"ng-bind" => "::shop.balance | formatBalance", "ng-class" => "{'credit' : shop.balance < 0, 'debit' : shop.balance > 0, 'paid' : shop.balance == 0}" } .columns.small-1.text-right %h3.margin-top %i{"ng-class" => "{'ofn-i_005-caret-down' : !open(), 'ofn-i_006-caret-up' : open()}"} diff --git a/app/views/spree/users/_transactions.html.haml b/app/views/spree/users/_transactions.html.haml new file mode 100644 index 0000000000..c46fbd38d7 --- /dev/null +++ b/app/views/spree/users/_transactions.html.haml @@ -0,0 +1,11 @@ +%script{ type: "text/ng-template", id: "account/transactions.html" } + .active_table.orders{"ng-controller" => "OrdersCtrl", "ng-cloak" => true} + %h3.my-orders= t(:transaction_history) + %distributor.active_table_node.row.animate-repeat{"ng-if" => "Orders.shops.length > 0", "ng-repeat" => "shop in Orders.shops", + "ng-controller" => "ShopNodeCtrl", + "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !shop.active}", + id: "{{shop.hash}}"} + .small-12.columns + = render partial: "spree/users/skinny" + = render partial: "spree/users/fat" + .message{"ng-if" => "Orders.shops.length == 0", "ng-bind" => "::'you_have_no_orders_yet' | t"} diff --git a/app/views/spree/users/edit.html.haml b/app/views/spree/users/edit.html.haml index 9815b5f34d..29e600582c 100644 --- a/app/views/spree/users/edit.html.haml +++ b/app/views/spree/users/edit.html.haml @@ -1,8 +1,3 @@ .darkswarm .row - = render :partial => 'spree/shared/error_messages', :locals => { :target => @user } - %h1= t(:editing_user) - = form_for Spree::User.new, :as => @user, :url => spree.user_path(@user), :method => :put do |f| - = render :partial => 'spree/shared/user_form', :locals => { :f => f } - %p - = f.submit t(:update), :class => 'button primary' + = render 'form' diff --git a/app/views/spree/users/show.html.haml b/app/views/spree/users/show.html.haml index 8272cc9eeb..23de4fd744 100644 --- a/app/views/spree/users/show.html.haml +++ b/app/views/spree/users/show.html.haml @@ -1,5 +1,11 @@ .darkswarm - = inject_orders_by_distributor + = inject_orders + = inject_shops + = inject_saved_credit_cards + + - if Stripe.publishable_key + :javascript + angular.module('Darkswarm').value("stripeObject", Stripe("#{Stripe.publishable_key}")) .row.pad-top .small-12.columns.pad-top @@ -7,22 +13,22 @@ = accurate_title %span.account-summary{"data-hook" => "account_summary"} = @user.email - (#{link_to t(:edit), spree.edit_account_path}) - .orders{"ng-controller" => "OrdersCtrl", "ng-cloak" => true} - .my-open-orders{ ng: { show: 'Orders.changeable_orders.length > 0' } } - %h3= t(:open_orders) - = render 'open_orders' + = render 'orders' + = render 'cards' + = render 'transactions' + = render 'settings' - .active_table - %h3.my-orders= t(:transaction_history) - %distributor.active_table_node.row.animate-repeat{"ng-if" => "Orders.orders_by_distributor.length > 0", "ng-repeat" => "(key, distributor) in Orders.orders_by_distributor", - "ng-controller" => "DistributorNodeCtrl", - "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !distributor.active}", - id: "{{distributor.hash}}"} - .small-12.columns - = render partial: "spree/users/skinny" - = render partial: "spree/users/fat" - .message{"ng-if" => "Orders.orders_by_distributor.length == 0", "ng-bind" => "::'you_have_no_orders_yet' | t"} + .row.tabset-ctrl#account-tabs{ style: 'margin-bottom: 100px', navigate: 'true', selected: 'orders', prefix: 'account' } + .small.12.medium-3.columns.tab{ name: "orders" } + %a{ href: 'javascript:void(0)' }=t('.tabs.orders') + - if Spree::Config.stripe_connect_enabled && Stripe.publishable_key + .small.12.medium-3.columns.tab{ name: "cards" } + %a{ href: 'javascript:void(0)' }=t('.tabs.cards') + .small.12.medium-3.columns.tab{ name: "transactions" } + %a{ href: 'javascript:void(0)' }=t('.tabs.transactions') + .small.12.medium-3.columns.tab{ name: "settings" } + %a{ href: 'javascript:void(0)' }=t('.tabs.settings') + .small-12.columns.tab-view = render partial: "shared/footer" diff --git a/config/application.rb b/config/application.rb index cc402859da..e58c7e8115 100644 --- a/config/application.rb +++ b/config/application.rb @@ -24,27 +24,54 @@ module Openfoodnetwork end end - # Register Spree calculators - initializer "spree.register.calculators" do |app| - app.config.spree.calculators.shipping_methods << OpenFoodNetwork::Calculator::Weight + # Settings dependent on locale + # + # We need to set this config before the promo environment gets loaded and + # after the spree environment gets loaded... + # This is because Spree uses `Spree::Config` while evaluating classes :scream: + # + # https://github.com/spree/spree/blob/2-0-stable/core/app/models/spree/calculator/per_item.rb#L6 + # + # TODO: move back to spree initializer once we upgrade to a more recent version + # of Spree + initializer 'ofn.spree_locale_settings', before: 'spree.promo.environment' do |app| + Spree::Config['checkout_zone'] = ENV['CHECKOUT_ZONE'] + Spree::Config['currency'] = ENV['CURRENCY'] + if Spree::Country.table_exists? + country = Spree::Country.find_by_iso(ENV['DEFAULT_COUNTRY_CODE']) + Spree::Config['default_country_id'] = country.id if country.present? + else + Spree::Config['default_country_id'] = 12 # Australia + end + end - app.config.spree.calculators.enterprise_fees = [Calculator::FlatPercentPerItem, - Spree::Calculator::FlatRate, - Spree::Calculator::FlexiRate, - Spree::Calculator::PerItem, - Spree::Calculator::PriceSack, - OpenFoodNetwork::Calculator::Weight] - app.config.spree.calculators.payment_methods = [Spree::Calculator::FlatPercentItemTotal, - Spree::Calculator::FlatRate, - Spree::Calculator::FlexiRate, - Spree::Calculator::PerItem, - Spree::Calculator::PriceSack] + # Register Spree calculators + initializer 'spree.register.calculators' do |app| + app.config.spree.calculators.shipping_methods << OpenFoodNetwork::Calculator::Weight + app.config.spree.calculators.add_class('enterprise_fees') + config.spree.calculators.enterprise_fees = [ + Calculator::FlatPercentPerItem, + Spree::Calculator::FlatRate, + Spree::Calculator::FlexiRate, + Spree::Calculator::PerItem, + Spree::Calculator::PriceSack, + OpenFoodNetwork::Calculator::Weight + ] + app.config.spree.calculators.add_class('payment_methods') + config.spree.calculators.payment_methods = [ + Spree::Calculator::FlatPercentItemTotal, + Spree::Calculator::FlatRate, + Spree::Calculator::FlexiRate, + Spree::Calculator::PerItem, + Spree::Calculator::PriceSack + ] end # Register Spree payment methods initializer "spree.gateway.payment_methods", :after => "spree.register.payment_methods" do |app| app.config.spree.payment_methods << Spree::Gateway::Migs app.config.spree.payment_methods << Spree::Gateway::Pin + app.config.spree.payment_methods << Spree::Gateway::StripeConnect end # Settings in config/environments/* take precedence over those specified here. diff --git a/config/application.yml.example b/config/application.yml.example index e39f23fb59..310e248726 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -26,3 +26,12 @@ CURRENCY: AUD # see: https://developers.google.com/maps/documentation/javascript/get-api-key # GOOGLE_MAPS_API_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Stripe Connect details for instance account +# Find these under 'API keys' and 'Connect' in your Stripe account dashboard -> Account Settings +# Under 'Connect', the Redirect URI should be set to https://YOUR_SERVER_URL/stripe/callbacks (e.g. https://openfoodnetwork.org.uk/stripe/callbacks) +# Under 'Webhooks', you should set up a Connect endpoint pointing to https://YOUR_SERVER_URL/stripe/webhooks e.g. (https://openfoodnetwork.org.uk/stripe/webhooks) +# STRIPE_INSTANCE_SECRET_KEY: "sk_test_xxxxxx" # This can be a test key or a live key +# STRIPE_INSTANCE_PUBLISHABLE_KEY: "pk_test_xxxx" # This can be a test key or a live key +# STRIPE_CLIENT_ID: "ca_xxxx" # This can be a development ID or a production ID +# STRIPE_ENDPOINT_SECRET: "whsec_xxxx" diff --git a/config/environments/test.rb b/config/environments/test.rb index f0ef05acaa..3a33fd53d7 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -45,7 +45,6 @@ Openfoodnetwork::Application.configure do # Print deprecation notices to the stderr config.active_support.deprecation = :stderr - config.action_mailer.default_url_options = { :host => "test.host" } # To block requests before running the database cleaner require 'open_food_network/rack_request_blocker' @@ -55,3 +54,4 @@ end # Allows us to use _url helpers in Rspec Rails.application.routes.default_url_options[:host] = 'test.host' +Spree::Core::Engine.routes.default_url_options[:host] = 'test.host' diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index f439e779cf..dc41aa9258 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -6,27 +6,18 @@ # In order to initialize a setting do: # config.setting_name = 'new value' - require 'spree/product_filters' require 'spree/core/calculated_adjustments_decorator' require "#{Rails.root}/app/models/spree/payment_method_decorator" require "#{Rails.root}/app/models/spree/gateway_decorator" +Spree::Api::Config[:requires_authentication] = true + Spree.config do |config| config.shipping_instructions = true config.address_requires_state = true - # Settings dependent on locale - config.checkout_zone = ENV["CHECKOUT_ZONE"] - config.currency = ENV['CURRENCY'] - if Spree::Country.table_exists? - country = Spree::Country.find_by_iso(ENV['DEFAULT_COUNTRY_CODE']) - config.default_country_id = country.id if country.present? - else - config.default_country_id = 12 # Australia - end - # -- spree_paypal_express # Auto-capture payments. Without this option, payments must be manually captured in the paypal interface. config.auto_capture = true @@ -39,17 +30,6 @@ end module OpenFoodNetwork end -# Add calculators category for enterprise fees -module Spree - module Core - class Environment - class Calculators - attr_accessor :enterprise_fees, :payment_methods - end - end - end -end - # Forcing spree to always allow SSL connections # Since we are using config.force_ssl = true # Without this we get a redirect loop: see https://groups.google.com/forum/#!topic/spree-user/NwpqGxJ4klk diff --git a/config/initializers/spree_auth_devise.rb b/config/initializers/spree_auth_devise.rb new file mode 100644 index 0000000000..a59badf636 --- /dev/null +++ b/config/initializers/spree_auth_devise.rb @@ -0,0 +1,16 @@ +# `spree_auth_devise` gem decorators get loaded in a `to_prepare` callback +# referring to Spree classes that have not been loaded yet +# +# When this initializer is loaded we're sure that those Spree classes have been +# loaded and we load again the `spree_auth_devise` decorators to effectively +# apply them. +# +# Give a look at `if defined?(Spree::Admin::BaseController)` in the following file +# to get an example: +# https://github.com/openfoodfoundation/spree_auth_devise/blob/spree-upgrade-intermediate/app/controllers/spree/admin/admin_controller_decorator.rb#L1 +# +# TODO: remove this hack once we get to Spree 3.0 +gem_dir = Gem::Specification.find_by_name("spree_auth_devise").gem_dir +Dir.glob(File.join(gem_dir, 'app/**/*_decorator*.rb')) do |c| + load c +end diff --git a/config/initializers/stripe.rb b/config/initializers/stripe.rb new file mode 100644 index 0000000000..8237b0dcab --- /dev/null +++ b/config/initializers/stripe.rb @@ -0,0 +1,14 @@ +# Add some additional properties, to allow us to access these +# properties from the object, rather than calling from ENV directly. +# This is mostly useful for stubbing when testing, but also feels +# a bit cleaner than accessing keys in different ways. +module Stripe + class << self + attr_accessor :publishable_key, :endpoint_secret + end +end + +Stripe.api_key = ENV['STRIPE_INSTANCE_SECRET_KEY'] +Stripe.publishable_key = ENV['STRIPE_INSTANCE_PUBLISHABLE_KEY'] +Stripe.client_id = ENV['STRIPE_CLIENT_ID'] +Stripe.endpoint_secret = ENV['STRIPE_ENDPOINT_SECRET'] diff --git a/config/locales/en.yml b/config/locales/en.yml index 8ce260fb77..b759c83bdc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -161,7 +161,14 @@ en: powered_by: Powered by blocked_cookies_alert: "Your browser may be blocking cookies needed to use this shopfront. Click below to allow cookies and reload the page." allow_cookies: "Allow Cookies" + none: None + notes: Notes + error: Error + processing_payment: Processing payment... + + actions: + create_and_add_another: "Create and Add Another" admin: # Common properties / models date: Date @@ -375,6 +382,8 @@ en: index: capture: "Capture" ship: "Ship" + invoice_email_sent: 'Invoice email has been sent' + order_email_resent: 'Order email has been resent' bulk_management: tip: "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." shared: "Shared Resource?" @@ -514,6 +523,18 @@ en: close_date: Close Date social: twitter_placeholder: eg. @the_prof + stripe_connect: + connect_with_stripe: "Connect with Stripe" + stripe_connect_intro: "To accept payments using credit card, you will need to connect your stripe account to the Open Food Network. Use the button to the right to get started." + stripe_account_connected: "Stripe account connected." + disconnect: "Disconnect account" + confirm_modal: + title: Connect with Stripe + part1: Stripe is a payment processing service that allows shops on the OFN to accept credit card payments from customers. + part2: To use this feature, you must connect your Stripe account to the OFN. Clicking 'I Agree' below will redirect to you the Stripe website where you can connect an existing Stripe account, or create a new one if you don't already have one. + part3: This will allow the Open Food Network to accept credit card payments from customers on your behalf. Please note that you will need to maintain your own Stripe account, pay the fees Stripe charges and handle any chargebacks and customer service yourself. + i_agree: I Agree + cancel: Cancel tag_rules: default_rules: by_default: By Default @@ -597,12 +618,17 @@ en: advanced_settings: Advanced Settings update_and_close: Update and Close choose_products_from: 'Choose Products From:' - pickup_time_tip: When orders from this OC will be ready for the customer - pickup_instructions_tip: These instructions are shown to customers after they complete an order exchange_form: + pickup_time_tip: When orders from this OC will be ready for the customer pickup_instructions_placeholder: "Pick-up instructions" + pickup_instructions_tip: These instructions are shown to customers after they complete an order pickup_time_placeholder: "Ready for (ie. Date / Time)" receival_instructions_placeholder: "Receival instructions" + add_fee: 'Add fee' + selected: 'selected' + add_exchange_form: + add_supplier: 'Add supplier' + add_distributor: 'Add distributor' advanced_settings: title: Advanced Settings choose_product_tip: You can opt to restrict all available products (both incoming and outgoing), to only those in %{inventory}'s inventory. @@ -642,9 +668,11 @@ en: producer_properties: index: title: Producer Properties + shared: user_guide_link: user_guide: User Guide + invoice_settings: edit: title: Invoice Settings @@ -715,6 +743,34 @@ en: description: Invoices for import into Xero packing: name: Packing Reports + + stripe_connect_settings: + edit: + title: "Stripe Connect" + settings: "Settings" + stripe_beta_warning: "Warning: The Stripe Connect integration is currently in beta and should not be enabled in production yet" + stripe_connect_enabled: Enable shops to accept payments using Stripe Connect? + no_api_key_msg: No Stripe account exists for this enterprise. + configuration_explanation_html: For detailed instructions on configuring the Stripe Connect integration, please consult this guide. + status: Status + ok: Ok + instance_secret_key: Instance Secret Key + account_id: Account ID + business_name: Business Name + charges_enabled: Charges Enabled + charges_enabled_warning: "Warning: Charges are not enabled for your account" + auth_fail_error: The API key you provided is invalid + empty_api_key_error: No Stripe API key has been provided. To set your API key, please follow the instructions at + + # Admin controllers + controllers: + enterprises: + stripe_connect_cancelled: "Connection to Stripe has been cancelled" + stripe_connect_success: "Stripe account connected successfully" + stripe_connect_fail: Sorry, the connection of your Stripe account failed + stripe_connect_settings: + resource: Stripe Connect configuration + # Frontend views # # These keys are referenced relatively like `t('.message')` in @@ -746,6 +802,11 @@ en: require_login_html: "Please %{login} if you have an account already. Otherwise, %{register} to become a customer." require_customer_html: "Please %{contact} %{enterprise} to become a customer." + + # Front-end controller translations + card_could_not_be_saved: card could not be saved + spree_gateway_error_flash_for_checkout: "There was a problem with your payment information: %{error}" + # Printable Invoice Columns invoice_billing_address: "Billing address:" invoice_column_tax: "GST" @@ -844,6 +905,18 @@ en: card_number: Card Number card_securitycode: "Security Code" card_expiry_date: Expiry Date + card_masked_digit: "X" + card_expiry_abbreviation: "Exp" + new_credit_card: "New credit card" + my_credit_cards: My credit cards + add_new_credit_card: Add new credit card + saved_cards: Saved cards + add_a_card: Add a Card + add_card: Add Card + you_have_no_saved_cards: You haven't saved any cards yet + saving_credit_card: Saving credit card... + card_has_been_removed: "Your card has been removed (number: %{number})" + card_could_not_be_removed: Sorry, the card could not be removed ie_warning_headline: "Your browser is out of date :-(" ie_warning_text: "For the best Open Food Network experience, we strongly recommend upgrading your browser:" @@ -936,6 +1009,7 @@ en: checkout_shipping_price: Shipping checkout_total_price: Total checkout_back_to_cart: "Back to Cart" + cost_currency: "Cost Currency" order_paid: PAID order_not_paid: NOT PAID @@ -1311,10 +1385,67 @@ See the %{link} to find out more about %{sitename}'s features and to start using reset_password: "Reset password" registration_greeting: "Greetings!" who_is_managing_enterprise: "Who is responsible for managing %{enterprise}?" + enterprise: + registration: + modal: + steps: + details: + title: 'Details' + headline: "Let's Get Started" + enterprise: "Woot! First we 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:" + 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_placeholder: "e.g. 123 Cranberry Drive" + address1_field_error: "Please enter an address" + 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_placeholder: "e.g. 3070" + postcode_field_error: "Postcode required" + state_field: "State:" + state_field_error: "State required" + country_field: "Country:" + country_field_error: "Please select a country" + contact: + title: 'Contact' + contact_field: 'Primary Contact' + contact_field_placeholder: 'Contact Name' + contact_field_required: "You need to enter a primary contact." + email_field: 'Email address' + email_field_placeholder: 'eg. charlie@thefarm.com' + phone_field: 'Phone number' + 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" + 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." + + about: + title: 'About' + images: + title: 'Images' + social: + title: 'Social' + # TODO: Remove these once the enterprise.registration.modal keys are translated enterprise_contact: "Primary Contact" + enterprise_contact_placeholder: "Contact Name" enterprise_contact_required: "You need to enter a primary contact." enterprise_email_address: "Email address" + enterprise_email_placeholder: "eg. charlie@thefarm.com" enterprise_phone: "Phone number" + enterprise_phone_placeholder: "eg. (03) 1234 5678" + # END back: "Back" continue: "Continue" limit_reached_headline: "Oh no!" @@ -1385,6 +1516,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using registration_finished_activate_instruction_html: "We've sent a confirmation email to %{email} if it hasn't been activated before.
Please follow the instructions there to make your enterprise visible on the Open Food Network." registration_finished_action: "Open Food Network home" + registration_contact_name: 'Contact Name' + + # TODO: Remove these once the enterprise.registration.modal keys are translated registration_type_headline: "Last step to add %{enterprise}!" registration_type_question: "Are you a producer?" registration_type_producer: "Yes, I'm a producer" @@ -1392,9 +1526,13 @@ Please follow the instructions there to make your enterprise visible on the Open registration_type_error: "Please choose one. Are you are producer?" registration_type_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." registration_type_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." + # END create_profile: "Create Profile" registration_images_headline: "Thanks!" registration_images_description: "Let's upload some pretty pictures so your profile looks great! :)" + + + # TODO: Remove these once the enterprise.registration.modal keys are translated registration_detail_headline: "Let's Get Started" registration_detail_enterprise: "Woot! First we need to know a little bit about your enterprise:" registration_detail_producer: "Woot! First we need to know a little bit about your farm:" @@ -1416,6 +1554,7 @@ Please follow the instructions there to make your enterprise visible on the Open registration_detail_state_error: "State required" registration_detail_country: "Country:" registration_detail_country_error: "Please select a country" + # END shipping_method_destroy_error: "That shipping method cannot be deleted as it is referenced by an order: %{number}." accounts_and_billing_task_already_running_error: "A task is already running, please wait until it has finished" accounts_and_billing_start_task_notice: "Task Queued" @@ -1484,6 +1623,9 @@ Please follow the instructions there to make your enterprise visible on the Open create: "Create" search: "Search" supplier: "Supplier" + product_name: "Product Name" + product_description: "Product Description" + units: "Unit Size" coordinator: "Coordinator" distributor: "Distributor" enterprise_fees: "Enterprise Fees" @@ -1498,9 +1640,11 @@ Please follow the instructions there to make your enterprise visible on the Open new_order_cycles: "New Order Cycles" new_order_cycle: "New Order Cycle" select_a_coordinator_for_your_order_cycle: "Select a coordinator for your order cycle" + notify_producers: 'Notify producers' edit_order_cycle: "Edit Order Cycle" roles: "Roles" update: "Update" + delete: Delete add_producer_property: "Add producer property" in_progress: "In Progress" started_at: "Started at" @@ -1545,6 +1689,8 @@ Please follow the instructions there to make your enterprise visible on the Open spree_admin_unit_description: Unit Description spree_admin_variant_unit: Variant unit spree_admin_variant_unit_scale: Variant unit scale + spree_admin_supplier: Supplier + spree_admin_product_category: Product Category spree_admin_variant_unit_name: Variant unit name change_package: "Change Package" spree_admin_single_enterprise_hint: "Hint: To allow people to find you, turn on your visibility under" @@ -2014,6 +2160,10 @@ Please follow the instructions there to make your enterprise visible on the Open start_free_profile: "Start with a free profile, and expand when you're ready!" spree: + # TODO: remove `email` key once we get to Spree 2.0 + email: Email + # TODO: remove 'account_updated' key once we get to Spree 2.0 + account_updated: "Account updated!" admin: orders: invoice: @@ -2036,7 +2186,28 @@ Please follow the instructions there to make your enterprise visible on the Open one: "You have one active order cycle." other: "You have %{count} active order cycles." manage_order_cycles: "MANAGE ORDER CYCLES" + payment_methods: + stripe_connect: + enterprise_select_placeholder: Choose... + loading_account_information_msg: Loading account information from stripe, please wait... + stripe_disabled_msg: Stripe payments have been disabled by the system administrator. + request_failed_msg: Sorry. Something went wrong when trying to verify account details with Stripe... + account_missing_msg: No Stripe account exists for this enterprise. + connect_one: Connect One + access_revoked_msg: Access to this Stripe account has been revoked, please reconnect your account. + status: Status + connected: Connected + account_id: Account ID + business_name: Business Name + charges_enabled: Charges Enabled + payments: + source_forms: + stripe: + no_payment_via_admin_backend: Creating Stripe-based payments from the admin backend is not possible at this time products: + new: + title: 'New Product' + unit_name_placeholder: 'eg. bunches' bulk_edit: header: title: Bulk Edit Products @@ -2044,15 +2215,34 @@ Please follow the instructions there to make your enterprise visible on the Open title: LOADING PRODUCTS no_products: "No products yet. Why don't you add some?" no_results: "Sorry, no results match" + product_name: Product Name + primary_taxon_form: + product_category: Product Category + group_buy_form: + group_buy: "Group Buy?" + bulk_unit_size: Bulk unit size + display_as: + display_as: Display As reports: bulk_coop: bulk_coop_supplier_report: 'Bulk Co-op - Totals by Supplier' bulk_coop_allocation: 'Bulk Co-op - Allocation' bulk_coop_packing_sheets: 'Bulk Co-op - Packing Sheets' bulk_coop_customer_payments: 'Bulk Co-op - Customer Payments' + shared: + configuration_menu: + stripe_connect: Stripe Connect variants: autocomplete: producer_name: Producer + checkout: + payment: + stripe: + 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' @@ -2125,8 +2315,18 @@ Please follow the instructions there to make your enterprise visible on the Open weight: Weight (per kg) zipcode: Postcode users: + form: + account_settings: Account Settings show: + tabs: + orders: Orders + cards: Credit Cards + transactions: Transactions + settings: Account Settings + orders: open_orders: Open Orders + past_orders: Past Orders + transactions: transaction_history: Transaction History open_orders: order: Order @@ -2138,3 +2338,11 @@ Please follow the instructions there to make your enterprise visible on the Open cancel: Cancel closed: Closed until: Until + past_orders: + order: Order + shop: Shop + completed_at: Completed At + items: Items + total: Total + paid?: Paid? + view: View diff --git a/config/routes.rb b/config/routes.rb index 52b23758ab..ca8114ddb8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,6 +53,15 @@ Openfoodnetwork::Application.routes.draw do end end + namespace :stripe do + resources :callbacks, only: [:index] + resources :webhooks, only: [:create] + end + + namespace :admin do + resources :bulk_line_items + end + get '/checkout', :to => 'checkout#edit' , :as => :checkout put '/checkout', :to => 'checkout#update' , :as => :update_checkout get '/checkout/paypal_payment/:order_id', to: 'checkout#paypal_payment', as: :paypal_payment @@ -160,6 +169,13 @@ Openfoodnetwork::Application.routes.draw do end resource :invoice_settings, only: [:edit, :update] + + resource :stripe_connect_settings, only: [:edit, :update] + + resources :stripe_accounts, only: [:destroy] do + get :connect, on: :collection + get :status, on: :collection + end end namespace :api do @@ -223,6 +239,9 @@ Spree::Core::Engine.routes.prepend do match '/admin/reports/xero_invoices' => 'admin/reports#xero_invoices', :as => "xero_invoices_admin_reports", :via => [:get, :post] match '/admin', :to => 'admin/overview#index', :as => :admin match '/admin/payment_methods/show_provider_preferences' => 'admin/payment_methods#show_provider_preferences', :via => :get + put 'credit_cards/new_from_token', to: 'credit_cards#new_from_token' + + resources :credit_cards namespace :api, :defaults => { :format => 'json' } do @@ -237,6 +256,7 @@ Spree::Core::Engine.routes.prepend do get :overridable end delete :soft_delete + post :clone resources :variants do delete :soft_delete @@ -246,6 +266,7 @@ Spree::Core::Engine.routes.prepend do resources :orders do get :managed, on: :collection end + end namespace :admin do @@ -254,6 +275,8 @@ Spree::Core::Engine.routes.prepend do resources :products do get :product_distributions, on: :member + get :group_buy_options, on: :member + get :seo, on: :member post :bulk_update, :on => :collection, :as => :bulk_update end @@ -264,8 +287,6 @@ Spree::Core::Engine.routes.prepend do get :print_ticket, on: :member get :managed, on: :collection end - - resources :line_items, only: [:index], format: :json end resources :orders do diff --git a/db/migrate/20160828115018_create_stripe_accounts.rb b/db/migrate/20160828115018_create_stripe_accounts.rb new file mode 100644 index 0000000000..56ab206540 --- /dev/null +++ b/db/migrate/20160828115018_create_stripe_accounts.rb @@ -0,0 +1,12 @@ +class CreateStripeAccounts < ActiveRecord::Migration + def change + create_table :stripe_accounts do |t| + t.string :stripe_user_id + t.string :stripe_publishable_key + t.timestamps + t.belongs_to :enterprise + end + + add_index :stripe_accounts, :enterprise_id, unique: true + end +end diff --git a/db/migrate/20160916024535_add_state_to_spree_adjustments.spree.rb b/db/migrate/20160916024535_add_state_to_spree_adjustments.spree.rb new file mode 100644 index 0000000000..a4224678df --- /dev/null +++ b/db/migrate/20160916024535_add_state_to_spree_adjustments.spree.rb @@ -0,0 +1,7 @@ +# This migration comes from spree (originally 20121213162028) +class AddStateToSpreeAdjustments < ActiveRecord::Migration + def change + add_column :spree_adjustments, :state, :string + remove_column :spree_adjustments, :locked + end +end diff --git a/db/migrate/20170225203658_add_user_id_to_spree_credit_cards.rb b/db/migrate/20170225203658_add_user_id_to_spree_credit_cards.rb new file mode 100644 index 0000000000..29e50e0c1a --- /dev/null +++ b/db/migrate/20170225203658_add_user_id_to_spree_credit_cards.rb @@ -0,0 +1,8 @@ +class AddUserIdToSpreeCreditCards < ActiveRecord::Migration + def change + unless Spree::CreditCard.column_names.include? "user_id" + add_column :spree_credit_cards, :user_id, :integer + add_index :spree_credit_cards, :user_id + end + end +end diff --git a/db/migrate/20170304151129_add_payment_method_to_spree_credit_cards.rb b/db/migrate/20170304151129_add_payment_method_to_spree_credit_cards.rb new file mode 100644 index 0000000000..76c1cab805 --- /dev/null +++ b/db/migrate/20170304151129_add_payment_method_to_spree_credit_cards.rb @@ -0,0 +1,8 @@ +class AddPaymentMethodToSpreeCreditCards < ActiveRecord::Migration + def change + unless Spree::CreditCard.column_names.include? "payment_method_id" + add_column :spree_credit_cards, :payment_method_id, :integer + add_index :spree_credit_cards, :payment_method_id + end + end +end diff --git a/db/migrate/20170413074528_rename_payment_methods.spree_paypal_express.rb b/db/migrate/20170413074528_rename_payment_methods.spree_paypal_express.rb new file mode 100644 index 0000000000..2c426824d8 --- /dev/null +++ b/db/migrate/20170413074528_rename_payment_methods.spree_paypal_express.rb @@ -0,0 +1,14 @@ +# This migration comes from spree_paypal_express (originally 20140117051315) +class RenamePaymentMethods < ActiveRecord::Migration + def up + execute <<-SQL + update spree_payment_methods set type = 'Spree::Gateway::PayPalExpress' WHERE type = 'Spree::BillingIntegration::PaypalExpress' + SQL + end + + def down + execute <<-SQL + update spree_payment_methods set type = 'Spree::BillingIntegration::PaypalExpress' WHERE type = 'Spree::Gateway::PayPalExpress' + SQL + end +end diff --git a/db/migrate/20170413083148_add_tracking_url_to_spree_shipping_methods.spree.rb b/db/migrate/20170413083148_add_tracking_url_to_spree_shipping_methods.spree.rb new file mode 100644 index 0000000000..28a9807184 --- /dev/null +++ b/db/migrate/20170413083148_add_tracking_url_to_spree_shipping_methods.spree.rb @@ -0,0 +1,6 @@ +# This migration comes from spree (originally 20130301205200) +class AddTrackingUrlToSpreeShippingMethods < ActiveRecord::Migration + def change + add_column :spree_shipping_methods, :tracking_url, :string + end +end diff --git a/db/migrate/20170512115519_add_locale_to_spree_users.rb b/db/migrate/20170512115519_add_locale_to_spree_users.rb new file mode 100644 index 0000000000..f694bff1f7 --- /dev/null +++ b/db/migrate/20170512115519_add_locale_to_spree_users.rb @@ -0,0 +1,5 @@ +class AddLocaleToSpreeUsers < ActiveRecord::Migration + def change + add_column :spree_users, :locale, :string, limit: 5 + end +end diff --git a/db/migrate/20170921065259_update_adjustment_states.spree.rb b/db/migrate/20170921065259_update_adjustment_states.spree.rb new file mode 100644 index 0000000000..9040709751 --- /dev/null +++ b/db/migrate/20170921065259_update_adjustment_states.spree.rb @@ -0,0 +1,17 @@ +# This migration comes from spree (originally 20130417120035) +class UpdateAdjustmentStates < ActiveRecord::Migration + def up + Spree::Order.complete.find_each do |order| + order.adjustments.update_all(:state => 'closed') + end + + Spree::Shipment.shipped.includes(:adjustment).find_each do |shipment| + shipment.adjustment.update_column(:state, 'finalized') if shipment.adjustment + end + + Spree::Adjustment.where(:state => nil).update_all(:state => 'open') + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 75c43b1d72..061c97e920 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20161215230219) do +ActiveRecord::Schema.define(:version => 20170921065259) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -401,12 +401,12 @@ ActiveRecord::Schema.define(:version => 20161215230219) do t.datetime "created_at", :null => false t.datetime "updated_at", :null => false t.boolean "mandatory" - t.boolean "locked" t.integer "originator_id" t.string "originator_type" t.boolean "eligible", :default => true t.string "adjustable_type" t.decimal "included_tax", :precision => 10, :scale => 2, :default => 0.0, :null => false + t.string "state" end add_index "spree_adjustments", ["adjustable_id"], :name => "index_adjustments_on_order_id" @@ -469,8 +469,13 @@ ActiveRecord::Schema.define(:version => 20161215230219) do t.datetime "updated_at", :null => false t.string "gateway_customer_profile_id" t.string "gateway_payment_profile_id" + t.integer "user_id" + t.integer "payment_method_id" end + add_index "spree_credit_cards", ["payment_method_id"], :name => "index_spree_credit_cards_on_payment_method_id" + add_index "spree_credit_cards", ["user_id"], :name => "index_spree_credit_cards_on_user_id" + create_table "spree_gateways", :force => true do |t| t.string "type" t.string "name" @@ -870,6 +875,7 @@ ActiveRecord::Schema.define(:version => 20161215230219) do t.datetime "deleted_at" t.boolean "require_ship_address", :default => true t.text "description" + t.string "tracking_url" end create_table "spree_skrill_transactions", :force => true do |t| @@ -997,6 +1003,7 @@ ActiveRecord::Schema.define(:version => 20161215230219) do t.datetime "reset_password_sent_at" t.string "api_key", :limit => 40 t.integer "enterprise_limit", :default => 1, :null => false + t.string "locale", :limit => 5 end add_index "spree_users", ["email"], :name => "email_idx_unique", :unique => true @@ -1042,6 +1049,16 @@ ActiveRecord::Schema.define(:version => 20161215230219) do t.integer "zone_members_count", :default => 0 end + create_table "stripe_accounts", :force => true do |t| + t.string "stripe_user_id" + t.string "stripe_publishable_key" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.integer "enterprise_id" + end + + add_index "stripe_accounts", ["enterprise_id"], :name => "index_stripe_accounts_on_enterprise_id", :unique => true + create_table "suburbs", :force => true do |t| t.string "name" t.string "postcode" diff --git a/knapsack_rspec_report.json b/knapsack_rspec_report.json index 5d71fa12e2..8b7e7a0084 100644 --- a/knapsack_rspec_report.json +++ b/knapsack_rspec_report.json @@ -1,213 +1,221 @@ { - "spec/controllers/admin/accounts_and_billing_settings_controller_spec.rb": 4.729289770126343, - "spec/controllers/admin/business_model_configuration_controller_spec.rb": 0.3204472064971924, - "spec/controllers/admin/column_preferences_controller_spec.rb": 0.21457862854003906, - "spec/controllers/admin/customers_controller_spec.rb": 1.2570579051971436, - "spec/controllers/admin/enterprises_controller_spec.rb": 6.453823804855347, - "spec/controllers/admin/inventory_items_controller_spec.rb": 3.3494999408721924, - "spec/controllers/admin/order_cycles_controller_spec.rb": 3.2418012619018555, - "spec/controllers/admin/tag_rules_controller_spec.rb": 0.278639554977417, - "spec/controllers/admin/variant_overrides_controller_spec.rb": 4.334253549575806, - "spec/controllers/api/enterprises_controller_spec.rb": 0.4233386516571045, - "spec/controllers/api/order_cycles_controller_spec.rb": 1.9799659252166748, - "spec/controllers/api/statuses_controller_spec.rb": 0.0580599308013916, - "spec/controllers/base_controller_spec.rb": 0.025392770767211914, - "spec/controllers/cart_controller_spec.rb": 1.188997507095337, - "spec/controllers/checkout_controller_spec.rb": 1.7111992835998535, - "spec/controllers/enterprise_confirmations_controller_spec.rb": 1.057147741317749, - "spec/controllers/enterprises_controller_spec.rb": 3.9115149974823, - "spec/controllers/groups_controller_spec.rb": 0.4256296157836914, - "spec/controllers/registration_controller_spec.rb": 0.1444110870361328, - "spec/controllers/shop_controller_spec.rb": 2.8928422927856445, - "spec/controllers/shops_controller_spec.rb": 0.23369908332824707, - "spec/controllers/spree/admin/adjustments_controller_spec.rb": 1.0890753269195557, - "spec/controllers/spree/admin/base_controller_spec.rb": 0.1624138355255127, - "spec/controllers/spree/admin/line_items_controller_spec.rb": 16.208045721054077, - "spec/controllers/spree/admin/orders_controller_spec.rb": 15.907819509506226, - "spec/controllers/spree/admin/overview_controller_spec.rb": 0.8662476539611816, - "spec/controllers/spree/admin/payment_methods_controller_spec.rb": 0.4896993637084961, - "spec/controllers/spree/admin/products_controller_spec.rb": 2.074070930480957, - "spec/controllers/spree/admin/reports_controller_spec.rb": 55.07741975784302, - "spec/controllers/spree/admin/search_controller_spec.rb": 0.5158224105834961, - "spec/controllers/spree/admin/variants_controller_spec.rb": 1.990790605545044, - "spec/controllers/spree/api/line_items_controller_spec.rb": 0.3319206237792969, - "spec/controllers/spree/api/products_controller_spec.rb": 6.679011106491089, - "spec/controllers/spree/api/variants_controller_spec.rb": 3.7782905101776123, - "spec/controllers/spree/checkout_controller_spec.rb": 0.9790353775024414, - "spec/controllers/spree/orders_controller_spec.rb": 4.158925294876099, - "spec/controllers/spree/paypal_controller_spec.rb": 0.020407676696777344, - "spec/controllers/spree/store_controller_spec.rb": 0.03319215774536133, - "spec/controllers/spree/user_sessions_controller_spec.rb": 0.07898402214050293, - "spec/controllers/user_passwords_controller_spec.rb": 0.717381477355957, - "spec/controllers/user_registrations_controller_spec.rb": 0.2160170078277588, - "spec/features/admin/account_spec.rb": 0.2934560775756836, - "spec/features/admin/accounts_and_billing_settings_spec.rb": 15.889720678329468, - "spec/features/admin/adjustments_spec.rb": 5.175323009490967, - "spec/features/admin/authentication_spec.rb": 18.345781087875366, - "spec/features/admin/bulk_order_management_spec.rb": 111.50722217559814, - "spec/features/admin/bulk_product_update_spec.rb": 63.564337730407715, - "spec/features/admin/business_model_configuration_spec.rb": 2.1073272228240967, - "spec/features/admin/caching_spec.rb": 0.8505651950836182, - "spec/features/admin/content_spec.rb": 1.1712932586669922, - "spec/features/admin/customers_spec.rb": 71.91736245155334, - "spec/features/admin/enterprise_fees_spec.rb": 15.482876300811768, - "spec/features/admin/enterprise_groups_spec.rb": 8.615704774856567, - "spec/features/admin/enterprise_relationships_spec.rb": 11.908889293670654, - "spec/features/admin/enterprise_roles_spec.rb": 5.027954578399658, - "spec/features/admin/enterprise_user_spec.rb": 2.158304214477539, - "spec/features/admin/enterprises/index_spec.rb": 5.7792346477508545, - "spec/features/admin/enterprises_spec.rb": 39.01360893249512, - "spec/features/admin/external_services_spec.rb": 0.41648149490356445, - "spec/features/admin/image_settings_spec.rb": 0.4291551113128662, - "spec/features/admin/order_cycles_spec.rb": 66.84531092643738, - "spec/features/admin/orders_spec.rb": 50.86089587211609, - "spec/features/admin/overview_spec.rb": 4.884965896606445, - "spec/features/admin/payment_method_spec.rb": 14.099174499511719, - "spec/features/admin/products_spec.rb": 17.05465793609619, - "spec/features/admin/reports_spec.rb": 142.85665917396545, - "spec/features/admin/shipping_methods_spec.rb": 6.785600900650024, - "spec/features/admin/tag_rules_spec.rb": 21.80374526977539, - "spec/features/admin/tax_settings_spec.rb": 0.5856199264526367, - "spec/features/admin/variant_overrides_spec.rb": 54.87969517707825, - "spec/features/admin/variants_spec.rb": 4.425906658172607, - "spec/features/consumer/account_spec.rb": 14.138294458389282, - "spec/features/consumer/authentication_spec.rb": 15.800535440444946, - "spec/features/consumer/external_services_spec.rb": 0.47263646125793457, - "spec/features/consumer/groups_spec.rb": 2.167065143585205, - "spec/features/consumer/producers_spec.rb": 8.219613790512085, - "spec/features/consumer/registration_spec.rb": 3.2899246215820312, - "spec/features/consumer/shopping/cart_spec.rb": 7.931907653808594, - "spec/features/consumer/shopping/checkout_auth_spec.rb": 10.027384042739868, - "spec/features/consumer/shopping/checkout_spec.rb": 60.93560552597046, - "spec/features/consumer/shopping/shopping_spec.rb": 64.26991128921509, - "spec/features/consumer/shopping/variant_overrides_spec.rb": 71.18549585342407, - "spec/features/consumer/shops_spec.rb": 12.581299543380737, - "spec/helpers/admin/business_model_configuration_helper_spec.rb": 0.4121088981628418, - "spec/helpers/checkout_helper_spec.rb": 0.017447471618652344, - "spec/helpers/enterprises_helper_spec.rb": 3.16050124168396, - "spec/helpers/groups_helper_spec.rb": 0.008687734603881836, - "spec/helpers/html_helper_spec.rb": 0.12075495719909668, - "spec/helpers/injection_helper_spec.rb": 13.099636316299438, - "spec/helpers/navigation_helper_spec.rb": 0.037546634674072266, - "spec/helpers/order_cycles_helper_spec.rb": 0.5602025985717773, - "spec/helpers/products_helper_spec.rb": 0.009445667266845703, - "spec/helpers/shared_helper_spec.rb": 0.021656036376953125, - "spec/helpers/shop_helper_spec.rb": 0.06465697288513184, - "spec/jobs/confirm_order_job_spec.rb": 0.04894542694091797, - "spec/jobs/confirm_signup_job_spec.rb": 0.15027284622192383, - "spec/jobs/finalize_account_invoices_spec.rb": 4.740641832351685, - "spec/jobs/heartbeat_job_spec.rb": 0.020777225494384766, - "spec/jobs/order_cycle_notification_job_spec.rb": 2.3326029777526855, - "spec/jobs/products_cache_integrity_checker_job_spec.rb": 2.176734685897827, - "spec/jobs/refresh_products_cache_job_spec.rb": 0.14060688018798828, - "spec/jobs/update_account_invoices_spec.rb": 18.77696418762207, - "spec/jobs/update_billable_periods_spec.rb": 5.19831395149231, - "spec/jobs/welcome_enterprise_job_spec.rb": 0.05716228485107422, - "spec/lib/open_food_network/bulk_coop_report_spec.rb": 5.522400140762329, - "spec/lib/open_food_network/cached_products_renderer_spec.rb": 0.0779104232788086, - "spec/lib/open_food_network/customers_report_spec.rb": 2.789498805999756, - "spec/lib/open_food_network/distribution_change_validator_spec.rb": 0.12454366683959961, - "spec/lib/open_food_network/enterprise_fee_applicator_spec.rb": 1.0582823753356934, - "spec/lib/open_food_network/enterprise_fee_calculator_spec.rb": 9.390950918197632, - "spec/lib/open_food_network/enterprise_injection_data_spec.rb": 0.30153727531433105, - "spec/lib/open_food_network/enterprise_issue_validator_spec.rb": 0.09757637977600098, - "spec/lib/open_food_network/feature_toggle_spec.rb": 0.011552810668945312, - "spec/lib/open_food_network/group_buy_report_spec.rb": 4.40512228012085, - "spec/lib/open_food_network/last_used_address_spec.rb": 0.4271695613861084, - "spec/lib/open_food_network/lettuce_share_report_spec.rb": 2.5144362449645996, - "spec/lib/open_food_network/option_value_namer_spec.rb": 0.47345566749572754, - "spec/lib/open_food_network/order_and_distributor_report_spec.rb": 1.1131298542022705, - "spec/lib/open_food_network/order_cycle_form_applicator_spec.rb": 7.482408761978149, - "spec/lib/open_food_network/order_cycle_management_report_spec.rb": 2.9279346466064453, - "spec/lib/open_food_network/order_cycle_permissions_spec.rb": 27.034855365753174, - "spec/lib/open_food_network/order_grouper_spec.rb": 0.0347137451171875, - "spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb": 6.190460443496704, - "spec/lib/open_food_network/packing_report_spec.rb": 5.795913934707642, - "spec/lib/open_food_network/permissions_spec.rb": 9.981389284133911, - "spec/lib/open_food_network/products_and_inventory_report_spec.rb": 4.536849737167358, - "spec/lib/open_food_network/products_cache_refreshment_spec.rb": 0.3400561809539795, - "spec/lib/open_food_network/products_cache_spec.rb": 13.676804304122925, - "spec/lib/open_food_network/products_renderer_spec.rb": 5.883910894393921, - "spec/lib/open_food_network/property_merge_spec.rb": 0.28261804580688477, - "spec/lib/open_food_network/referer_parser_spec.rb": 0.016553640365600586, - "spec/lib/open_food_network/reports/report_spec.rb": 0.027279138565063477, - "spec/lib/open_food_network/reports/row_spec.rb": 0.004057884216308594, - "spec/lib/open_food_network/reports/rule_spec.rb": 0.018927812576293945, - "spec/lib/open_food_network/sales_tax_report_spec.rb": 0.1267712116241455, - "spec/lib/open_food_network/scope_variant_to_hub_spec.rb": 5.346986532211304, - "spec/lib/open_food_network/tag_rule_applicator_spec.rb": 2.972744941711426, - "spec/lib/open_food_network/user_balance_calculator_spec.rb": 6.290127754211426, - "spec/lib/open_food_network/users_and_enterprises_report_spec.rb": 0.435042142868042, - "spec/lib/open_food_network/xero_invoices_report_spec.rb": 1.327892780303955, - "spec/lib/spree/product_filters_spec.rb": 0.14480209350585938, - "spec/mailers/enterprise_mailer_spec.rb": 1.2255680561065674, - "spec/mailers/order_mailer_spec.rb": 1.9922146797180176, - "spec/mailers/producer_mailer_spec.rb": 28.504019021987915, - "spec/mailers/user_mailer_spec.rb": 0.06116366386413574, - "spec/models/adjustment_metadata_spec.rb": 0.22940421104431152, - "spec/models/billable_period_spec.rb": 5.919523477554321, - "spec/models/calculator/weight_spec.rb": 0.011056900024414062, - "spec/models/cart_spec.rb": 4.7867491245269775, - "spec/models/column_preference_spec.rb": 0.12476158142089844, - "spec/models/content_configuration_spec.rb": 0.0069255828857421875, - "spec/models/coordinator_fee_spec.rb": 0.1413099765777588, - "spec/models/customer_spec.rb": 0.9213364124298096, - "spec/models/enterprise_caching_spec.rb": 0.8015868663787842, - "spec/models/enterprise_fee_spec.rb": 3.8326468467712402, - "spec/models/enterprise_group_spec.rb": 0.35931992530822754, - "spec/models/enterprise_relationship_spec.rb": 7.324019908905029, - "spec/models/enterprise_spec.rb": 20.723163843154907, - "spec/models/exchange_fee_spec.rb": 0.24502134323120117, - "spec/models/exchange_spec.rb": 15.133025646209717, - "spec/models/inventory_item_spec.rb": 0.2637319564819336, - "spec/models/model_set_spec.rb": 0.2381300926208496, - "spec/models/order_cycle_spec.rb": 19.834176540374756, - "spec/models/producer_property_spec.rb": 0.12196111679077148, - "spec/models/product_distribution_spec.rb": 2.8120880126953125, - "spec/models/spree/ability_spec.rb": 16.657139778137207, - "spec/models/spree/addresses_spec.rb": 0.06702327728271484, - "spec/models/spree/adjustment_spec.rb": 13.998104333877563, - "spec/models/spree/classification_spec.rb": 0.7607810497283936, - "spec/models/spree/image_spec.rb": 2.1546812057495117, - "spec/models/spree/line_item_spec.rb": 18.319732189178467, - "spec/models/spree/option_type_spec.rb": 0.38923072814941406, - "spec/models/spree/option_value_spec.rb": 0.4280354976654053, - "spec/models/spree/order_populator_spec.rb": 1.4095511436462402, - "spec/models/spree/order_spec.rb": 9.809221029281616, - "spec/models/spree/payment_method_spec.rb": 0.5280671119689941, - "spec/models/spree/payment_spec.rb": 2.4764130115509033, - "spec/models/spree/preference_spec.rb": 0.059625864028930664, - "spec/models/spree/preferences/file_configuration_spec.rb": 0.038376808166503906, - "spec/models/spree/price_spec.rb": 0.5022625923156738, - "spec/models/spree/product_property_spec.rb": 0.3601999282836914, - "spec/models/spree/product_spec.rb": 16.564993381500244, - "spec/models/spree/property_spec.rb": 1.6884117126464844, - "spec/models/spree/shipping_method_spec.rb": 2.2080821990966797, - "spec/models/spree/tax_rate_spec.rb": 0.37114739418029785, - "spec/models/spree/taxon_spec.rb": 1.0655884742736816, - "spec/models/spree/user_spec.rb": 16.094335317611694, - "spec/models/spree/variant_spec.rb": 14.885905027389526, - "spec/models/tag_rule/filter_order_cycles_spec.rb": 0.21634840965270996, - "spec/models/tag_rule/filter_payment_methods_spec.rb": 0.4332749843597412, - "spec/models/tag_rule/filter_products_spec.rb": 0.21471834182739258, - "spec/models/tag_rule/filter_shipping_methods_spec.rb": 0.3680458068847656, - "spec/models/tag_rule_spec.rb": 0.05348682403564453, - "spec/models/variant_override_spec.rb": 5.598196029663086, - "spec/performance/injection_helper_spec.rb": 4.83400297164917, - "spec/performance/orders_controller_spec.rb": 0.028135061264038086, - "spec/performance/shop_controller_spec.rb": 14.35703420639038, - "spec/requests/large_request_spec.rb": 0.024456262588500977, - "spec/requests/shop_spec.rb": 1.0987565517425537, - "spec/serializers/admin/customer_serializer_spec.rb": 0.0909874439239502, - "spec/serializers/admin/enterprise_serializer_spec.rb": 0.06178736686706543, - "spec/serializers/admin/exchange_serializer_spec.rb": 2.586963653564453, - "spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb": 0.8507771492004395, - "spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb": 0.6380510330200195, - "spec/serializers/admin/index_enterprise_serializer_spec.rb": 0.19609999656677246, - "spec/serializers/admin/variant_override_serializer_spec.rb": 0.27136850357055664, - "spec/serializers/enterprise_serializer_spec.rb": 0.22696876525878906, - "spec/serializers/order_serializer_spec.rb": 1.3858006000518799, - "spec/serializers/orders_by_distributor_serializer_spec.rb": 3.6581554412841797, - "spec/serializers/spree/product_serializer_spec.rb": 0.17654776573181152, - "spec/serializers/spree/variant_serializer_spec.rb": 0.2116224765777588 + "spec/controllers/admin/accounts_and_billing_settings_controller_spec.rb": 8.00712776184082, + "spec/controllers/admin/business_model_configuration_controller_spec.rb": 0.6013796329498291, + "spec/controllers/admin/column_preferences_controller_spec.rb": 0.6397809982299805, + "spec/controllers/admin/customers_controller_spec.rb": 1.770902156829834, + "spec/controllers/admin/enterprises_controller_spec.rb": 12.199191331863403, + "spec/controllers/admin/inventory_items_controller_spec.rb": 6.397810935974121, + "spec/controllers/admin/order_cycles_controller_spec.rb": 5.024861812591553, + "spec/controllers/admin/tag_rules_controller_spec.rb": 0.5270967483520508, + "spec/controllers/admin/variant_overrides_controller_spec.rb": 7.399488925933838, + "spec/controllers/api/enterprises_controller_spec.rb": 0.42368054389953613, + "spec/controllers/api/order_cycles_controller_spec.rb": 3.215219259262085, + "spec/controllers/api/statuses_controller_spec.rb": 0.06884050369262695, + "spec/controllers/base_controller_spec.rb": 0.03565478324890137, + "spec/controllers/cart_controller_spec.rb": 1.8274929523468018, + "spec/controllers/checkout_controller_spec.rb": 2.7706351280212402, + "spec/controllers/enterprise_confirmations_controller_spec.rb": 1.4932606220245361, + "spec/controllers/enterprises_controller_spec.rb": 6.069838762283325, + "spec/controllers/groups_controller_spec.rb": 0.6966016292572021, + "spec/controllers/registration_controller_spec.rb": 0.16758990287780762, + "spec/controllers/shop_controller_spec.rb": 5.656877517700195, + "spec/controllers/shops_controller_spec.rb": 0.9992175102233887, + "spec/controllers/spree/admin/adjustments_controller_spec.rb": 1.6936447620391846, + "spec/controllers/spree/admin/base_controller_spec.rb": 0.27201366424560547, + "spec/controllers/spree/admin/line_items_controller_spec.rb": 32.69303798675537, + "spec/controllers/spree/admin/orders_controller_spec.rb": 23.98875856399536, + "spec/controllers/spree/admin/overview_controller_spec.rb": 1.6584975719451904, + "spec/controllers/spree/admin/payment_methods_controller_spec.rb": 0.6059751510620117, + "spec/controllers/spree/admin/products_controller_spec.rb": 2.822031021118164, + "spec/controllers/spree/admin/reports_controller_spec.rb": 97.03786563873291, + "spec/controllers/spree/admin/search_controller_spec.rb": 2.2991843223571777, + "spec/controllers/spree/admin/variants_controller_spec.rb": 2.7615997791290283, + "spec/controllers/spree/api/line_items_controller_spec.rb": 0.519737720489502, + "spec/controllers/spree/api/products_controller_spec.rb": 15.634222030639648, + "spec/controllers/spree/api/variants_controller_spec.rb": 6.291322231292725, + "spec/controllers/spree/checkout_controller_spec.rb": 0.7929987907409668, + "spec/controllers/spree/orders_controller_spec.rb": 7.134824752807617, + "spec/controllers/spree/paypal_controller_spec.rb": 0.03331351280212402, + "spec/controllers/spree/store_controller_spec.rb": 0.05157065391540527, + "spec/controllers/spree/user_sessions_controller_spec.rb": 0.1207573413848877, + "spec/controllers/user_passwords_controller_spec.rb": 0.5991458892822266, + "spec/controllers/user_registrations_controller_spec.rb": 0.378939151763916, + "spec/features/admin/account_spec.rb": 0.9386508464813232, + "spec/features/admin/accounts_and_billing_settings_spec.rb": 21.184889793395996, + "spec/features/admin/adjustments_spec.rb": 8.66775107383728, + "spec/features/admin/authentication_spec.rb": 25.82377004623413, + "spec/features/admin/bulk_order_management_spec.rb": 164.24249410629272, + "spec/features/admin/bulk_product_update_spec.rb": 90.47183227539062, + "spec/features/admin/business_model_configuration_spec.rb": 2.6363422870635986, + "spec/features/admin/caching_spec.rb": 1.2790143489837646, + "spec/features/admin/content_spec.rb": 1.8612418174743652, + "spec/features/admin/customers_spec.rb": 56.48403525352478, + "spec/features/admin/enterprise_fees_spec.rb": 20.162436962127686, + "spec/features/admin/enterprise_groups_spec.rb": 11.802961111068726, + "spec/features/admin/enterprise_relationships_spec.rb": 15.264024496078491, + "spec/features/admin/enterprise_roles_spec.rb": 6.748473167419434, + "spec/features/admin/enterprise_user_spec.rb": 3.581881523132324, + "spec/features/admin/enterprises/index_spec.rb": 8.482917070388794, + "spec/features/admin/enterprises_spec.rb": 49.892003297805786, + "spec/features/admin/external_services_spec.rb": 0.6888332366943359, + "spec/features/admin/image_settings_spec.rb": 0.7543714046478271, + "spec/features/admin/order_cycles_spec.rb": 96.08795166015625, + "spec/features/admin/orders_spec.rb": 68.66088509559631, + "spec/features/admin/overview_spec.rb": 5.806450366973877, + "spec/features/admin/payment_method_spec.rb": 16.470131158828735, + "spec/features/admin/product_import_spec.rb": 35.73773694038391, + "spec/features/admin/products_spec.rb": 22.878377199172974, + "spec/features/admin/reports_spec.rb": 156.537535905838, + "spec/features/admin/shipping_methods_spec.rb": 9.275840759277344, + "spec/features/admin/tag_rules_spec.rb": 28.019567728042603, + "spec/features/admin/tax_settings_spec.rb": 0.8582248687744141, + "spec/features/admin/variant_overrides_spec.rb": 68.96011137962341, + "spec/features/admin/variants_spec.rb": 7.299575567245483, + "spec/features/consumer/account_spec.rb": 17.58187508583069, + "spec/features/consumer/authentication_spec.rb": 16.81424307823181, + "spec/features/consumer/external_services_spec.rb": 0.6560425758361816, + "spec/features/consumer/groups_spec.rb": 8.783919095993042, + "spec/features/consumer/producers_spec.rb": 13.732691049575806, + "spec/features/consumer/registration_spec.rb": 39.105244636535645, + "spec/features/consumer/shopping/cart_spec.rb": 10.158715724945068, + "spec/features/consumer/shopping/checkout_auth_spec.rb": 11.223945617675781, + "spec/features/consumer/shopping/checkout_spec.rb": 90.94879722595215, + "spec/features/consumer/shopping/shopping_spec.rb": 78.30096817016602, + "spec/features/consumer/shopping/variant_overrides_spec.rb": 83.33234310150146, + "spec/features/consumer/shops_spec.rb": 25.136919498443604, + "spec/features/consumer/sitemap_spec.rb": 0.12806296348571777, + "spec/helpers/admin/business_model_configuration_helper_spec.rb": 2.4084229469299316, + "spec/helpers/checkout_helper_spec.rb": 0.11321282386779785, + "spec/helpers/enterprises_helper_spec.rb": 4.312507390975952, + "spec/helpers/groups_helper_spec.rb": 0.012323379516601562, + "spec/helpers/html_helper_spec.rb": 0.06277966499328613, + "spec/helpers/injection_helper_spec.rb": 18.53943371772766, + "spec/helpers/navigation_helper_spec.rb": 0.06260061264038086, + "spec/helpers/order_cycles_helper_spec.rb": 0.7225909233093262, + "spec/helpers/products_helper_spec.rb": 0.010187149047851562, + "spec/helpers/shared_helper_spec.rb": 0.027238845825195312, + "spec/helpers/shop_helper_spec.rb": 0.08263945579528809, + "spec/jobs/confirm_order_job_spec.rb": 0.054292917251586914, + "spec/jobs/confirm_signup_job_spec.rb": 0.025557994842529297, + "spec/jobs/finalize_account_invoices_spec.rb": 6.124559640884399, + "spec/jobs/heartbeat_job_spec.rb": 0.025922060012817383, + "spec/jobs/order_cycle_notification_job_spec.rb": 3.4907965660095215, + "spec/jobs/products_cache_integrity_checker_job_spec.rb": 0.7591326236724854, + "spec/jobs/refresh_products_cache_job_spec.rb": 0.14359831809997559, + "spec/jobs/update_account_invoices_spec.rb": 25.14212441444397, + "spec/jobs/update_billable_periods_spec.rb": 7.548252820968628, + "spec/jobs/welcome_enterprise_job_spec.rb": 0.07267236709594727, + "spec/lib/open_food_network/bulk_coop_report_spec.rb": 6.935410737991333, + "spec/lib/open_food_network/cached_products_renderer_spec.rb": 0.1050572395324707, + "spec/lib/open_food_network/customers_report_spec.rb": 3.6295740604400635, + "spec/lib/open_food_network/distribution_change_validator_spec.rb": 0.16306090354919434, + "spec/lib/open_food_network/enterprise_fee_applicator_spec.rb": 1.435694932937622, + "spec/lib/open_food_network/enterprise_fee_calculator_spec.rb": 13.101191759109497, + "spec/lib/open_food_network/enterprise_injection_data_spec.rb": 0.5890538692474365, + "spec/lib/open_food_network/enterprise_issue_validator_spec.rb": 0.12314796447753906, + "spec/lib/open_food_network/feature_toggle_spec.rb": 0.01638960838317871, + "spec/lib/open_food_network/group_buy_report_spec.rb": 7.850870132446289, + "spec/lib/open_food_network/last_used_address_spec.rb": 0.03868246078491211, + "spec/lib/open_food_network/lettuce_share_report_spec.rb": 4.304181098937988, + "spec/lib/open_food_network/option_value_namer_spec.rb": 0.09165167808532715, + "spec/lib/open_food_network/order_and_distributor_report_spec.rb": 1.9102568626403809, + "spec/lib/open_food_network/order_cycle_form_applicator_spec.rb": 10.645771026611328, + "spec/lib/open_food_network/order_cycle_management_report_spec.rb": 3.786757469177246, + "spec/lib/open_food_network/order_cycle_permissions_spec.rb": 37.860092639923096, + "spec/lib/open_food_network/order_grouper_spec.rb": 0.046637773513793945, + "spec/lib/open_food_network/orders_and_fulfillments_report_spec.rb": 8.23448920249939, + "spec/lib/open_food_network/packing_report_spec.rb": 7.166008949279785, + "spec/lib/open_food_network/permissions_spec.rb": 14.218589782714844, + "spec/lib/open_food_network/products_and_inventory_report_spec.rb": 6.037408828735352, + "spec/lib/open_food_network/products_cache_refreshment_spec.rb": 0.43891096115112305, + "spec/lib/open_food_network/products_cache_spec.rb": 20.597163438796997, + "spec/lib/open_food_network/products_renderer_spec.rb": 8.173654317855835, + "spec/lib/open_food_network/property_merge_spec.rb": 0.4074442386627197, + "spec/lib/open_food_network/referer_parser_spec.rb": 0.0202939510345459, + "spec/lib/open_food_network/reports/report_spec.rb": 0.03491067886352539, + "spec/lib/open_food_network/reports/row_spec.rb": 0.00494694709777832, + "spec/lib/open_food_network/reports/rule_spec.rb": 0.02185201644897461, + "spec/lib/open_food_network/sales_tax_report_spec.rb": 0.2910194396972656, + "spec/lib/open_food_network/scope_variant_to_hub_spec.rb": 7.502454519271851, + "spec/lib/open_food_network/tag_rule_applicator_spec.rb": 3.9437036514282227, + "spec/lib/open_food_network/user_balance_calculator_spec.rb": 10.744928121566772, + "spec/lib/open_food_network/users_and_enterprises_report_spec.rb": 0.5333633422851562, + "spec/lib/open_food_network/xero_invoices_report_spec.rb": 1.6665534973144531, + "spec/lib/spree/product_filters_spec.rb": 0.17748093605041504, + "spec/mailers/enterprise_mailer_spec.rb": 0.44661855697631836, + "spec/mailers/order_mailer_spec.rb": 2.8625006675720215, + "spec/mailers/producer_mailer_spec.rb": 38.360108375549316, + "spec/mailers/user_mailer_spec.rb": 0.07233500480651855, + "spec/models/adjustment_metadata_spec.rb": 0.2955667972564697, + "spec/models/billable_period_spec.rb": 7.409304141998291, + "spec/models/calculator/flat_percent_per_item_spec.rb": 0.011715412139892578, + "spec/models/calculator/weight_spec.rb": 0.013759613037109375, + "spec/models/cart_spec.rb": 7.489097833633423, + "spec/models/column_preference_spec.rb": 0.12457847595214844, + "spec/models/content_configuration_spec.rb": 0.005526542663574219, + "spec/models/coordinator_fee_spec.rb": 0.1765611171722412, + "spec/models/customer_spec.rb": 0.7244162559509277, + "spec/models/enterprise_caching_spec.rb": 5.130070209503174, + "spec/models/enterprise_fee_spec.rb": 5.398890018463135, + "spec/models/enterprise_group_spec.rb": 0.4309041500091553, + "spec/models/enterprise_relationship_spec.rb": 11.179081439971924, + "spec/models/enterprise_spec.rb": 28.993456602096558, + "spec/models/exchange_fee_spec.rb": 0.31769680976867676, + "spec/models/exchange_spec.rb": 23.51863121986389, + "spec/models/inventory_item_spec.rb": 0.35733699798583984, + "spec/models/model_set_spec.rb": 1.2280616760253906, + "spec/models/order_cycle_spec.rb": 28.214235305786133, + "spec/models/producer_property_spec.rb": 3.207193613052368, + "spec/models/product_distribution_spec.rb": 4.137674570083618, + "spec/models/product_importer_spec.rb": 0.1319897174835205, + "spec/models/spree/ability_spec.rb": 23.034581184387207, + "spec/models/spree/addresses_spec.rb": 0.08480358123779297, + "spec/models/spree/adjustment_spec.rb": 19.734310150146484, + "spec/models/spree/calculator/flat_percent_item_total_spec.rb": 0.011153221130371094, + "spec/models/spree/calculator/flexi_rate_spec.rb": 0.016460657119750977, + "spec/models/spree/calculator/per_item_spec.rb": 0.006463527679443359, + "spec/models/spree/calculator/price_sack_spec.rb": 0.006432294845581055, + "spec/models/spree/classification_spec.rb": 0.8814013004302979, + "spec/models/spree/image_spec.rb": 3.7731142044067383, + "spec/models/spree/line_item_spec.rb": 24.783292293548584, + "spec/models/spree/option_type_spec.rb": 1.5399932861328125, + "spec/models/spree/option_value_spec.rb": 0.6168241500854492, + "spec/models/spree/order_populator_spec.rb": 1.8702630996704102, + "spec/models/spree/order_spec.rb": 13.248756647109985, + "spec/models/spree/payment_method_spec.rb": 0.5306015014648438, + "spec/models/spree/payment_spec.rb": 2.695707082748413, + "spec/models/spree/preference_spec.rb": 0.07149314880371094, + "spec/models/spree/preferences/file_configuration_spec.rb": 0.04694247245788574, + "spec/models/spree/price_spec.rb": 0.9263966083526611, + "spec/models/spree/product_property_spec.rb": 0.5712625980377197, + "spec/models/spree/product_spec.rb": 24.63173007965088, + "spec/models/spree/property_spec.rb": 9.745300054550171, + "spec/models/spree/shipping_method_spec.rb": 2.9844515323638916, + "spec/models/spree/tax_rate_spec.rb": 0.4417886734008789, + "spec/models/spree/taxon_spec.rb": 2.0731396675109863, + "spec/models/spree/user_spec.rb": 22.796449184417725, + "spec/models/spree/variant_spec.rb": 22.682554483413696, + "spec/models/tag_rule/filter_order_cycles_spec.rb": 0.2527964115142822, + "spec/models/tag_rule/filter_payment_methods_spec.rb": 0.36441707611083984, + "spec/models/tag_rule/filter_products_spec.rb": 0.249955415725708, + "spec/models/tag_rule/filter_shipping_methods_spec.rb": 1.038332462310791, + "spec/models/tag_rule_spec.rb": 0.06748843193054199, + "spec/models/variant_override_spec.rb": 7.998929977416992, + "spec/performance/injection_helper_spec.rb": 10.3869309425354, + "spec/performance/orders_controller_spec.rb": 0.029515743255615234, + "spec/performance/shop_controller_spec.rb": 20.927804470062256, + "spec/requests/large_request_spec.rb": 0.026319265365600586, + "spec/requests/shop_spec.rb": 1.9282338619232178, + "spec/serializers/admin/customer_serializer_spec.rb": 0.25053882598876953, + "spec/serializers/admin/enterprise_serializer_spec.rb": 0.07662534713745117, + "spec/serializers/admin/exchange_serializer_spec.rb": 4.449889421463013, + "spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb": 1.3775944709777832, + "spec/serializers/admin/for_order_cycle/supplied_product_serializer_spec.rb": 0.6928744316101074, + "spec/serializers/admin/index_enterprise_serializer_spec.rb": 0.22203683853149414, + "spec/serializers/admin/variant_override_serializer_spec.rb": 0.3394124507904053, + "spec/serializers/enterprise_serializer_spec.rb": 0.8813695907592773, + "spec/serializers/order_serializer_spec.rb": 1.798943281173706, + "spec/serializers/orders_by_distributor_serializer_spec.rb": 5.19302225112915, + "spec/serializers/spree/product_serializer_spec.rb": 0.24654531478881836, + "spec/serializers/spree/variant_serializer_spec.rb": 0.8831884860992432 } \ No newline at end of file diff --git a/lib/open_food_network/available_payment_method_filter.rb b/lib/open_food_network/available_payment_method_filter.rb new file mode 100644 index 0000000000..379beb22b5 --- /dev/null +++ b/lib/open_food_network/available_payment_method_filter.rb @@ -0,0 +1,23 @@ +module OpenFoodNetwork + class AvailablePaymentMethodFilter + def filter!(payment_methods) + if stripe_enabled? + payment_methods.reject!{ |p| p.type.ends_with?("StripeConnect") && stripe_configuration_incomplete?(p) } + else + payment_methods.reject!{ |p| p.type.ends_with?("StripeConnect") } + end + end + + private + + def stripe_enabled? + Spree::Config.stripe_connect_enabled && Stripe.publishable_key + end + + def stripe_configuration_incomplete?(payment_method) + return true if payment_method.preferred_enterprise_id.zero? + + payment_method.stripe_account_id.blank? + end + end +end diff --git a/lib/open_food_network/column_preference_defaults.rb b/lib/open_food_network/column_preference_defaults.rb index 41eef63c09..6972fa3785 100644 --- a/lib/open_food_network/column_preference_defaults.rb +++ b/lib/open_food_network/column_preference_defaults.rb @@ -16,7 +16,7 @@ module OpenFoodNetwork sku: { name: I18n.t("admin.sku"), visible: false }, price: { name: I18n.t("admin.price"), visible: true }, on_hand: { name: I18n.t("admin.on_hand"), visible: true }, - on_demand: { name: I18n.t("admin.on_demand?"), visible: false }, + on_demand: { name: I18n.t("admin.on_demand?"), visible: true }, reset: { name: I18n.t("#{node}.enable_reset?"), visible: false }, inheritance: { name: I18n.t("#{node}.inherit?"), visible: false }, tags: { name: I18n.t("admin.tags"), visible: false }, @@ -64,7 +64,7 @@ module OpenFoodNetwork unit: { name: I18n.t("#{node}.unit"), visible: true }, price: { name: I18n.t("admin.price"), visible: true }, on_hand: { name: I18n.t("admin.on_hand"), visible: true }, - on_demand: { name: I18n.t("admin.on_demand"), visible: false }, + on_demand: { name: I18n.t("admin.on_demand"), visible: true }, category: { name: I18n.t("#{node}.category"), visible: false }, tax_category: { name: I18n.t("#{node}.tax_category"), visible: false }, inherits_properties: { name: I18n.t("#{node}.inherits_properties?"), visible: false }, diff --git a/lib/open_food_network/enterprise_fee_applicator.rb b/lib/open_food_network/enterprise_fee_applicator.rb index e6d52e1749..e213204047 100644 --- a/lib/open_food_network/enterprise_fee_applicator.rb +++ b/lib/open_food_network/enterprise_fee_applicator.rb @@ -1,7 +1,7 @@ module OpenFoodNetwork class EnterpriseFeeApplicator < Struct.new(:enterprise_fee, :variant, :role) def create_line_item_adjustment(line_item) - a = enterprise_fee.create_locked_adjustment(line_item_adjustment_label, line_item.order, line_item, true) + a = enterprise_fee.create_adjustment(line_item_adjustment_label, line_item.order, line_item, true) AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role @@ -9,7 +9,7 @@ module OpenFoodNetwork end def create_order_adjustment(order) - a = enterprise_fee.create_locked_adjustment(order_adjustment_label, order, order, true) + a = enterprise_fee.create_adjustment(order_adjustment_label, order, order, true) AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role diff --git a/lib/open_food_network/enterprise_issue_validator.rb b/lib/open_food_network/enterprise_issue_validator.rb index f755a6faf1..eccc5a4a68 100644 --- a/lib/open_food_network/enterprise_issue_validator.rb +++ b/lib/open_food_network/enterprise_issue_validator.rb @@ -1,7 +1,7 @@ module OpenFoodNetwork class EnterpriseIssueValidator include Rails.application.routes.url_helpers - include Spree::Core::UrlHelpers + include Spree::TestingSupport::UrlHelpers def initialize(enterprise) @enterprise = enterprise diff --git a/lib/open_food_network/users_and_enterprises_report.rb b/lib/open_food_network/users_and_enterprises_report.rb index fd580393be..58cb9688c8 100644 --- a/lib/open_food_network/users_and_enterprises_report.rb +++ b/lib/open_food_network/users_and_enterprises_report.rb @@ -30,7 +30,7 @@ module OpenFoodNetwork uae["sells"], uae["visible"], to_local_datetime(uae["confirmed_at"]) - ] + ] end end diff --git a/lib/spree/core/calculated_adjustments_decorator.rb b/lib/spree/core/calculated_adjustments_decorator.rb index 6a42eb68c5..592bf59854 100644 --- a/lib/spree/core/calculated_adjustments_decorator.rb +++ b/lib/spree/core/calculated_adjustments_decorator.rb @@ -1,13 +1,15 @@ module Spree module Core module CalculatedAdjustments - module ClassMethods - def calculated_adjustments_with_explicit_class_name - calculated_adjustments_without_explicit_class_name - # Class name is mis-inferred outside of Spree namespace - has_one :calculator, as: :calculable, dependent: :destroy, class_name: 'Spree::Calculator' + class << self + def included_with_explicit_class_name(klass) + included_without_explicit_class_name(klass) + + klass.class_eval do + has_one :calculator, as: :calculable, dependent: :destroy, class_name: 'Spree::Calculator' + end end - alias_method_chain :calculated_adjustments, :explicit_class_name + alias_method_chain :included, :explicit_class_name end end end diff --git a/lib/stripe/account_connector.rb b/lib/stripe/account_connector.rb new file mode 100644 index 0000000000..9d0c8d3ddb --- /dev/null +++ b/lib/stripe/account_connector.rb @@ -0,0 +1,60 @@ +# Encapsulation of logic used to handle the response from Stripe following an +# attempt to connect an account to the instance using the OAuth Connection Flow +# https://stripe.com/docs/connect/standard-accounts#oauth-flow + +module Stripe + class AccountConnector + attr_reader :user, :params + + def initialize(user, params) + @user = user + @params = params + end + + def create_account + return false if connection_cancelled_by_user? + + raise StripeError, params["error_description"] unless params["code"] + raise CanCan::AccessDenied unless state.keys.include? "enterprise_id" + + # Local authorisation issue, so request disconnection from Stripe + deauthorize unless user_has_permission_to_connect? + + StripeAccount.create( + stripe_user_id: token.stripe_user_id, + stripe_publishable_key: token.stripe_publishable_key, + enterprise: enterprise + ) + end + + def connection_cancelled_by_user? + params[:action] == "connect_callback" && params[:error] == "access_denied" + end + + def enterprise + @enterprise ||= Enterprise.find_by_permalink(state["enterprise_id"]) + end + + private + + def state + # Returns the original payload + key = Openfoodnetwork::Application.config.secret_token + JWT.decode(params["state"], key, true, algorithm: 'HS256')[0] + end + + def token + # Request an access token based on the code provided + @token ||= OAuth.token(grant_type: 'authorization_code', code: params["code"]) + end + + def deauthorize + OAuth.deauthorize(stripe_user_id: token.stripe_user_id) + raise CanCan::AccessDenied + end + + def user_has_permission_to_connect? + user.enterprises.include?(enterprise) || user.admin? + end + end +end diff --git a/lib/stripe/profile_storer.rb b/lib/stripe/profile_storer.rb new file mode 100644 index 0000000000..9b1579228d --- /dev/null +++ b/lib/stripe/profile_storer.rb @@ -0,0 +1,63 @@ +# Encapsulation of logic used to convert a token generated by Stripe Elements +# into a Stripe Customer + Card which can then be charged at a later point in time +# Stores the generated customer & card ids against the local instance of Spree::CreditCard + +module Stripe + class ProfileStorer + def initialize(payment, provider) + @payment = payment + @provider = provider + end + + def create_customer_from_token + token = @payment.source.gateway_payment_profile_id + response = @provider.store(token, options) + + if response.success? + attrs = source_attrs_from(response) + @payment.source.update_attributes!(attrs) + else + @payment.send(:gateway_error, response.message) + end + end + + private + + def options + { + email: @payment.order.email, + login: Stripe.api_key, + address: address_for(@payment) + } + end + + def address_for(payment) + {}.tap do |hash| + if address = payment.order.bill_address + hash = { + address1: address.address1, + address2: address.address2, + city: address.city, + zip: address.zipcode + } + + if address.country + hash[:country] = address.country.name + end + + if address.state + hash[:state] = address.state.name + end + end + end + end + + 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'] + } + end + end +end diff --git a/lib/stripe/webhook_handler.rb b/lib/stripe/webhook_handler.rb new file mode 100644 index 0000000000..875dd9e52b --- /dev/null +++ b/lib/stripe/webhook_handler.rb @@ -0,0 +1,34 @@ +module Stripe + class WebhookHandler + def initialize(event) + @event = event + end + + def handle + return :unknown unless known_event? + send(event_mappings[@event.type]) + end + + private + + def event_mappings + { + "account.application.deauthorized" => :deauthorize + } + end + + def known_event? + event_mappings.keys.include? @event.type + end + + def deauthorize + return :ignored unless @event.respond_to?(:account) + destroyed = destroy_stripe_accounts_linked_to(@event.account) + destroyed.any? ? :success : :ignored + end + + def destroy_stripe_accounts_linked_to(account) + StripeAccount.where(stripe_user_id: account).destroy_all + end + end +end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index db5c2bad0c..cbbdc9d66a 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -9,6 +9,13 @@ namespace :openfoodnetwork do require_relative '../../spec/support/spree/init' task_name = "openfoodnetwork:dev:load_sample_data" + # -- MailMethod + # TODO: Remove me when in Spree 2.0. See http://guides.spreecommerce.org/release_notes/spree_2_0_0.html#mailmethod-model-no-longer-exists + Spree::MailMethod.create!( + environment: Rails.env, + preferred_mails_from: 'spree@example.com' + ) + # -- Shipping / payment information unless Spree::Zone.find_by_name 'Australia' puts "[#{task_name}] Seeding shipping / payment information" diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake index 3755dacc63..7cc245cfa4 100644 --- a/lib/tasks/karma.rake +++ b/lib/tasks/karma.rake @@ -1,28 +1,22 @@ +ENV["RAILS_ENV"] ||= 'test' + namespace :karma do - task :start => :environment do |task| - continue_only_in_test_env task + task :start => :environment do |_task| with_tmp_config :start end - task :run => :environment do |task| - continue_only_in_test_env task + task :run => :environment do |_task| with_tmp_config :start, "--single-run" end private - def continue_only_in_test_env task - if Rails.env != 'test' - raise "Task must be called in test environment:\n bundle exec rake #{task.name} RAILS_ENV=test" - end - end - def with_tmp_config(command, args = nil) Tempfile.open('karma_unit.js', Rails.root.join('tmp') ) do |f| f.write unit_js(application_spec_files << i18n_file) f.flush trap('SIGINT') { puts "Killing Karma"; exit } - exec "karma #{command} #{f.path} #{args}" + exec "node_modules/.bin/karma #{command} #{f.path} #{args}" end end diff --git a/public/embedded-group-preview.html b/public/embedded-group-preview.html new file mode 100644 index 0000000000..a15a5c6a17 --- /dev/null +++ b/public/embedded-group-preview.html @@ -0,0 +1,20 @@ + + Embedded Group + + +

+ This is a preview page for embedded groups. + Choose a group to display by copying its permalink id into the URL after the question mark. + Example: embedded-group-preview.html?flavour-crusader +

+ + + + + + diff --git a/spec/controllers/admin/bulk_line_items_controller_spec.rb b/spec/controllers/admin/bulk_line_items_controller_spec.rb new file mode 100644 index 0000000000..da1f753dce --- /dev/null +++ b/spec/controllers/admin/bulk_line_items_controller_spec.rb @@ -0,0 +1,258 @@ +require 'spec_helper' + +describe Admin::BulkLineItemsController do + include AuthenticationWorkflow + + describe '#index' do + render_views + + let(:line_item_attributes) { %i[id quantity max_quantity price supplier final_weight_volume units_product units_variant order] } + let!(:dist1) { FactoryGirl.create(:distributor_enterprise) } + let!(:order1) { FactoryGirl.create(:order, state: 'complete', completed_at: 1.day.ago, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } + let!(:order2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } + let!(:order3) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } + let!(:line_item1) { FactoryGirl.create(:line_item, order: order1) } + let!(:line_item2) { FactoryGirl.create(:line_item, order: order2) } + let!(:line_item3) { FactoryGirl.create(:line_item, order: order2) } + let!(:line_item4) { FactoryGirl.create(:line_item, order: order3) } + + context "as a normal user" do + before { controller.stub spree_current_user: create_enterprise_user } + + it "should deny me access to the index action" do + spree_get :index, :format => :json + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "as an administrator" do + before do + controller.stub spree_current_user: quick_login_as_admin + end + + context "when no ransack params are passed in" do + before do + spree_get :index, :format => :json + end + + it "retrieves a list of line_items with appropriate attributes, including line items with appropriate attributes" do + keys = json_response.first.keys.map(&:to_sym) + line_item_attributes.all?{ |attr| keys.include? attr }.should == true + end + + it "sorts line_items in ascending id line_item" do + ids = json_response.map{ |line_item| line_item['id'] } + ids[0].should < ids[1] + ids[1].should < ids[2] + end + + it "formats final_weight_volume as a float" do + json_response.map{ |line_item| line_item['final_weight_volume'] }.all?{ |fwv| fwv.is_a?(Float) }.should == true + end + + it "returns distributor object with id key" do + json_response.map{ |line_item| line_item['supplier'] }.all?{ |d| d.key?('id') }.should == true + end + end + + context "when ransack params are passed in for line items" do + before do + spree_get :index, :format => :json, q: { order_id_eq: order2.id } + end + + it "retrives a list of line items which match the criteria" do + expect(json_response.map{ |line_item| line_item['id'] }).to eq [line_item2.id, line_item3.id] + end + end + + context "when ransack params are passed in for orders" do + before do + spree_get :index, :format => :json, q: { order: { completed_at_gt: 2.hours.ago } } + end + + it "retrives a list of line items whose orders match the criteria" do + expect(json_response.map{ |line_item| line_item['id'] }).to eq [line_item2.id, line_item3.id, line_item4.id] + end + end + end + + context "as an enterprise user" do + let(:supplier) { create(:supplier_enterprise) } + let(:distributor1) { create(:distributor_enterprise) } + let(:distributor2) { create(:distributor_enterprise) } + let(:coordinator) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } + let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } + let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } + let!(:line_item2) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } + let!(:order2) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor2, billing_address: FactoryGirl.create(:address) ) } + let!(:line_item3) { FactoryGirl.create(:line_item, order: order2, product: FactoryGirl.create(:product, supplier: supplier)) } + + context "producer enterprise" do + before do + controller.stub spree_current_user: supplier.owner + spree_get :index, :format => :json + end + + it "does not display line items for which my enterprise is a supplier" do + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "coordinator enterprise" do + before do + controller.stub spree_current_user: coordinator.owner + spree_get :index, :format => :json + end + + it "retrieves a list of line_items" do + keys = json_response.first.keys.map(&:to_sym) + line_item_attributes.all?{ |attr| keys.include? attr }.should == true + end + end + + context "hub enterprise" do + before do + controller.stub spree_current_user: distributor1.owner + spree_get :index, :format => :json + end + + it "retrieves a list of line_items" do + keys = json_response.first.keys.map(&:to_sym) + line_item_attributes.all?{ |attr| keys.include? attr }.should == true + end + end + end + end + + describe '#update' do + let(:supplier) { create(:supplier_enterprise) } + let(:distributor1) { create(:distributor_enterprise) } + let(:coordinator) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } + let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } + let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } + let(:line_item_params) { { quantity: 3, final_weight_volume: 3000, price: 3.00 } } + let(:params) { { id: line_item1.id, order_id: order1.number, line_item: line_item_params } } + + context "as an enterprise user" do + context "producer enterprise" do + before do + controller.stub spree_current_user: supplier.owner + spree_put :update, params + end + + it "does not allow access" do + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "coordinator enterprise" do + render_views + + before do + controller.stub spree_current_user: coordinator.owner + end + + # Used in admin/orders/bulk_management + context 'when the request is JSON (angular)' do + before { params[:format] = :json } + + it "updates the line item" do + spree_put :update, params + line_item1.reload + expect(line_item1.quantity).to eq 3 + expect(line_item1.final_weight_volume).to eq 3000 + expect(line_item1.price).to eq 3.00 + end + + it "returns an empty JSON response" do + spree_put :update, params + expect(response.body).to eq ' ' + end + + it 'returns a 204 response' do + spree_put :update, params + expect(response.status).to eq 204 + end + + it 'applies enterprise fees locking the order with an exclusive row lock' do + allow(Spree::LineItem) + .to receive(:find).with(line_item1.id.to_s).and_return(line_item1) + + expect(line_item1.order).to receive(:reload).with(lock: true) + expect(line_item1.order).to receive(:update_distribution_charge!) + + spree_put :update, params + end + + context 'when the line item params are not correct' do + let(:line_item_params) { { price: 'hola' } } + let(:errors) { { 'price' => ['is not a number'] } } + + it 'returns a JSON with the errors' do + spree_put :update, params + expect(JSON.parse(response.body)['errors']).to eq(errors) + end + + it 'returns a 412 response' do + spree_put :update, params + expect(response.status).to eq 412 + end + end + end + end + + context "hub enterprise" do + before do + controller.stub spree_current_user: distributor1.owner + xhr :put, :update, params + end + + it "updates the line item" do + line_item1.reload + expect(line_item1.quantity).to eq 3 + expect(line_item1.final_weight_volume).to eq 3000 + expect(line_item1.price).to eq 3.00 + end + end + end + end + + describe '#destroy' do + render_views + + let(:supplier) { create(:supplier_enterprise) } + let(:distributor1) { create(:distributor_enterprise) } + let(:coordinator) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } + let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } + let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } + let(:params) { { id: line_item1.id, order_id: order1.number } } + + before do + controller.stub spree_current_user: coordinator.owner + end + + # Used in admin/orders/bulk_management + context 'when the request is JSON (angular)' do + before { params[:format] = :json } + + it 'destroys the line item' do + expect { + spree_delete :destroy, params + }.to change { Spree::LineItem.where(id: line_item1).count }.from(1).to(0) + end + + it 'returns an empty JSON response' do + spree_delete :destroy, params + expect(response.body).to eq ' ' + end + + it 'returns a 204 response' do + spree_delete :destroy, params + expect(response.status).to eq 204 + end + end + end +end diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 94282e6c2c..5e2300d122 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -144,7 +144,6 @@ module Admin expect(distributor.users).to_not include user end - describe "enterprise properties" do let(:producer) { create(:enterprise) } let!(:property) { create(:property, name: "A nice name") } diff --git a/spec/controllers/admin/stripe_accounts_controller_spec.rb b/spec/controllers/admin/stripe_accounts_controller_spec.rb new file mode 100644 index 0000000000..9a6854ae85 --- /dev/null +++ b/spec/controllers/admin/stripe_accounts_controller_spec.rb @@ -0,0 +1,167 @@ +require 'spec_helper' + +describe Admin::StripeAccountsController, type: :controller do + let(:enterprise) { create(:distributor_enterprise) } + + before do + allow(Stripe).to receive(:client_id) { "some_id" } + end + + describe "#connect" do + before do + allow(controller).to receive(:spree_current_user) { enterprise.owner } + end + + it "redirects to Stripe Authorization url constructed OAuth" do + spree_get :connect + expect(response.location).to match %r(\Ahttps://connect.stripe.com) + uri = URI.parse(response.location) + params = CGI.parse(uri.query) + expect(params.keys).to include 'client_id', 'response_type', 'state', 'scope' + end + end + + describe "#destroy" do + let(:params) { { format: :json, id: "some_id" } } + + context "when the specified stripe account doesn't exist" do + it "raises an error?" do + spree_delete :destroy, params + end + end + + context "when the specified stripe account exists" do + let(:stripe_account) { create(:stripe_account, enterprise: enterprise) } + + before do + # So that we can stub #deauthorize_and_destroy + allow(StripeAccount).to receive(:find) { stripe_account } + params[:id] = stripe_account.id + end + + context "when I don't manage the enterprise linked to the stripe account" do + let(:some_user) { create(:user) } + + before { allow(controller).to receive(:spree_current_user) { some_user } } + + it "redirects to unauthorized" do + spree_delete :destroy, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "when I manage the enterprise linked to the stripe account" do + before { allow(controller).to receive(:spree_current_user) { enterprise.owner } } + + context "and the attempt to deauthorize_and_destroy succeeds" do + before { allow(stripe_account).to receive(:deauthorize_and_destroy) { stripe_account } } + + it "redirects to unauthorized" do + spree_delete :destroy, params + expect(response).to redirect_to edit_admin_enterprise_path(enterprise) + expect(flash[:success]).to eq "Stripe account disconnected." + end + end + + context "and the attempt to deauthorize_and_destroy fails" do + before { allow(stripe_account).to receive(:deauthorize_and_destroy) { false } } + + it "redirects to unauthorized" do + spree_delete :destroy, params + expect(response).to redirect_to edit_admin_enterprise_path(enterprise) + expect(flash[:error]).to eq "Failed to disconnect Stripe." + end + end + end + end + end + + describe "#status" do + let(:params) { { format: :json, enterprise_id: enterprise.id } } + + before do + allow(Stripe).to receive(:api_key) { "sk_test_12345" } + Spree::Config.set(stripe_connect_enabled: false) + end + + context "when I don't manage the specified enterprise" do + let(:user) { create(:user) } + + before do + allow(controller).to receive(:spree_current_user) { user } + end + + it "redirects to unauthorized" do + spree_get :status, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "when I manage the specified enterprise" do + before do + allow(controller).to receive(:spree_current_user) { enterprise.owner } + end + + context "when Stripe is not enabled" do + it "returns with a status of 'stripe_disabled'" do + spree_get :status, params + json_response = JSON.parse(response.body) + expect(json_response["status"]).to eq "stripe_disabled" + end + end + + context "when Stripe is enabled" do + before { Spree::Config.set(stripe_connect_enabled: true) } + + context "when no stripe account is associated with the specified enterprise" do + it "returns with a status of 'account_missing'" do + spree_get :status, params + json_response = JSON.parse(response.body) + expect(json_response["status"]).to eq "account_missing" + end + end + + context "when a stripe account is associated with the specified enterprise" do + let!(:account) { create(:stripe_account, stripe_user_id: "acc_123", enterprise: enterprise) } + + context "but access has been revoked or does not exist on stripe's servers" do + before do + stub_request(:get, "https://api.stripe.com/v1/accounts/acc_123").to_return(status: 404) + end + + it "returns with a status of 'access_revoked'" do + spree_get :status, params + json_response = JSON.parse(response.body) + expect(json_response["status"]).to eq "access_revoked" + end + end + + context "which is connected" do + let(:stripe_account_mock) do + { + id: "acc_123", + business_name: "My Org", + charges_enabled: true, + some_other_attr: "something" + } + end + + before do + stub_request(:get, "https://api.stripe.com/v1/accounts/acc_123").to_return(body: JSON.generate(stripe_account_mock)) + end + + it "returns with a status of 'connected'" do + spree_get :status, params + json_response = JSON.parse(response.body) + expect(json_response["status"]).to eq "connected" + # serializes required attrs + expect(json_response["business_name"]).to eq "My Org" + # ignores other attrs + expect(json_response["some_other_attr"]).to be nil + end + end + end + end + end + end +end diff --git a/spec/controllers/admin/stripe_connect_settings_controller_spec.rb b/spec/controllers/admin/stripe_connect_settings_controller_spec.rb new file mode 100644 index 0000000000..7d0b47974e --- /dev/null +++ b/spec/controllers/admin/stripe_connect_settings_controller_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe Admin::StripeConnectSettingsController, type: :controller do + let(:user) { create(:user) } + let(:admin) { create(:admin_user) } + + before do + Spree::Config.set(stripe_connect_enabled: true) + end + + describe "edit" do + context "as an enterprise user" do + before { allow(controller).to receive(:spree_current_user) { user } } + + it "does not allow access" do + spree_get :edit + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "as super admin" do + before { allow(controller).to receive(:spree_current_user) { admin } } + + context "when a Stripe API key is not set" do + before do + allow(Stripe).to receive(:api_key) { nil } + end + + it "sets the account status to :empty_api_key" do + spree_get :edit + expect(assigns(:stripe_account)[:status]).to eq :empty_api_key + expect(assigns(:settings).stripe_connect_enabled).to be true + end + end + + context "when a Stripe API key is set" do + before do + allow(Stripe).to receive(:api_key) { "sk_test_xxxx" } + end + + context "and the request to retrieve Stripe account info fails" do + before do + stub_request(:get, "https://api.stripe.com/v1/account"). + to_return(:status => 401, :body => "{\"error\": {\"message\": \"Invalid API Key provided: sk_test_****xxxx\"}}") + end + + it "sets the account status to :auth_fail" do + spree_get :edit + expect(assigns(:stripe_account)[:status]).to eq :auth_fail + expect(assigns(:settings).stripe_connect_enabled).to be true + end + end + + context "and the request to retrieve Stripe account info succeeds" do + before do + stub_request(:get, "https://api.stripe.com/v1/account"). + to_return(:status => 200, :body => "{ \"id\": \"acct_1234\", \"business_name\": \"OFN\" }") + end + + it "sets the account status to :ok, loads settings into Struct" do + spree_get :edit + expect(assigns(:stripe_account)[:status]).to eq :ok + expect(assigns(:obfuscated_secret_key)).to eq "sk_test_****xxxx" + expect(assigns(:settings).stripe_connect_enabled).to be true + end + end + end + end + end + + describe "update" do + let(:params) { { settings: { stripe_connect_enabled: false } } } + + context "as an enterprise user" do + before { allow(controller).to receive(:spree_current_user) { user } } + + it "does not allow access" do + spree_get :update, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "as super admin" do + before { allow(controller).to receive(:spree_current_user) { admin } } + + it "sets global config to the specified values" do + expect(Spree::Config.stripe_connect_enabled).to be true + spree_get :update, params + expect(Spree::Config.stripe_connect_enabled).to be false + end + end + end +end diff --git a/spec/controllers/checkout_controller_spec.rb b/spec/controllers/checkout_controller_spec.rb index 1f5e40a841..70332b5ea5 100644 --- a/spec/controllers/checkout_controller_spec.rb +++ b/spec/controllers/checkout_controller_spec.rb @@ -4,6 +4,8 @@ describe CheckoutController do let(:distributor) { double(:distributor) } let(:order_cycle) { create(:simple_order_cycle) } let(:order) { create(:order) } + let(:reset_order_service) { double(ResetOrderService) } + before do order.stub(:checkout_allowed?).and_return true controller.stub(:check_authorization).and_return true @@ -67,14 +69,14 @@ describe CheckoutController do it "clears the ship address when re-rendering edit" do controller.should_receive(:clear_ship_address).and_return true order.stub(:update_attributes).and_return false - spree_post :update, order: {} + spree_post :update, format: :json, order: {} end it "clears the ship address when the order state cannot be advanced" do controller.should_receive(:clear_ship_address).and_return true order.stub(:update_attributes).and_return true order.stub(:next).and_return false - spree_post :update, order: {} + spree_post :update, format: :json, order: {} end it "only clears the ship address with a pickup shipping method" do @@ -82,6 +84,44 @@ describe CheckoutController do order.should_receive(:ship_address=) controller.send(:clear_ship_address) end + + context 'when completing the order' do + before do + order.state = 'complete' + allow(order).to receive(:update_attributes).and_return(true) + allow(order).to receive(:next).and_return(true) + allow(order).to receive(:set_distributor!).and_return(true) + end + + it "sets the new order's token to the same as the old order" do + order = controller.current_order(true) + spree_post :update, order: {} + expect(controller.current_order.token).to eq order.token + end + + it 'expires the current order' do + allow(controller).to receive(:expire_current_order) + put :update, order: {} + expect(controller).to have_received(:expire_current_order) + end + + it 'sets the access_token of the session' do + put :update, order: {} + expect(session[:access_token]).to eq(controller.current_order.token) + end + end + end + + describe '#expire_current_order' do + it 'empties the order_id of the session' do + expect(session).to receive(:[]=).with(:order_id, nil) + controller.expire_current_order + end + + it 'resets the @current_order ivar' do + controller.expire_current_order + expect(controller.instance_variable_get(:@current_order)).to be_nil + end end context "via xhr" do @@ -93,7 +133,7 @@ describe CheckoutController do end it "returns errors" do - xhr :post, :update, order: {}, use_route: :spree + spree_post :update, format: :json, order: {} response.status.should == 400 response.body.should == {errors: assigns[:order].errors, flash: {}}.to_json end @@ -101,21 +141,27 @@ describe CheckoutController do it "returns flash" do order.stub(:update_attributes).and_return true order.stub(:next).and_return false - xhr :post, :update, order: {}, use_route: :spree + spree_post :update, format: :json, order: {} response.body.should == {errors: assigns[:order].errors, flash: {error: "Payment could not be processed, please check the details you entered"}}.to_json end it "returns order confirmation url on success" do + allow(ResetOrderService).to receive(:new).with(controller, order) { reset_order_service } + expect(reset_order_service).to receive(:call) + order.stub(:update_attributes).and_return true order.stub(:state).and_return "complete" - xhr :post, :update, order: {}, use_route: :spree + spree_post :update, format: :json, order: {} response.status.should == 200 response.body.should == {path: spree.order_path(order)}.to_json end describe "stale object handling" do it "retries when a stale object error is encountered" do + allow(ResetOrderService).to receive(:new).with(controller, order) { reset_order_service } + expect(reset_order_service).to receive(:call) + order.stub(:update_attributes).and_return true controller.stub(:state_callback) @@ -127,7 +173,7 @@ describe CheckoutController do true end - xhr :post, :update, order: {}, use_route: :spree + spree_post :update, format: :json, order: {} response.status.should == 200 end @@ -135,7 +181,7 @@ describe CheckoutController do order.stub(:update_attributes).and_return true order.stub(:next) { raise ActiveRecord::StaleObjectError.new(Spree::Variant.new, 'update') } - xhr :post, :update, order: {}, use_route: :spree + spree_post :update, format: :json, order: {} response.status.should == 400 end end @@ -144,16 +190,72 @@ describe CheckoutController do describe "Paypal routing" do let(:payment_method) { create(:payment_method, type: "Spree::Gateway::PayPalExpress") } before do - controller.stub(:current_distributor).and_return(distributor) - controller.stub(:current_order_cycle).and_return(order_cycle) - controller.stub(:current_order).and_return(order) + allow(controller).to receive(:current_distributor) { distributor } + allow(controller).to receive(:current_order_cycle) { order_cycle } + allow(controller).to receive(:current_order) { order } + allow(controller).to receive(:restart_checkout) end it "should check the payment method for Paypalness if we've selected one" do - Spree::PaymentMethod.should_receive(:find).with(payment_method.id.to_s).and_return payment_method - order.stub(:update_attributes).and_return true - order.stub(:state).and_return "payment" + expect(Spree::PaymentMethod).to receive(:find).with(payment_method.id.to_s) { payment_method } + allow(order).to receive(:update_attributes) { true } + allow(order).to receive(:state) { "payment" } spree_post :update, order: {payments_attributes: [{payment_method_id: payment_method.id}]} end end + + describe "#update_failed" do + before do + controller.instance_variable_set(:@order, order) + end + + it "clears the shipping address and restarts the checkout" do + expect(controller).to receive(:clear_ship_address) + expect(controller).to receive(:restart_checkout) + expect(controller).to receive(:respond_to) + controller.send(:update_failed) + end + end + + describe "#restart_checkout" do + let!(:shipment_pending) { create(:shipment, order: order, state: 'pending') } + let!(:payment_checkout) { create(:payment, order: order, state: 'checkout') } + let!(:payment_failed) { create(:payment, order: order, state: 'failed') } + + before do + order.update_attribute(:shipping_method_id, shipment_pending.shipping_method_id) + controller.instance_variable_set(:@order, order.reload) + end + + context "when the order is already in the 'cart' state" do + it "does nothing" do + expect(order).to_not receive(:restart_checkout!) + controller.send(:restart_checkout) + end + end + + context "when the order is in a subsequent state" do + before do + order.update_attribute(:state, "payment") + end + + # NOTE: at the time of writing, it was not possible to create a shipment with a state other than + # 'pending' when the order has not been completed, so this is not a case that requires testing. + it "resets the order state, and clears incomplete shipments and payments" do + expect(order).to receive(:restart_checkout!).and_call_original + expect(order.shipping_method_id).to_not be nil + expect(order.shipments.count).to be 1 + expect(order.adjustments.shipping.count).to be 1 + expect(order.payments.count).to be 2 + expect(order.adjustments.payment_fee.count).to be 2 + controller.send(:restart_checkout) + expect(order.reload.state).to eq 'cart' + expect(order.shipping_method_id).to be nil + expect(order.shipments.count).to be 0 + expect(order.adjustments.shipping.count).to be 0 + expect(order.payments.count).to be 1 + expect(order.adjustments.payment_fee.count).to be 1 + end + end + end end diff --git a/spec/controllers/line_items_controller_spec.rb b/spec/controllers/line_items_controller_spec.rb index 49c7a15742..db9ebabe8e 100644 --- a/spec/controllers/line_items_controller_spec.rb +++ b/spec/controllers/line_items_controller_spec.rb @@ -35,6 +35,9 @@ describe LineItemsController do item end + let(:order) { item.order } + let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], variants: [order.line_item_variants]) } + before { controller.stub spree_current_user: item.order.user } context "without a line item id" do @@ -55,42 +58,32 @@ describe LineItemsController do end context "where the item's order is associated with the current user" do - before { item.order.update_attributes(user_id: user.id) } + before { order.update_attributes!(user_id: user.id) } - context "without an order cycle" do + context "without an order cycle or distributor" do it "denies deletion" do delete :destroy, params expect(response.status).to eq 403 end end - context "with an order cycle" do - before { item.order.update_attributes(order_cycle_id: order_cycle.id) } + context "with an order cycle and distributor" do + before { order.update_attributes!(order_cycle_id: order_cycle.id, distributor_id: distributor.id) } - context "without a distributor" do + context "where changes are not allowed" do it "denies deletion" do delete :destroy, params expect(response.status).to eq 403 end end - context "where the item's order has a distributor" do - before { item.order.update_attributes(distributor_id: distributor.id) } - context "where changes are not allowed" do - it "denies deletion" do - delete :destroy, params - expect(response.status).to eq 403 - end - end + context "where changes are allowed" do + before { distributor.update_attributes!(allow_order_changes: true) } - context "where changes are allowed" do - before { distributor.update_attributes(allow_order_changes: true) } - - it "deletes the line item" do - delete :destroy, params - expect(response.status).to eq 204 - expect { item.reload }.to raise_error ActiveRecord::RecordNotFound - end + it "deletes the line item" do + delete :destroy, params + expect(response.status).to eq 204 + expect { item.reload }.to raise_error ActiveRecord::RecordNotFound end end end @@ -127,7 +120,7 @@ describe LineItemsController do order.shipments.last.reload expect(order.adjustment_total).to eq initial_fees - shipping_fee - payment_fee expect(order.shipments.last.adjustment.amount).to eq shipping_fee - expect(order.payment.adjustment.amount).to eq payment_fee + expect(order.payments.first.adjustment.amount).to eq payment_fee expect(order.shipments.last.adjustment.included_tax).to eq 0.6 end end diff --git a/spec/controllers/spree/admin/line_items_controller_spec.rb b/spec/controllers/spree/admin/line_items_controller_spec.rb index 907bec2bbb..b8b6cd2785 100644 --- a/spec/controllers/spree/admin/line_items_controller_spec.rb +++ b/spec/controllers/spree/admin/line_items_controller_spec.rb @@ -3,129 +3,6 @@ require 'spec_helper' describe Spree::Admin::LineItemsController do include AuthenticationWorkflow - describe "#index" do - render_views - - let(:line_item_attributes) { [:id, :quantity, :max_quantity, :price, :supplier, :final_weight_volume, :units_product, :units_variant, :order] } - let!(:dist1) { FactoryGirl.create(:distributor_enterprise) } - let!(:order1) { FactoryGirl.create(:order, state: 'complete', completed_at: 1.day.ago, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:order2) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:order3) { FactoryGirl.create(:order, state: 'complete', completed_at: Time.zone.now, distributor: dist1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1) } - let!(:line_item2) { FactoryGirl.create(:line_item, order: order2) } - let!(:line_item3) { FactoryGirl.create(:line_item, order: order2) } - let!(:line_item4) { FactoryGirl.create(:line_item, order: order3) } - - context "as a normal user" do - before { controller.stub spree_current_user: create_enterprise_user } - - it "should deny me access to the index action" do - spree_get :index, :format => :json - expect(response).to redirect_to spree.unauthorized_path - end - end - - context "as an administrator" do - - before do - controller.stub spree_current_user: quick_login_as_admin - end - - context "when no ransack params are passed in" do - before do - spree_get :index, :format => :json - end - - it "retrieves a list of line_items with appropriate attributes, including line items with appropriate attributes" do - keys = json_response.first.keys.map{ |key| key.to_sym } - line_item_attributes.all?{ |attr| keys.include? attr }.should == true - end - - it "sorts line_items in ascending id line_item" do - ids = json_response.map{ |line_item| line_item['id'] } - ids[0].should < ids[1] - ids[1].should < ids[2] - end - - it "formats final_weight_volume as a float" do - json_response.map{ |line_item| line_item['final_weight_volume'] }.all?{ |fwv| fwv.is_a?(Float) }.should == true - end - - it "returns distributor object with id key" do - json_response.map{ |line_item| line_item['supplier'] }.all?{ |d| d.has_key?('id') }.should == true - end - end - - context "when ransack params are passed in for line items" do - before do - spree_get :index, :format => :json, q: { order_id_eq: order2.id } - end - - it "retrives a list of line items which match the criteria" do - expect(json_response.map{ |line_item| line_item['id'] }).to eq [line_item2.id, line_item3.id] - end - end - - context "when ransack params are passed in for orders" do - before do - spree_get :index, :format => :json, q: { order: { completed_at_gt: 2.hours.ago } } - end - - it "retrives a list of line items whose orders match the criteria" do - expect(json_response.map{ |line_item| line_item['id'] }).to eq [line_item2.id, line_item3.id, line_item4.id] - end - end - end - - context "as an enterprise user" do - let(:supplier) { create(:supplier_enterprise) } - let(:distributor1) { create(:distributor_enterprise) } - let(:distributor2) { create(:distributor_enterprise) } - let(:coordinator) { create(:distributor_enterprise) } - let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } - let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } - let!(:line_item2) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } - let!(:order2) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor2, billing_address: FactoryGirl.create(:address) ) } - let!(:line_item3) { FactoryGirl.create(:line_item, order: order2, product: FactoryGirl.create(:product, supplier: supplier)) } - - context "producer enterprise" do - before do - controller.stub spree_current_user: supplier.owner - spree_get :index, :format => :json - end - - it "does not display line items for which my enterprise is a supplier" do - expect(response).to redirect_to spree.unauthorized_path - end - end - - context "coordinator enterprise" do - before do - controller.stub spree_current_user: coordinator.owner - spree_get :index, :format => :json - end - - it "retrieves a list of line_items" do - keys = json_response.first.keys.map{ |key| key.to_sym } - line_item_attributes.all?{ |attr| keys.include? attr }.should == true - end - end - - context "hub enterprise" do - before do - controller.stub spree_current_user: distributor1.owner - spree_get :index, :format => :json - end - - it "retrieves a list of line_items" do - keys = json_response.first.keys.map{ |key| key.to_sym } - line_item_attributes.all?{ |attr| keys.include? attr }.should == true - end - end - end - end - describe "#create" do let!(:variant) { create(:variant, price: 88) } let!(:vo) { create(:variant_override, hub: distributor, variant: variant, price: 11.11) } @@ -143,14 +20,15 @@ describe Spree::Admin::LineItemsController do end end - describe "#update" do + describe '#update' do let(:supplier) { create(:supplier_enterprise) } let(:distributor1) { create(:distributor_enterprise) } let(:coordinator) { create(:distributor_enterprise) } let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator) } let!(:order1) { FactoryGirl.create(:order, order_cycle: order_cycle, state: 'complete', completed_at: Time.zone.now, distributor: distributor1, billing_address: FactoryGirl.create(:address) ) } let!(:line_item1) { FactoryGirl.create(:line_item, order: order1, product: FactoryGirl.create(:product, supplier: supplier)) } - let(:params) { { format: :json, id: line_item1.id, order_id: order1.number, line_item: { quantity: 3, final_weight_volume: 3000, price: 3.00 } } } + let(:line_item_params) { { quantity: 3, final_weight_volume: 3000, price: 3.00 } } + let(:params) { { id: line_item1.id, order_id: order1.number, line_item: line_item_params } } context "as an enterprise user" do context "producer enterprise" do @@ -165,26 +43,65 @@ describe Spree::Admin::LineItemsController do end context "coordinator enterprise" do + render_views + before do controller.stub spree_current_user: coordinator.owner - spree_put :update, params end - it "updates the line_item" do - line_item1.reload - expect(line_item1.quantity).to eq 3 - expect(line_item1.final_weight_volume).to eq 3000 - expect(line_item1.price).to eq 3.00 + # Used in admin/orders/edit + context 'when the request is JS/XHR (jquery-rails gem)' do + it "updates the line item" do + xhr :put, :update, params + line_item1.reload + expect(line_item1.quantity).to eq 3 + expect(line_item1.final_weight_volume).to eq 3000 + expect(line_item1.price).to eq 3.00 + end + + it "returns an empty JSON response" do + xhr :put, :update, params + expect(response.body).to eq ' ' + end + + it 'returns a 204 response' do + xhr :put, :update, params + expect(response.status).to eq 204 + end + + context 'when the line item params are not correct' do + let(:line_item_params) { { price: 'hola' } } + let(:errors) { { 'price' => ['is not a number'] } } + + it 'returns a JSON with the errors' do + xhr :put, :update, params + expect(JSON.parse(response.body)['errors']).to eq(errors) + end + + it 'returns a 412 response' do + xhr :put, :update, params + expect(response.status).to eq 412 + end + end + end + + context 'when the request is HTML' do + before { params[:format] = :html } + + it 'returns an HTML response with the order form' do + spree_put :update, params + expect(response.body).to match(/admin_order_form_fields/) + end end end context "hub enterprise" do before do controller.stub spree_current_user: distributor1.owner - spree_put :update, params + xhr :put, :update, params end - it "retrieves a list of line_items" do + it "updates the line item" do line_item1.reload expect(line_item1.quantity).to eq 3 expect(line_item1.final_weight_volume).to eq 3000 diff --git a/spec/controllers/spree/admin/orders_controller_spec.rb b/spec/controllers/spree/admin/orders_controller_spec.rb index 5ecc393dab..60bdf9a3d9 100644 --- a/spec/controllers/spree/admin/orders_controller_spec.rb +++ b/spec/controllers/spree/admin/orders_controller_spec.rb @@ -179,6 +179,12 @@ describe Spree::Admin::OrdersController do context "when the distributor's ABN has been set" do before { distributor.update_attribute(:abn, "123") } + before do + Spree::MailMethod.create!( + environment: Rails.env, + preferred_mails_from: 'spree@example.com' + ) + end it "should allow me to send order invoices" do expect do spree_get :invoice, params diff --git a/spec/controllers/spree/admin/payment_methods_controller_spec.rb b/spec/controllers/spree/admin/payment_methods_controller_spec.rb index c27fe16f19..681d7c5309 100644 --- a/spec/controllers/spree/admin/payment_methods_controller_spec.rb +++ b/spec/controllers/spree/admin/payment_methods_controller_spec.rb @@ -1,6 +1,61 @@ require 'spec_helper' describe Spree::Admin::PaymentMethodsController do + describe "#update" do + context "on a StripeConnect payment method" do + let!(:user) { create(:user, enterprise_limit: 2) } + let!(:enterprise1) { create(:distributor_enterprise, owner: user) } + let!(:enterprise2) { create(:distributor_enterprise, owner: create(:user)) } + let!(:payment_method) { create(:stripe_payment_method, distributor_ids: [enterprise1.id, enterprise2.id], preferred_enterprise_id: enterprise2.id) } + + before { allow(controller).to receive(:spree_current_user) { user } } + + context "when an attempt is made to change the stripe account holder (preferred_enterprise_id)" do + let(:params) { { id: payment_method.id, payment_method: { type: "Spree::Gateway::StripeConnect", preferred_enterprise_id: enterprise1.id } } } + + context "as a user that does not manage the existing stripe account holder" do + it "prevents the stripe account holder from being updated" do + spree_put :update, params + expect(payment_method.reload.preferred_enterprise_id).to eq enterprise2.id + end + end + + context "as a user that manages the existing stripe account holder" do + before { enterprise2.update_attributes!(owner_id: user.id) } + + it "allows the stripe account holder to be updated" do + spree_put :update, params + expect(payment_method.reload.preferred_enterprise_id).to eq enterprise1.id + end + + context "when no enterprise is selected as the account holder" do + before { payment_method.update_attribute(:preferred_enterprise_id, nil) } + + context "id not provided at all" do + before { params[:payment_method].delete(:preferred_enterprise_id) } + + it "does not save the payment method" do + spree_put :update, params + expect(response).to render_template :edit + expect(assigns(:payment_method).errors.messages[:stripe_account_owner]).to include I18n.t(:error_required) + end + end + + context "enterprise_id of 0" do + before { params[:payment_method][:preferred_enterprise_id] = 0 } + + it "does not save the payment method" do + spree_put :update, params + expect(response).to render_template :edit + expect(assigns(:payment_method).errors.messages[:stripe_account_owner]).to include I18n.t(:error_required) + end + end + end + end + end + end + end + context "Requesting provider preference fields" do let(:enterprise) { create(:distributor_enterprise) } let(:user) do diff --git a/spec/controllers/spree/admin/payments_controller_spec.rb b/spec/controllers/spree/admin/payments_controller_spec.rb new file mode 100644 index 0000000000..cfc1b3336c --- /dev/null +++ b/spec/controllers/spree/admin/payments_controller_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe Spree::Admin::PaymentsController do + let!(:shop) { create(:enterprise) } + let!(:user) { shop.owner } + let!(:order) { create(:order, distributor: shop) } + let!(:line_item) { create(:line_item, order: order, price: 5.0) } + + context "as an enterprise user" do + before do + allow(controller).to receive(:spree_current_user) { user } + order.reload.update_totals + end + + context "requesting a refund on a payment" do + let(:params) { { id: payment.id, order_id: order.number, e: :void } } + + # Required for the respond override in the controller decorator to work + before { @request.env['HTTP_REFERER'] = spree.admin_order_payments_url(payment) } + + context "that was processed by stripe" do + let!(:payment_method) { create(:stripe_payment_method, distributors: [shop], preferred_enterprise_id: shop.id) } + # let!(:credit_card) { create(:credit_card, gateway_customer_profile_id: "cus_1", gateway_payment_profile_id: 'card_2') } + let!(:payment) { create(:payment, order: order, state: 'completed', payment_method: payment_method, response_code: 'ch_1a2b3c', amount: order.total) } + + + before do + allow(Stripe).to receive(:api_key) { "sk_test_12345" } + end + + context "where the request succeeds" do + before do + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges/ch_1a2b3c/refunds"). + to_return(:status => 200, :body => JSON.generate(id: 're_123', object: 'refund', status: 'succeeded') ) + end + + it "voids the payment" do + order.reload + expect(order.payment_total).to_not eq 0 + expect(order.outstanding_balance).to eq 0 + spree_put :fire, params + expect(payment.reload.state).to eq 'void' + order.reload + expect(order.payment_total).to eq 0 + expect(order.outstanding_balance).to_not eq 0 + end + end + + context "where the request fails" do + before do + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges/ch_1a2b3c/refunds"). + to_return(:status => 200, :body => JSON.generate(error: { message: "Bup-bow!"}) ) + end + + it "does not void the payment" do + order.reload + expect(order.payment_total).to_not eq 0 + expect(order.outstanding_balance).to eq 0 + spree_put :fire, params + expect(payment.reload.state).to eq 'completed' + order.reload + expect(order.payment_total).to_not eq 0 + expect(order.outstanding_balance).to eq 0 + expect(flash[:error]).to eq "Bup-bow!" + end + end + end + end + end +end diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb index b8d76abd9c..02a5d6bced 100644 --- a/spec/controllers/spree/admin/products_controller_spec.rb +++ b/spec/controllers/spree/admin/products_controller_spec.rb @@ -12,7 +12,7 @@ describe Spree::Admin::ProductsController do end it "denies access" do - response.should redirect_to "http://test.host/unauthorized" + response.should redirect_to spree.unauthorized_url end it "does not update any product" do diff --git a/spec/controllers/spree/api/products_controller_spec.rb b/spec/controllers/spree/api/products_controller_spec.rb index 312c6a29b4..1969a0abaf 100644 --- a/spec/controllers/spree/api/products_controller_spec.rb +++ b/spec/controllers/spree/api/products_controller_spec.rb @@ -107,5 +107,43 @@ module Spree product1.deleted_at.should_not be_nil end end + + describe '#clone' do + before do + spree_post :clone, product_id: product1.id, format: :json + end + + context 'as a normal user' do + sign_in_as_user! + + it 'denies access' do + assert_unauthorized! + end + end + + context 'as an enterprise user' do + sign_in_as_enterprise_user! [:supplier] + + it 'responds with a successful response' do + expect(response.status).to eq(201) + end + + it 'clones the product' do + expect(json_response['name']).to eq("COPY OF #{product1.name}") + end + end + + context 'as an administrator' do + sign_in_as_admin! + + it 'responds with a successful response' do + expect(response.status).to eq(201) + end + + it 'clones the product' do + expect(json_response['name']).to eq("COPY OF #{product1.name}") + end + end + end end end diff --git a/spec/controllers/spree/checkout_controller_spec.rb b/spec/controllers/spree/checkout_controller_spec.rb index b9b85c92fa..116c18955b 100644 --- a/spec/controllers/spree/checkout_controller_spec.rb +++ b/spec/controllers/spree/checkout_controller_spec.rb @@ -4,51 +4,19 @@ require 'support/request/authentication_workflow' describe Spree::CheckoutController do - include AuthenticationWorkflow + context 'rendering edit from within spree for the current checkout state' do + let(:order) { controller.current_order(true) } + let(:user) { create(:user) } - context "After completing an order" do - it "should create a new empty order" do - controller.current_order(true) - controller.send(:after_complete) - session[:order_id].should_not be_nil + before do + create(:line_item, order: order) + + allow(controller).to receive(:skip_state_validation?) { true } + allow(controller).to receive(:spree_current_user) { user } end - it "should clear the current order cache" do - order = controller.current_order(true) - controller.send(:after_complete) - controller.current_order.should_not == order - end - - it "should set the new order's distributor to the same as the old order" do - order = controller.current_order(true) - distributor = create(:distributor_enterprise) - order.set_distributor!(distributor) - - controller.send(:after_complete) - - controller.current_order.distributor.should == distributor - end - - it "should set the new order's token to the same as the old order, and preserve the access token in the session" do - order = controller.current_order(true) - - controller.send(:after_complete) - - controller.current_order.token.should == order.token - session[:access_token].should == order.token - end - end - - context "rendering edit from within spree for the current checkout state" do - let!(:order) { controller.current_order(true) } - let!(:line_item) { create(:line_item, order: order) } - let!(:user) { create_enterprise_user } - it "redirects to the OFN checkout page" do - controller.stub(:skip_state_validation?) { true } - controller.stub(:spree_current_user) { user } - spree_get :edit - response.should redirect_to checkout_path + expect(spree_get(:edit)).to redirect_to checkout_path end end end diff --git a/spec/controllers/spree/credit_cards_controller_spec.rb b/spec/controllers/spree/credit_cards_controller_spec.rb new file mode 100644 index 0000000000..0fc910f2d2 --- /dev/null +++ b/spec/controllers/spree/credit_cards_controller_spec.rb @@ -0,0 +1,131 @@ +require 'spec_helper' +require 'support/request/authentication_workflow' + +describe Spree::CreditCardsController do + include AuthenticationWorkflow + let(:user) { create_enterprise_user } + let(:token) { "tok_234bd2c22" } + + before do + allow(Stripe).to receive(:api_key) { "sk_test_12345" } + allow(controller).to receive(:spree_current_user) { user } + end + + describe "#new_from_token" do + let(:params) do + { + format: :json, + "exp_month" => 12, + "exp_year" => 2020, + "last4" => 4242, + "token" => token, + "cc_type" => "visa" + } + end + + before do + stub_request(:post, "https://api.stripe.com/v1/customers") + .with(:body => { email: user.email, source: token }) + .to_return(response_mock) + end + + context "when the request to store the customer/card with Stripe is successful" do + let(:response_mock) { { status: 200, body: JSON.generate(id: "cus_AZNMJ", default_source: "card_1AEEb") } } + + it "saves the card locally" do + expect{ post :new_from_token, params }.to change(Spree::CreditCard, :count).by(1) + + card = Spree::CreditCard.last + card.gateway_payment_profile_id.should eq "card_1AEEb" + card.gateway_customer_profile_id.should eq "cus_AZNMJ" + card.user_id.should eq user.id + card.last_digits.should eq "4242" + end + + context "when saving the card locally fails" do + before do + allow(controller).to receive(:stored_card_attributes) { {} } + end + + it "renders a flash error" do + expect{ post :new_from_token, params }.to_not change(Spree::CreditCard, :count) + + json_response = JSON.parse(response.body) + flash_message = I18n.t(:spree_gateway_error_flash_for_checkout, error: I18n.t(:card_could_not_be_saved)) + expect(json_response["flash"]["error"]).to eq flash_message + end + end + end + + context "when the request to store the customer/card with Stripe fails" do + let(:response_mock) { { status: 402, body: JSON.generate(error: { message: "Bup-bow..." }) } } + it "doesn't save the card locally, and renders a flash error" do + expect{ post :new_from_token, params }.to_not change(Spree::CreditCard, :count) + + json_response = JSON.parse(response.body) + flash_message = I18n.t(:spree_gateway_error_flash_for_checkout, error: "Bup-bow...") + expect(json_response["flash"]["error"]).to eq flash_message + end + end + end + + describe "#destroy" do + context "when the specified credit card is not found" do + let(:params) { { id: 123 } } + + it "redirects to /account with a flash error, does not request deletion with Stripe" do + expect(controller).to_not receive(:destroy_at_stripe) + delete :destroy, params + expect(flash[:error]).to eq I18n.t(:card_could_not_be_removed) + expect(response).to redirect_to spree.account_path(anchor: 'cards') + end + end + + context "when the specified credit card is found" do + let!(:card) { create(:credit_card, gateway_customer_profile_id: 'cus_AZNMJ') } + let(:params) { { id: card.id } } + + context "but the card is not owned by the user" do + it "redirects to unauthorized" do + delete :destroy, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "and the card is owned by the user" do + before do + card.update_attribute(:user_id, user.id) + + stub_request(:get, "https://api.stripe.com/v1/customers/cus_AZNMJ"). + to_return(:status => 200, :body => JSON.generate(id: "cus_AZNMJ")) + end + + context "where the request to destroy the Stripe customer fails" do + before do + stub_request(:delete, "https://api.stripe.com/v1/customers/cus_AZNMJ"). + to_return(:status => 402, :body => JSON.generate(error: { message: 'Bup-bow!' })) + end + + it "doesn't delete the card" do + expect{ delete :destroy, params }.to_not change(Spree::CreditCard, :count) + expect(flash[:error]).to eq I18n.t(:card_could_not_be_removed) + expect(response).to redirect_to spree.account_path(anchor: 'cards') + end + end + + context "where the request to destroy the Stripe customer succeeds" do + before do + stub_request(:delete, "https://api.stripe.com/v1/customers/cus_AZNMJ"). + to_return(:status => 200, :body => JSON.generate(deleted: true, id: "cus_AZNMJ")) + end + + it "deletes the card and redirects to account_path" do + expect{ delete :destroy, params }.to change(Spree::CreditCard, :count).by(-1) + expect(flash[:success]).to eq I18n.t(:card_has_been_removed, number: "x-#{card.last_digits}") + expect(response).to redirect_to spree.account_path(anchor: 'cards') + end + end + end + end + end +end diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index e7b5a7e12a..e3a625784c 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -350,11 +350,17 @@ describe Spree::OrdersController do end context "and the order is editable" do - let(:order_cycle) { create(:simple_order_cycle) } let(:distributor) { create(:enterprise, allow_order_changes: true) } + let(:order_cycle) do + create( + :simple_order_cycle, + distributors: [distributor], + variants: order.line_item_variants + ) + end before do - order.update_attributes(order_cycle_id: order_cycle.id, distributor_id: distributor.id) + order.update_attributes!(order_cycle_id: order_cycle.id, distributor_id: distributor.id) end it "returns the order" do @@ -392,6 +398,13 @@ describe Spree::OrdersController do context "when the order is complete" do let(:order) { create(:completed_order_with_totals, user: user) } + before do + Spree::MailMethod.create!( + environment: Rails.env, + preferred_mails_from: 'spree@example.com' + ) + end + it "responds with success" do spree_put :cancel, params expect(response.status).to redirect_to spree.order_path(order) diff --git a/spec/controllers/spree/paypal_controller_spec.rb b/spec/controllers/spree/paypal_controller_spec.rb index 200f4a9ff1..ff5f539e2d 100644 --- a/spec/controllers/spree/paypal_controller_spec.rb +++ b/spec/controllers/spree/paypal_controller_spec.rb @@ -2,9 +2,41 @@ require 'spec_helper' module Spree describe PaypalController do - it "should redirect back to checkout after cancel" do - spree_get :cancel - response.should redirect_to checkout_path + context 'when cancelling' do + it 'redirects back to checkout' do + expect(spree_get(:cancel)).to redirect_to checkout_path + end + end + + context 'when confirming' do + let(:previous_order) { controller.current_order(true) } + let(:payment_method) { create(:payment_method) } + + before do + allow(previous_order).to receive(:complete?).and_return(true) + end + + it 'resets the order' do + spree_post :confirm, payment_method_id: payment_method.id + expect(controller.current_order).not_to eq(previous_order) + end + + it 'sets the access token of the session' do + spree_post :confirm, payment_method_id: payment_method.id + expect(session[:access_token]).to eq(controller.current_order.token) + end + end + + describe '#expire_current_order' do + it 'empties the order_id of the session' do + expect(session).to receive(:[]=).with(:order_id, nil) + controller.expire_current_order + end + + it 'resets the @current_order ivar' do + controller.expire_current_order + expect(controller.instance_variable_get(:@current_order)).to be_nil + end end end end diff --git a/spec/controllers/spree/users_controller_spec.rb b/spec/controllers/spree/users_controller_spec.rb new file mode 100644 index 0000000000..f980e21498 --- /dev/null +++ b/spec/controllers/spree/users_controller_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Spree::UsersController do + include AuthenticationWorkflow + + describe "show" do + let!(:u1) { create(:user) } + let!(:u2) { create(:user) } + let!(:distributor1) { create(:distributor_enterprise) } + let!(:distributor2) { create(:distributor_enterprise) } + let!(:d1o1) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id) } + let!(:d1o2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id) } + let!(:d1_order_for_u2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u2.id) } + let!(:d1o3) { create(:order, state: 'cart', distributor: distributor1, user_id: u1.id) } + let!(:d2o1) { create(:completed_order_with_totals, distributor: distributor2, user_id: u2.id) } + let!(:accounts_distributor) { create :distributor_enterprise } + let!(:order_account_invoice) { create(:order, distributor: accounts_distributor, state: 'complete', user: u1) } + + let(:orders) { assigns(:orders) } + let(:shops) { Enterprise.where(id: orders.pluck(:distributor_id)) } + + before do + Spree::Config.set(accounts_distributor_id: accounts_distributor.id) + allow(controller).to receive(:spree_current_user) { u1 } + end + + it "returns orders placed by the user at normal shops" do + spree_get :show + + expect(orders).to eq [d1o1, d1o2] + expect(shops).to include distributor1 + + # Doesn't return orders belonging to the accounts distributor" do + expect(orders).to_not include order_account_invoice + expect(shops).to_not include accounts_distributor + + # Doesn't return orders for irrelevant distributors" do + expect(orders).not_to include d2o1 + expect(shops).not_to include distributor2 + + # Doesn't return other users' orders" do + expect(orders).not_to include d1_order_for_u2 + + # Doesn't return uncompleted orders" do + expect(orders).not_to include d1o3 + end + end +end diff --git a/spec/controllers/stripe/callbacks_controller_spec.rb b/spec/controllers/stripe/callbacks_controller_spec.rb new file mode 100644 index 0000000000..831dc5e348 --- /dev/null +++ b/spec/controllers/stripe/callbacks_controller_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe Stripe::CallbacksController do + let(:enterprise) { create(:distributor_enterprise) } + + context "#index" do + let(:params) { { id: enterprise.permalink } } + let(:connector) { double(:connector) } + + before do + allow(controller).to receive(:spree_current_user) { enterprise.owner } + allow(Stripe::AccountConnector).to receive(:new) { connector } + end + + context "when the connector.create_account raises a StripeError" do + before do + allow(connector).to receive(:create_account).and_raise Stripe::StripeError, "some error" + end + + it "returns a 500 error" do + spree_get :index, params + expect(response.status).to be 500 + end + end + + context "when the connector.create_account raises an AccessDenied error" do + before do + allow(connector).to receive(:create_account).and_raise CanCan::AccessDenied, "some error" + end + + it "redirects to unauthorized" do + spree_get :index, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "when the connector fails in creating a new stripe account record" do + before { allow(connector).to receive(:create_account) { false } } + + context "when the user cancelled the connection" do + before { allow(connector).to receive(:connection_cancelled_by_user?) { true } } + + it "renders a failure message" do + allow(connector).to receive(:enterprise) { enterprise } + spree_get :index, params + expect(flash[:notice]).to eq I18n.t('admin.controllers.enterprises.stripe_connect_cancelled') + expect(response).to redirect_to edit_admin_enterprise_path(enterprise, anchor: 'payment_methods') + end + end + + context "when some other error caused the failure" do + before { allow(connector).to receive(:connection_cancelled_by_user?) { false } } + + it "renders a failure message" do + allow(connector).to receive(:enterprise) { enterprise } + spree_get :index, params + expect(flash[:error]).to eq I18n.t('admin.controllers.enterprises.stripe_connect_fail') + expect(response).to redirect_to edit_admin_enterprise_path(enterprise, anchor: 'payment_methods') + end + end + end + + context "when the connector succeeds in creating a new stripe account record" do + before { allow(connector).to receive(:create_account) { true } } + + it "redirects to the enterprise edit path" do + allow(connector).to receive(:enterprise) { enterprise } + spree_get :index, params + expect(flash[:success]).to eq I18n.t('admin.controllers.enterprises.stripe_connect_success') + expect(response).to redirect_to edit_admin_enterprise_path(enterprise, anchor: 'payment_methods') + end + end + end +end diff --git a/spec/controllers/stripe/webhooks_controller_spec.rb b/spec/controllers/stripe/webhooks_controller_spec.rb new file mode 100644 index 0000000000..8c0bda6c87 --- /dev/null +++ b/spec/controllers/stripe/webhooks_controller_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +describe Stripe::WebhooksController do + describe "#create" do + let(:params) do + { + "format" => "json", + "id" => "evt_123", + "object" => "event", + "data" => { "object" => { "id" => "ca_9B" } }, + "type" => "account.application.authorized", + "account" => "webhook_id1" + } + end + + context "when invalid json is provided" do + before do + allow(Stripe::Webhook).to receive(:construct_event).and_raise JSON::ParserError, "parsing failed" + end + + it "responds with a 400" do + post 'create', params + expect(response.status).to eq 400 + end + end + + context "when event signature verification fails" do + before do + allow(Stripe::Webhook).to receive(:construct_event).and_raise Stripe::SignatureVerificationError.new("verfication failed", "header") + end + + it "responds with a 401" do + post 'create', params + expect(response.status).to eq 401 + end + end + + context "when event signature verification succeeds" do + before do + allow(Stripe::Webhook).to receive(:construct_event) { Stripe::Event.construct_from(params) } + end + + describe "setting the response status" do + let(:handler) { double(:handler) } + before { allow(Stripe::WebhookHandler).to receive(:new) { handler } } + + context "when an unknown result is returned by the handler" do + before { allow(handler).to receive(:handle) { :garbage } } + + it "falls back to 200" do + post 'create', params + expect(response.status).to eq 200 + end + end + + context "when the result returned by the handler is :unknown" do + before { allow(handler).to receive(:handle) { :unknown } } + + it "responds with 202" do + post 'create', params + expect(response.status).to eq 202 + end + end + end + + describe "when an account.application.deauthorized event is received" do + let!(:stripe_account) { create(:stripe_account, stripe_user_id: "webhook_id") } + before do + params["type"] = "account.application.deauthorized" + end + + context "when the stripe_account id on the event does not match any known accounts" do + it "doesn't delete any Stripe accounts, responds with 204" do + post 'create', params + expect(response.status).to eq 204 + expect(StripeAccount.all).to include stripe_account + end + end + + context "when the stripe_account id on the event matches a known account" do + before { params["account"] = "webhook_id" } + + it "deletes Stripe accounts in response to a webhook" do + post 'create', params + expect(response.status).to eq 200 + expect(StripeAccount.all).not_to include stripe_account + end + end + end + end + end +end diff --git a/spec/controllers/user_passwords_controller_spec.rb b/spec/controllers/user_passwords_controller_spec.rb index 59fadf8e99..26584a8619 100644 --- a/spec/controllers/user_passwords_controller_spec.rb +++ b/spec/controllers/user_passwords_controller_spec.rb @@ -6,7 +6,6 @@ describe UserPasswordsController do before do @request.env["devise.mapping"] = Devise.mappings[:spree_user] - ActionMailer::Base.default_url_options[:host] = "test.host" end describe "create" do diff --git a/spec/factories.rb b/spec/factories.rb index 3b2f67e531..14c97d0f55 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,5 +1,5 @@ require 'ffaker' -require 'spree/core/testing_support/factories' +require 'spree/testing_support/factories' # http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md # @@ -362,6 +362,17 @@ FactoryGirl.define do tr.calculator = Spree::Calculator::FlatPercentItemTotal.new(calculable: tr) end end + + factory :stripe_payment_method, :class => Spree::Gateway::StripeConnect do + name 'Stripe' + environment 'test' + end + + factory :stripe_account do + enterprise { FactoryGirl.create :distributor_enterprise } + stripe_user_id "abc123" + stripe_publishable_key "xyz456" + end end diff --git a/spec/features/admin/multilingual_spec.rb b/spec/features/admin/multilingual_spec.rb index bfad7c39a6..0f98e8bbde 100644 --- a/spec/features/admin/multilingual_spec.rb +++ b/spec/features/admin/multilingual_spec.rb @@ -3,9 +3,13 @@ require 'spec_helper' feature 'Multilingual', js: true do include AuthenticationWorkflow include WebHelper + let(:admin_role) { Spree::Role.find_or_create_by_name!('admin') } + let(:admin_user) { create(:user) } background do - login_to_admin_section + admin_user.spree_roles << admin_role + quick_login_as admin_user + visit spree.admin_path end it 'has two locales available' do @@ -18,11 +22,14 @@ feature 'Multilingual', js: true do expect(get_i18n_locale).to eq 'en' expect(get_i18n_translation('spree_admin_overview_enterprises_header')).to eq 'My Enterprises' expect(page).to have_content 'My Enterprises' + expect(admin_user.locale).to be_nil visit spree.admin_path(locale: 'es') expect(get_i18n_locale).to eq 'es' expect(get_i18n_translation('spree_admin_overview_enterprises_header')).to eq 'Mis Organizaciones' expect(page).to have_content 'Mis Organizaciones' + admin_user.reload + expect(admin_user.locale).to eq 'es' end it 'fallbacks to default_locale' do @@ -34,6 +41,7 @@ feature 'Multilingual', js: true do visit spree.admin_path(locale: 'it') expect(get_i18n_locale).to eq 'it' expect(get_i18n_translation('spree_admin_overview_enterprises_header')).to eq 'My Enterprises' + expect(admin_user.locale).to be_nil # This still is italian until we change enforce_available_locales to `true` expect(page).to have_content 'Le Mie Aziende' end diff --git a/spec/features/admin/orders_spec.rb b/spec/features/admin/orders_spec.rb index 5b0a215dd8..4b5df630f1 100644 --- a/spec/features/admin/orders_spec.rb +++ b/spec/features/admin/orders_spec.rb @@ -204,7 +204,6 @@ feature %q{ end context "viewing the edit page" do - before { Rails.application.routes.default_url_options[:host] = "test.host" } it "shows the dropdown menu" do distributor1.update_attribute(:abn, '12345678') order = create(:completed_order_with_totals, distributor: distributor1) diff --git a/spec/features/admin/overview_spec.rb b/spec/features/admin/overview_spec.rb index 30f2cc0336..31591349c7 100644 --- a/spec/features/admin/overview_spec.rb +++ b/spec/features/admin/overview_spec.rb @@ -5,8 +5,8 @@ feature %q{ I want to be given information about the state of my enterprises, products and order cycles }, js: true do include AuthenticationWorkflow - include AuthorizationHelpers include WebHelper + include ::Spree::TestingSupport::AuthorizationHelpers context "as an enterprise user" do before do diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index 45703a4d07..9df6ef0114 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -29,6 +29,44 @@ feature %q{ payment_method = Spree::PaymentMethod.find_by_name('Cheque payment method') payment_method.distributors.should == [@distributors[0]] end + + context "using stripe connect" do + let(:user) { create(:user, enterprise_limit: 5) } + let!(:connected_enterprise) { create(:distributor_enterprise, name: "Connected", owner: user) } + let!(:revoked_account_enterprise) { create(:distributor_enterprise, name: "Revoked", owner: user) } + let!(:missing_account_enterprise) { create(:distributor_enterprise, name: "Missing", owner: user) } + let!(:valid_stripe_account) { create(:stripe_account, enterprise: connected_enterprise, stripe_user_id: "acc_connected123") } + let!(:disconnected_stripe_account) { create(:stripe_account, enterprise: revoked_account_enterprise, stripe_user_id: "acc_revoked123") } + let!(:stripe_account_mock) { { id: "acc_connected123", business_name: "My Org", charges_enabled: true } } + + before do + Spree::Config.set(stripe_connect_enabled: true) + allow(Stripe).to receive(:api_key) { "sk_test_12345" } + stub_request(:get, "https://api.stripe.com/v1/accounts/acc_connected123").to_return(body: JSON.generate(stripe_account_mock)) + stub_request(:get, "https://api.stripe.com/v1/accounts/acc_revoked123").to_return(status: 404) + end + + it "communicates the status of the stripe connection to the user" do + login_as user + visit spree.new_admin_payment_method_path + + select2_select "Stripe", from: "payment_method_type" + + select2_select "Missing", from: "payment_method_preferred_enterprise_id" + expect(page).to have_selector "#stripe-account-status .alert-box.error", text: I18n.t("spree.admin.payment_methods.stripe_connect.account_missing_msg") + connect_one = I18n.t("spree.admin.payment_methods.stripe_connect.connect_one") + expect(page).to have_link connect_one, href: edit_admin_enterprise_path(missing_account_enterprise, anchor: "/payment_methods") + + select2_select "Revoked", from: "payment_method_preferred_enterprise_id" + expect(page).to have_selector "#stripe-account-status .alert-box.error", text: I18n.t("spree.admin.payment_methods.stripe_connect.access_revoked_msg") + + select2_select "Connected", from: "payment_method_preferred_enterprise_id" + expect(page).to have_selector "#stripe-account-status .status", text: "Status: Connected" + expect(page).to have_selector "#stripe-account-status .account_id", text: "Account ID: acc_connected123" + expect(page).to have_selector "#stripe-account-status .business_name", text: "Business Name: My Org" + expect(page).to have_selector "#stripe-account-status .charges_enabled", text: "Charges Enabled: Yes" + end + end end scenario "updating a payment method", js: true do diff --git a/spec/features/admin/product_import_spec.rb b/spec/features/admin/product_import_spec.rb index 82e1c6e91a..3ab66ca237 100644 --- a/spec/features/admin/product_import_spec.rb +++ b/spec/features/admin/product_import_spec.rb @@ -193,9 +193,10 @@ feature "Product Import", js: true do it "returns and error if nothing was uploaded" do visit main_app.admin_product_import_path + expect(page).to have_content 'Select a spreadsheet to upload' click_button 'Import' - expect(page).to have_content "File not found or could not be opened" + expect(flash_message).to eq I18n.t(:product_import_file_not_found_notice) end it "handles cases where no meaningful data can be read from the file" do diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index 2cebd58dce..eb6e765b00 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -86,24 +86,6 @@ feature %q{ variant = product.variants.first variant.on_demand.should be_true end - - scenario "making a product into a group buy product" do - product = create(:simple_product, name: 'group buy product') - - login_to_admin_section - - visit spree.edit_admin_product_path(product) - - choose 'product_group_buy_1' - fill_in 'Bulk unit size', :with => '10' - - click_button 'Update' - - flash_message.should == 'Product "group buy product" has been successfully updated!' - product.reload - product.group_buy.should be_true - product.group_buy_unit_size.should == 10.0 - end end context "as an enterprise user" do @@ -121,15 +103,6 @@ feature %q{ login_to_admin_as @new_user end - - context "additional fields" do - it "should have a notes field" do - product = create(:simple_product, supplier: @supplier2) - visit spree.edit_admin_product_path product - page.should have_content "Notes" - end - end - context "products do not require a tax category" do scenario "creating a new product", js: true do with_products_require_tax_category(false) do @@ -174,6 +147,22 @@ feature %q{ product.tax_category.should == tax_category end + scenario "editing product group buy options" do + product = product = create(:simple_product, supplier: @supplier2) + + visit spree.edit_admin_product_path product + within('#sidebar') { click_link 'Group Buy Options' } + choose('product_group_buy_1') + fill_in 'Bulk unit size', :with => '10' + + click_button 'Update' + + flash_message.should == "Product \"#{product.name}\" has been successfully updated!" + product.reload + product.group_buy.should be_true + product.group_buy_unit_size.should == 10.0 + end + scenario "editing product distributions" do product = create(:simple_product, supplier: @supplier2) @@ -195,6 +184,20 @@ feature %q{ product.distributors.should == [@distributors[0]] end + scenario "editing product SEO" do + product = product = create(:simple_product, supplier: @supplier2) + visit spree.edit_admin_product_path product + within('#sidebar') { click_link 'SEO' } + fill_in "product_meta_keywords", :with => 'Meta Keywords' + fill_in 'Meta Description', :with => 'Meta Description' + fill_in 'Notes', :with => 'Just testing Notes' + click_button 'Update' + flash_message.should == "Product \"#{product.name}\" has been successfully updated!" + product.reload + product.notes.should == 'Just testing Notes' + product.meta_keywords.should == 'Meta Keywords' + product.meta_description.should == 'Meta Description' + end scenario "deleting product properties", js: true do # Given a product with a property diff --git a/spec/features/admin/stripe_connect_spec.rb b/spec/features/admin/stripe_connect_spec.rb new file mode 100644 index 0000000000..1c56f703e9 --- /dev/null +++ b/spec/features/admin/stripe_connect_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +feature "Connecting a Stripe Account" do + include AuthenticationWorkflow + include WebHelper + + let!(:enterprise_user) { create :enterprise_user } + before(:each) do + login_to_admin_as enterprise_user + end +end diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 44fa258eb3..9064913868 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -66,12 +66,12 @@ feature %q{ context "with no overrides" do it "displays the list of products with variants" do - page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] - page.should have_table_row [producer.name, product.name, '', ''] + page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND', 'ON DEMAND?'] + page.should have_table_row [producer.name, product.name, '', '', ''] page.should have_input "variant-overrides-#{variant.id}-price", placeholder: '1.23' page.should have_input "variant-overrides-#{variant.id}-count_on_hand", placeholder: '12' - page.should have_table_row [producer_related.name, product_related.name, '', ''] + page.should have_table_row [producer_related.name, product_related.name, '', '', ''] page.should have_input "variant-overrides-#{variant_related.id}-price", placeholder: '2.34' page.should have_input "variant-overrides-#{variant_related.id}-count_on_hand", placeholder: '23' @@ -133,7 +133,6 @@ feature %q{ it "creates new overrides" do first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "SKU").click - first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click first("div#columns-dropdown", :text => "COLUMNS").click fill_in "variant-overrides-#{variant.id}-sku", with: 'NEWSKU' @@ -320,24 +319,24 @@ feature %q{ describe "when manually placing an order" do let!(:order_cycle) { create(:order_cycle_with_overrides, name: "Overidden") } + let(:distributor) { order_cycle.distributors.first } + let(:product) { order_cycle.products.first } before do - dist = order_cycle.distributors.first login_to_admin_section + visit 'admin/orders/new' - select2_select dist.name, from: 'order_distributor_id' - page.should have_select2 'order_order_cycle_id', with_options: ['Overidden (open)'] + select2_select distributor.name, from: 'order_distributor_id' select2_select order_cycle.name, from: 'order_order_cycle_id' click_button 'Next' end # Reproducing a bug, issue #1446 it "shows the overridden price" do - product = order_cycle.products.first targetted_select2_search product.name, from: '#add_variant_id', dropdown_css: '.select2-drop' click_link 'Add' - page.has_selector? "table.index tbody[data-hook='admin_order_form_line_items'] tr" # Wait for JS - page.should have_content product.variants.first.variant_overrides.first.price + expect(page).to have_selector("table.index tbody[data-hook='admin_order_form_line_items'] tr") # Wait for JS + expect(page).to have_content(product.variants.first.variant_overrides.first.price) end end diff --git a/spec/features/consumer/account/cards_spec.rb b/spec/features/consumer/account/cards_spec.rb new file mode 100644 index 0000000000..3a9e32ddd6 --- /dev/null +++ b/spec/features/consumer/account/cards_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +feature "Credit Cards", js: true do + include AuthenticationWorkflow + describe "as a logged in user" do + let(:user) { create(:user) } + let!(:card) { create(:credit_card, user_id: user.id, gateway_customer_profile_id: 'cus_AZNMJ') } + + before do + quick_login_as user + + allow(Stripe).to receive(:api_key) { "sk_test_xxxx" } + allow(Stripe).to receive(:publishable_key) { "some_token" } + Spree::Config.set(stripe_connect_enabled: true) + + stub_request(:get, "https://api.stripe.com/v1/customers/cus_AZNMJ"). + to_return(:status => 200, :body => JSON.generate(id: "cus_AZNMJ")) + + stub_request(:delete, "https://api.stripe.com/v1/customers/cus_AZNMJ"). + to_return(:status => 200, :body => JSON.generate(deleted: true, id: "cus_AZNMJ")) + end + + it "lists saved cards, shows interface for adding new cards" do + visit "/account" + + click_link I18n.t('spree.users.show.tabs.cards') + + expect(page).to have_content I18n.t(:saved_cards) + + within(".card#card#{card.id}") do + expect(page).to have_content card.cc_type.capitalize + expect(page).to have_content card.last_digits + end + + # Shows the interface for adding a card + click_button I18n.t(:add_a_card) + expect(page).to have_field 'first_name' + expect(page).to have_selector '#card-element.StripeElement' + + # Allows deletion of cards + click_link I18n.t(:delete) + + expect(page).to have_content I18n.t(:card_has_been_removed, number: "x-#{card.last_digits}") + expect(page).to have_content I18n.t(:you_have_no_saved_cards) + end + end +end diff --git a/spec/features/consumer/account/settings_spec.rb b/spec/features/consumer/account/settings_spec.rb new file mode 100644 index 0000000000..a7fbf9ce94 --- /dev/null +++ b/spec/features/consumer/account/settings_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +feature "Account Settings", js: true do + include AuthenticationWorkflow + + describe "as a logged in user" do + let(:user) { create(:user) } + + before do + quick_login_as user + end + + it "allows me to update my account details" do + visit "/account" + + click_link I18n.t('spree.users.show.tabs.settings') + expect(page).to have_content I18n.t('spree.users.form.account_settings') + fill_in 'user_email', with: 'new@email.com' + + click_button I18n.t(:update) + + expect(find(".alert-box.success").text.strip).to eq "#{I18n.t(:account_updated)} ×" + user.reload + expect(user.email).to eq 'new@email.com' + end + end +end diff --git a/spec/features/consumer/account_spec.rb b/spec/features/consumer/account_spec.rb index d5c7236b72..3e30bb4cd7 100644 --- a/spec/features/consumer/account_spec.rb +++ b/spec/features/consumer/account_spec.rb @@ -38,7 +38,21 @@ feature %q{ visit "/account" # No distributors allow changes to orders - expect(page).to_not have_content I18n.t('spree.users.show.open_orders') + expect(page).to_not have_content I18n.t('spree.users.orders.open_orders') + + expect(page).to have_content I18n.t('spree.users.orders.past_orders') + + # Doesn't show orders from the special Accounts & Billing distributor + expect(page).not_to have_content accounts_distributor.name + + # Lists all other orders + expect(page).to have_content d1o1.number.to_s + expect(page).to have_content d1o2.number.to_s + expect(page).to have_content d2o1.number.to_s + expect(page).to have_content credit_order.number.to_s + + # Viewing transaction history + click_link I18n.t('spree.users.show.tabs.transactions') # It shows all hubs that have been ordered from with balance or credit expect(page).to have_content distributor1.name @@ -65,7 +79,7 @@ feature %q{ it "shows such orders in a section labelled 'Open Orders'" do visit '/account' - expect(page).to have_content I18n.t('spree.users.show.open_orders') + expect(page).to have_content I18n.t('spree.users.orders.open_orders') expect(page).to have_link d1o1.number, href: spree.order_path(d1o1) expect(page).to have_link d1o2.number, href: spree.order_path(d1o2) diff --git a/spec/features/consumer/multilingual_spec.rb b/spec/features/consumer/multilingual_spec.rb index 59858875e2..4f163dee01 100644 --- a/spec/features/consumer/multilingual_spec.rb +++ b/spec/features/consumer/multilingual_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' feature 'Multilingual', js: true do + include AuthenticationWorkflow include WebHelper it 'has two locales available' do @@ -9,24 +10,62 @@ feature 'Multilingual', js: true do expect(Rails.application.config.i18n[:available_locales]).to eq ['en', 'es'] end + it '18n-js fallsback to default language' do # in backend it doesn't until we change enforce_available_locales to `true` + visit root_path + set_i18n_locale('it') + expect(get_i18n_translation('label_shops')).to eq 'Shops' + end + it 'can switch language by params' do visit root_path expect(get_i18n_locale).to eq 'en' expect(get_i18n_translation('label_shops')).to eq 'Shops' + expect(page.driver.browser.cookies['locale']).to be_nil expect(page).to have_content 'Interested in getting on the Open Food Network?' expect(page).to have_content 'SHOPS' visit root_path(locale: 'es') expect(get_i18n_locale).to eq 'es' expect(get_i18n_translation('label_shops')).to eq 'Tiendas' + expect(page.driver.browser.cookies['locale'].value).to eq 'es' expect(page).to have_content '¿Estás interesada en entrar en Open Food Network?' expect(page).to have_content 'TIENDAS' - # I18n-js fallsback to 'en' + # it is not in the list of available of available_locales visit root_path(locale: 'it') - expect(get_i18n_locale).to eq 'it' - expect(get_i18n_translation('label_shops')).to eq 'Shops' - # This still is italian until we change enforce_available_locales to `true` - expect(page).to have_content 'NEGOZI' + expect(get_i18n_locale).to eq 'es' + expect(get_i18n_translation('label_shops')).to eq 'Tiendas' + expect(page.driver.browser.cookies['locale'].value).to eq 'es' + expect(page).to have_content '¿Estás interesada en entrar en Open Food Network?' + expect(page).to have_content 'TIENDAS' + end + + context 'with user' do + let(:user) { create(:user) } + + it 'updates user locale from cookie if it is empty' do + visit root_path(locale: 'es') + + expect(page.driver.browser.cookies['locale'].value).to eq 'es' + expect(user.locale).to be_nil + quick_login_as user + visit root_path + + expect(page.driver.browser.cookies['locale'].value).to eq 'es' + end + + it 'updates user locale and stays in cookie after logout' do + quick_login_as user + visit root_path(locale: 'es') + user.reload + + expect(user.locale).to eq 'es' + + logout + + expect(page.driver.browser.cookies['locale'].value).to eq 'es' + expect(page).to have_content '¿Estás interesada en entrar en Open Food Network?' + expect(page).to have_content 'TIENDAS' + end end end diff --git a/spec/features/consumer/shopping/cart_spec.rb b/spec/features/consumer/shopping/cart_spec.rb index 99c0f75450..2b824ef8d7 100644 --- a/spec/features/consumer/shopping/cart_spec.rb +++ b/spec/features/consumer/shopping/cart_spec.rb @@ -20,6 +20,13 @@ feature "full-page cart", js: true do set_order order end + around do |example| + allow_backorders = Spree::Config.allow_backorders + Spree::Config.allow_backorders = false + example.run + Spree::Config.allow_backorders = allow_backorders + end + describe "fees" do let(:percentage_fee) { create(:enterprise_fee, calculator: Calculator::FlatPercentPerItem.new(preferred_flat_percent: 20)) } diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 220fdcb89b..0a55324b37 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -39,7 +39,6 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do end end - before do distributor.shipping_methods << sm1 distributor.shipping_methods << sm2 @@ -61,13 +60,11 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do page.should have_content "An item in your cart has become unavailable" end end + context 'login in as user' do let(:user) { create(:user) } - before do - quick_login_as(user) - visit checkout_path - + def fill_out_form toggle_shipping choose sm1.name toggle_payment @@ -94,28 +91,39 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do check "Save as default shipping address" end - it "sets user's default billing address and shipping address" do - user.bill_address.should be_nil - user.ship_address.should be_nil - - order.bill_address.should be_nil - order.ship_address.should be_nil - - place_order - page.should have_content "Your order has been processed successfully" - - order.reload.bill_address.address1.should eq '123 Your Head' - order.reload.ship_address.address1.should eq '123 Your Head' - - order.customer.bill_address.address1.should eq '123 Your Head' - order.customer.ship_address.address1.should eq '123 Your Head' - - user.reload.bill_address.address1.should eq '123 Your Head' - user.reload.ship_address.address1.should eq '123 Your Head' + before do + quick_login_as(user) end - it "it doesn't tell about previous orders" do - expect(page).to_not have_content("You have an order for this order cycle already.") + context "with details filled out" do + before do + visit checkout_path + fill_out_form + end + + it "sets user's default billing address and shipping address" do + user.bill_address.should be_nil + user.ship_address.should be_nil + + order.bill_address.should be_nil + order.ship_address.should be_nil + + place_order + page.should have_content "Your order has been processed successfully" + + order.reload.bill_address.address1.should eq '123 Your Head' + order.reload.ship_address.address1.should eq '123 Your Head' + + order.customer.bill_address.address1.should eq '123 Your Head' + order.customer.ship_address.address1.should eq '123 Your Head' + + user.reload.bill_address.address1.should eq '123 Your Head' + user.reload.ship_address.address1.should eq '123 Your Head' + end + + it "it doesn't tell about previous orders" do + expect(page).to_not have_content("You have an order for this order cycle already.") + end end context "with previous orders" do @@ -124,13 +132,64 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do before do order.distributor.allow_order_changes = true order.distributor.save + visit checkout_path end it "informs about previous orders" do - visit checkout_path expect(page).to have_content("You have an order for this order cycle already.") end end + + context "with Stripe" do + let!(:stripe_pm) do + create(:stripe_payment_method, + distributors: [distributor], + name: "Stripe", + preferred_enterprise_id: distributor.id) + end + + let!(:saved_card) do + create(:credit_card, + user_id: user.id, + month: "01", + year: "2025", + cc_type: "visa", + number: "1111111111111111", + payment_method_id: stripe_pm.id, + gateway_customer_profile_id: "i_am_saved") + end + + let!(:stripe_account) { create(:stripe_account, enterprise_id: distributor.id, stripe_user_id: 'some_id') } + + let(:response_mock) { { id: "ch_1234", object: "charge", amount: 2000} } + + before do + allow(Stripe).to receive(:api_key) { "sk_test_12345" } + allow(Stripe).to receive(:publishable_key) { "some_key" } + Spree::Config.set(stripe_connect_enabled: true) + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges") + .to_return(status: 200, body: JSON.generate(response_mock)) + + visit checkout_path + fill_out_form + toggle_payment + choose stripe_pm.name + end + + it "allows use of a saved card" do + # shows the saved credit card dropdown + expect(page).to have_content I18n.t("spree.checkout.payment.stripe.used_saved_card") + + # removes the input fields when a saved card is selected" + expect(page).to have_selector "#card-element.StripeElement" + select "Visa x-1111 Exp:01/2025", from: "selected_card" + expect(page).to_not have_selector "#card-element.StripeElement" + + # allows checkout + place_order + expect(page).to have_content "Your order has been processed successfully" + end + end end context "on the checkout page" do @@ -245,13 +304,16 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do describe "purchasing" do it "takes us to the order confirmation page when we submit a complete form" do toggle_details + within "#details" do fill_in "First Name", with: "Will" fill_in "Last Name", with: "Marshall" fill_in "Email", with: "test@test.com" fill_in "Phone", with: "0468363090" end + toggle_billing + within "#billing" do fill_in "Address", with: "123 Your Face" select "Australia", from: "Country" @@ -259,35 +321,40 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do fill_in "City", with: "Melbourne" fill_in "Postcode", with: "3066" end + toggle_shipping + within "#shipping" do choose sm2.name fill_in 'Any comments or special instructions?', with: "SpEcIaL NoTeS" end + toggle_payment + within "#payment" do choose pm1.name end expect do place_order - page.should have_content "Your order has been processed successfully" + expect(page).to have_content "Your order has been processed successfully" end.to enqueue_job ConfirmOrderJob # And the order's special instructions should be set - o = Spree::Order.complete.first - expect(o.special_instructions).to eq "SpEcIaL NoTeS" + order = Spree::Order.complete.first + expect(order.special_instructions).to eq "SpEcIaL NoTeS" # And the Spree tax summary should not be displayed - page.should_not have_content product.tax_category.name + expect(page).not_to have_content product.tax_category.name # And the total tax for the order, including shipping and fee tax, should be displayed # product tax ($10.00 @ 10% = $0.91) # + fee tax ($ 1.23 @ 10% = $0.11) # + shipping tax ($ 4.56 @ 25% = $0.91) # = $1.93 - page.should have_content "(includes tax)" - page.should have_content with_currency(1.93) + expect(page).to have_content '(includes tax)' + expect(page).to have_content with_currency(1.93) + expect(page).to have_content 'Back To Store' end context "with basic details filled" do @@ -396,7 +463,7 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do fill_in 'Security Code', with: '123' place_order - page.should have_content "Payment could not be processed, please check the details you entered" + page.should have_content 'Bogus Gateway: Forced failure' # Does not show duplicate shipping fee visit checkout_path diff --git a/spec/features/consumer/shopping/embedded_shopfronts_spec.rb b/spec/features/consumer/shopping/embedded_shopfronts_spec.rb index 4c88cd492d..4a4900e8c3 100644 --- a/spec/features/consumer/shopping/embedded_shopfronts_spec.rb +++ b/spec/features/consumer/shopping/embedded_shopfronts_spec.rb @@ -9,37 +9,6 @@ feature "Using embedded shopfront functionality", js: true do Capybara.server_port = 9999 - describe "enabling embedded shopfronts" do - before do - Spree::Config[:enable_embedded_shopfronts] = false - end - - it "disables iframes by default" do - visit shops_path - expect(page.response_headers['X-Frame-Options']).to eq 'DENY' - expect(page.response_headers['Content-Security-Policy']).to eq "frame-ancestors 'none'" - end - - it "allows iframes on certain pages when enabled in configuration" do - quick_login_as_admin - - visit spree.edit_admin_general_settings_path - - check 'enable_embedded_shopfronts' - fill_in 'embedded_shopfronts_whitelist', with: "test.com" - - click_button 'Update' - - visit shops_path - expect(page.response_headers['X-Frame-Options']).to be_nil - expect(page.response_headers['Content-Security-Policy']).to eq "frame-ancestors test.com" - - visit spree.admin_path - expect(page.response_headers['X-Frame-Options']).to eq 'DENY' - expect(page.response_headers['Content-Security-Policy']).to eq "frame-ancestors 'none'" - end - end - describe "using iframes" do let(:distributor) { create(:distributor_enterprise, name: 'My Embedded Hub', permalink: 'test_enterprise', with_payment_and_shipping: true) } let(:supplier) { create(:supplier_enterprise) } diff --git a/spec/features/consumer/shopping/orders_spec.rb b/spec/features/consumer/shopping/orders_spec.rb index 379f1b5197..66f2bcf4ce 100644 --- a/spec/features/consumer/shopping/orders_spec.rb +++ b/spec/features/consumer/shopping/orders_spec.rb @@ -36,6 +36,12 @@ feature "Order Management", js: true do end context "when the distributor allows changes to be made to orders" do + before do + Spree::MailMethod.create!( + environment: Rails.env, + preferred_mails_from: 'spree@example.com' + ) + end before do order.distributor.update_attributes(allow_order_changes: true) end diff --git a/spec/features/consumer/shops_spec.rb b/spec/features/consumer/shops_spec.rb index 1ed7cd3615..5e28f8ae50 100644 --- a/spec/features/consumer/shops_spec.rb +++ b/spec/features/consumer/shops_spec.rb @@ -6,6 +6,7 @@ feature 'Shops', js: true do let!(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } let!(:invisible_distributor) { create(:distributor_enterprise, visible: false) } + let!(:profile) { create(:distributor_enterprise, sells: 'none') } let!(:d1) { create(:distributor_enterprise, with_payment_and_shipping: true) } let!(:d2) { create(:distributor_enterprise, with_payment_and_shipping: true) } let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise)) } @@ -22,39 +23,54 @@ feature 'Shops', js: true do end - context "on the shops path" do + describe "listing shops" do before do visit shops_path end it "shows hubs" do - page.should have_content distributor.name + expect(page).to have_content distributor.name expand_active_table_node distributor.name - page.should have_content "OUR PRODUCERS" + expect(page).to have_content "OUR PRODUCERS" end it "does not show invisible hubs" do - page.should_not have_content invisible_distributor.name + expect(page).not_to have_content invisible_distributor.name end - it "should not show hubs that are not in an order cycle" do - create(:simple_product, distributors: [d1, d2]) - visit shops_path - page.should have_no_selector 'hub.inactive' - page.should have_no_selector 'hub', text: d2.name + it "does not show hubs that are not in an order cycle" do + expect(page).to have_no_selector 'hub.inactive' + expect(page).to have_no_selector 'hub', text: d2.name end - it "should show closed shops after clicking the button" do - create(:simple_product, distributors: [d1, d2]) - visit shops_path + it "does not show profiles" do + expect(page).not_to have_content profile.name + end + + it "shows closed shops after clicking the button" do click_link_and_ensure("Show closed shops", -> { page.has_selector? 'hub.inactive' }) - page.should have_selector 'hub.inactive', text: d2.name + expect(page).to have_selector 'hub.inactive', text: d2.name end - it "should link to the hub page" do + it "links to the hub page" do follow_active_table_node distributor.name expect(page).to have_current_path enterprise_shop_path(distributor) end + + describe "showing profiles" do + before do + check "Show profiles" + end + + it "still shows hubs" do + expect(page).to have_content distributor.name + end + + # https://github.com/openfoodfoundation/openfoodnetwork/issues/1718 + it "shows profiles" do + expect(page).to have_content profile.name + end + end end describe "showing available hubs" do @@ -66,8 +82,8 @@ feature 'Shops', js: true do it "does not show hubs that are not ready for checkout" do visit shops_path - Enterprise.ready_for_checkout.should_not include hub - page.should_not have_content hub.name + expect(Enterprise.ready_for_checkout).not_to include hub + expect(page).not_to have_content hub.name end end @@ -183,8 +199,7 @@ feature 'Shops', js: true do end it "shows closed shops" do - #click_link_and_ensure("Show closed shops", -> { page.has_selector? 'hub.inactive' }) - page.should have_selector 'hub.inactive', text: d2.name + expect(page).to have_selector 'hub.inactive', text: d2.name end end diff --git a/spec/helpers/enterprises_helper_spec.rb b/spec/helpers/enterprises_helper_spec.rb index 8e8fa3ed5e..f63cb49c0e 100644 --- a/spec/helpers/enterprises_helper_spec.rb +++ b/spec/helpers/enterprises_helper_spec.rb @@ -219,5 +219,37 @@ describe EnterprisesHelper do end end end + + context "when StripeConnect payment methods are present" do + let!(:pm3) { create(:stripe_payment_method, distributors: [distributor], preferred_enterprise_id: distributor.id) } + let!(:pm4) { create(:stripe_payment_method, distributors: [distributor], preferred_enterprise_id: some_other_distributor.id) } + let(:available_payment_methods) { helper.available_payment_methods } + + before do + allow(helper).to receive(:current_distributor) { distributor } + end + + context "and Stripe Connect is disabled" do + before { Spree::Config.set(stripe_connect_enabled: false) } + + it "ignores Stripe payment methods" do + expect(available_payment_methods).to_not include pm3, pm4 + end + end + + context "and Stripe Connect is enabled" do + let!(:stripe_account) { create(:stripe_account, enterprise_id: distributor.id) } + + before do + Spree::Config.set(stripe_connect_enabled: true) + allow(Stripe).to receive(:publishable_key) { "some_key" } + end + + it "includes Stripe payment methods with a valid stripe accounts" do + expect(available_payment_methods).to include pm3 + expect(available_payment_methods).to_not include pm4 + end + end + end end end diff --git a/spec/helpers/i18n_helper_spec.rb b/spec/helpers/i18n_helper_spec.rb new file mode 100644 index 0000000000..4e044b3ca2 --- /dev/null +++ b/spec/helpers/i18n_helper_spec.rb @@ -0,0 +1,145 @@ +require 'spec_helper' + +describe I18nHelper do + let(:user) { create(:user) } + + # In the real world, the helper is called in every request and sets + # I18n.locale to the chosen locale or the default. For testing purposes we + # have to restore I18n.locale for unit tests that don't call the helper, but + # rely on translated strings. + around do |example| + locale = I18n.locale + example.run + I18n.locale = locale + end + + context "as guest" do + before do + allow(helper).to receive(:spree_current_user) { nil } + end + + it "sets the default locale" do + helper.set_locale + expect(I18n.locale).to eq :en + end + + it "sets the chosen locale" do + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + expect(I18n.locale).to eq :es + end + + it "remembers the chosen locale" do + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + + allow(helper).to receive(:params) { {} } + helper.set_locale + expect(I18n.locale).to eq :es + end + + it "ignores unavailable locales" do + allow(helper).to receive(:params) { {locale: "xx"} } + helper.set_locale + expect(I18n.locale).to eq :en + end + + it "remembers the last chosen locale" do + allow(helper).to receive(:params) { {locale: "en"} } + helper.set_locale + + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + + allow(helper).to receive(:params) { {} } + helper.set_locale + expect(I18n.locale).to eq :es + end + + it "remembers the chosen locale after logging in" do + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + + # log in + allow(helper).to receive(:spree_current_user) { user } + allow(helper).to receive(:params) { {} } + helper.set_locale + expect(I18n.locale).to eq :es + end + + it "forgets the chosen locale without cookies" do + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + + # clean up cookies + cookies.delete :locale + + allow(helper).to receive(:params) { {} } + helper.set_locale + expect(I18n.locale).to eq :en + end + end + + context "logged in" do + before do + allow(helper).to receive(:spree_current_user) { user } + end + + it "sets the default locale" do + helper.set_locale + expect(I18n.locale).to eq :en + end + + it "sets the chosen locale" do + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + expect(I18n.locale).to eq :es + expect(user.locale).to eq "es" + end + + it "remembers the chosen locale" do + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + + allow(helper).to receive(:params) { {} } + helper.set_locale + expect(I18n.locale).to eq :es + end + + it "remembers the last chosen locale" do + allow(helper).to receive(:params) { {locale: "en"} } + helper.set_locale + + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + + allow(helper).to receive(:params) { {} } + helper.set_locale + expect(I18n.locale).to eq :es + end + + it "remembers the chosen locale after logging out" do + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + + # log out + allow(helper).to receive(:spree_current_user) { nil } + allow(helper).to receive(:params) { {} } + helper.set_locale + expect(I18n.locale).to eq :es + end + + it "remembers the chosen locale on another computer" do + allow(helper).to receive(:params) { {locale: "es"} } + helper.set_locale + expect(cookies[:locale]).to eq "es" + + # switch computer / browser or loose cookies + cookies.delete :locale + + allow(helper).to receive(:params) { {} } + helper.set_locale + expect(I18n.locale).to eq :es + end + end +end diff --git a/spec/helpers/injection_helper_spec.rb b/spec/helpers/injection_helper_spec.rb index acec197b20..1cdeca6a3a 100644 --- a/spec/helpers/injection_helper_spec.rb +++ b/spec/helpers/injection_helper_spec.rb @@ -54,4 +54,12 @@ describe InjectionHelper do helper.inject_taxons.should match taxon.name end + it "only injects credit cards with a payment profile" do + allow(helper).to receive(:spree_current_user) { user } + card1 = create(:credit_card, last_digits: "1234", user_id: user.id, gateway_customer_profile_id: 'cust_123') + card2 = create(:credit_card, last_digits: "4321", user_id: user.id, gateway_customer_profile_id: nil) + injected_cards = helper.inject_saved_credit_cards + expect(injected_cards).to match "1234" + expect(injected_cards).to_not match "4321" + end end diff --git a/spec/helpers/spree/admin/base_helper_spec.rb b/spec/helpers/spree/admin/base_helper_spec.rb new file mode 100644 index 0000000000..a47b04113f --- /dev/null +++ b/spec/helpers/spree/admin/base_helper_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Spree::BaseHelper, type: :helper do + describe "#link_to_remove_fields" do + let(:name) { 'Hola' } + let(:form) { double('form_for', hidden_field: '') } + let(:options) { {} } + + subject { helper.link_to_remove_fields(name, form, options) } + + it 'returns an `a` tag followed by a hidden `input` tag' do + expect(subject).to eq("Hola<input type="hidden" name="_method" value="destroy">") + end + end +end diff --git a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee index c4a724a137..c168047ccf 100644 --- a/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/enterprises/controllers/side_menu_controller_spec.js.coffee @@ -17,6 +17,7 @@ describe "menuCtrl", -> inject ($rootScope, $controller, _SideMenu_) -> scope = $rootScope SideMenu = _SideMenu_ + spyOn(SideMenu, "init").and.callThrough() spyOn(SideMenu, "select").and.callThrough() spyOn(SideMenu, "setItems").and.callThrough() ctrl = $controller 'sideMenuCtrl', {$scope: scope, enterprise: enterprise, SideMenu: SideMenu, enterprisePermissions: {}} @@ -30,7 +31,7 @@ describe "menuCtrl", -> expect(scope.menu.items).toBe SideMenu.items it "sets the initally selected value", -> - expect(SideMenu.select).toHaveBeenCalledWith 0 + expect(SideMenu.init).toHaveBeenCalled() describe "selecting an item", -> diff --git a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee index 23c42efde0..16a70c8abd 100644 --- a/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/line_items/controllers/line_items_controller_spec.js.coffee @@ -34,7 +34,7 @@ describe "LineItemsCtrl", -> lineItem = { id: 7, quantity: 3, order: { id: 9 }, supplier: { id: 1 } } httpBackend.expectGET("/admin/orders.json?q%5Bcompleted_at_gt%5D=SomeDate&q%5Bcompleted_at_lt%5D=SomeDate&q%5Bcompleted_at_not_null%5D=true&q%5Bstate_not_eq%5D=canceled").respond [order] - httpBackend.expectGET("/admin/line_items.json?q%5Border%5D%5Bcompleted_at_gt%5D=SomeDate&q%5Border%5D%5Bcompleted_at_lt%5D=SomeDate&q%5Border%5D%5Bcompleted_at_not_null%5D=true&q%5Border%5D%5Bstate_not_eq%5D=canceled").respond [lineItem] + httpBackend.expectGET("/admin/bulk_line_items.json?q%5Border%5D%5Bcompleted_at_gt%5D=SomeDate&q%5Border%5D%5Bcompleted_at_lt%5D=SomeDate&q%5Border%5D%5Bcompleted_at_not_null%5D=true&q%5Border%5D%5Bstate_not_eq%5D=canceled").respond [lineItem] httpBackend.expectGET("/admin/enterprises/for_line_items.json?ams_prefix=basic&q%5Bsells_in%5D%5B%5D=own&q%5Bsells_in%5D%5B%5D=any").respond [distributor] httpBackend.expectGET("/admin/order_cycles.json?ams_prefix=basic&as=distributor&q%5Borders_close_at_gt%5D=SomeDate").respond [orderCycle] httpBackend.expectGET("/admin/enterprises/for_line_items.json?ams_prefix=basic&q%5Bis_primary_producer_eq%5D=true").respond [supplier] @@ -106,7 +106,7 @@ describe "LineItemsCtrl", -> describe "where the request is successful", -> beforeEach -> - httpBackend.expectDELETE("/admin/orders/R12345678/line_items/1.json").respond "nothing" + httpBackend.expectDELETE("/admin/bulk_line_items/1.json").respond "nothing" scope.deleteLineItem line_item1 httpBackend.flush() @@ -115,7 +115,7 @@ describe "LineItemsCtrl", -> describe "where the request is unsuccessful", -> beforeEach -> - httpBackend.expectDELETE("/admin/orders/R12345678/line_items/1.json").respond 404, "NO CONTENT" + httpBackend.expectDELETE("/admin/bulk_line_items/1.json").respond 404, "NO CONTENT" scope.deleteLineItem line_item1 httpBackend.flush() diff --git a/spec/javascripts/unit/admin/line_items/services/line_items_spec.js.coffee b/spec/javascripts/unit/admin/line_items/services/line_items_spec.js.coffee index 3de04003e5..7367e06d88 100644 --- a/spec/javascripts/unit/admin/line_items/services/line_items_spec.js.coffee +++ b/spec/javascripts/unit/admin/line_items/services/line_items_spec.js.coffee @@ -19,7 +19,7 @@ describe "LineItems service", -> beforeEach -> response = [{ id: 5, name: 'LineItem 1'}] - $httpBackend.expectGET('/admin/line_items.json').respond 200, response + $httpBackend.expectGET('/admin/bulk_line_items.json').respond 200, response result = LineItems.index() $httpBackend.flush() @@ -41,7 +41,7 @@ describe "LineItems service", -> beforeEach -> lineItem = new LineItemResource({ id: 15, order: { number: '12345678'} }) - $httpBackend.expectPUT('/admin/orders/12345678/line_items/15.json').respond 200, { id: 15, name: 'LineItem 1'} + $httpBackend.expectPUT('/admin/bulk_line_items/15.json').respond 200, { id: 15, name: 'LineItem 1'} LineItems.save(lineItem).then( -> resolved = true) $httpBackend.flush() @@ -60,7 +60,7 @@ describe "LineItems service", -> beforeEach -> lineItem = new LineItemResource( { id: 15, order: { number: '12345678'} } ) - $httpBackend.expectPUT('/admin/orders/12345678/line_items/15.json').respond 422, { error: 'obj' } + $httpBackend.expectPUT('/admin/bulk_line_items/15.json').respond 422, { error: 'obj' } LineItems.save(lineItem).catch( -> rejected = true) $httpBackend.flush() @@ -115,7 +115,7 @@ describe "LineItems service", -> lineItem = new LineItemResource({ id: 15, order: { number: '12345678'} }) LineItems.pristineByID[15] = lineItem LineItems.byID[15] = lineItem - $httpBackend.expectDELETE('/admin/orders/12345678/line_items/15.json').respond 200, { id: 15, name: 'LineItem 1'} + $httpBackend.expectDELETE('/admin/bulk_line_items/15.json').respond 200, { id: 15, name: 'LineItem 1'} LineItems.delete(lineItem, callback).then( -> resolved = true).catch( -> rejected = true) $httpBackend.flush() @@ -140,7 +140,7 @@ describe "LineItems service", -> lineItem = new LineItemResource({ id: 15, order: { number: '12345678'} }) LineItems.pristineByID[15] = lineItem LineItems.byID[15] = lineItem - $httpBackend.expectDELETE('/admin/orders/12345678/line_items/15.json').respond 422, { error: 'obj' } + $httpBackend.expectDELETE('/admin/bulk_line_items/15.json').respond 422, { error: 'obj' } LineItems.delete(lineItem, callback).then( -> resolved = true).catch( -> rejected = true) $httpBackend.flush() diff --git a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee index 7d4c5d1d7c..fc46c2a30d 100644 --- a/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/bulk_products_spec.js.coffee @@ -38,18 +38,14 @@ describe "BulkProducts service", -> describe "cloning products", -> - it "clones products using a http get request to /admin/products/(permalink)/clone.json", -> + it "clones products using a http post request to /api/products/(id)/clone", -> BulkProducts.products = [ id: 13 - permalink_live: "oranges" ] - $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, - product: - id: 17 - name: "new_product" + $httpBackend.expectPOST("/api/products/13/clone").respond 201, + id: 17 $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, [ id: 17 - name: "new_product" ] BulkProducts.cloneProduct BulkProducts.products[0] $httpBackend.flush() @@ -57,15 +53,13 @@ describe "BulkProducts service", -> it "adds the product", -> originalProduct = id: 16 - permalink_live: "oranges" clonedProduct = id: 17 spyOn(BulkProducts, "insertProductAfter") spyOn(BulkProducts, "unpackProduct") BulkProducts.products = [originalProduct] - $httpBackend.expectGET("/admin/products/oranges/clone.json").respond 200, - product: clonedProduct + $httpBackend.expectPOST("/api/products/16/clone").respond 201, clonedProduct $httpBackend.expectGET("/api/products/17?template=bulk_show").respond 200, clonedProduct BulkProducts.cloneProduct BulkProducts.products[0] $httpBackend.flush() diff --git a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee index f3ad508c4d..75d6fd8d36 100644 --- a/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/controllers/checkout/checkout_controller_spec.js.coffee @@ -16,6 +16,7 @@ describe "CheckoutCtrl", -> $provide.value "CurrentHub", CurrentHubMock null Checkout = + purchase: -> submit: -> navigate: -> bindFieldsToLocalStorage: -> @@ -42,17 +43,17 @@ describe "CheckoutCtrl", -> preventDefault: -> beforeEach -> - spyOn(Checkout, "submit") + spyOn(Checkout, "purchase") scope.submitted = false it "delegates to the service when valid", -> scope.purchase(event, {$valid: true}) - expect(Checkout.submit).toHaveBeenCalled() + expect(Checkout.purchase).toHaveBeenCalled() expect(scope.submitted).toBe(true) it "does nothing when invalid", -> scope.purchase(event, {$valid: false}) - expect(Checkout.submit).not.toHaveBeenCalled() + expect(Checkout.purchase).not.toHaveBeenCalled() expect(scope.submitted).toBe(true) it "is enabled", -> diff --git a/spec/javascripts/unit/darkswarm/filters/closed_shops_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/closed_shops_spec.js.coffee new file mode 100644 index 0000000000..8580ba7f3d --- /dev/null +++ b/spec/javascripts/unit/darkswarm/filters/closed_shops_spec.js.coffee @@ -0,0 +1,30 @@ +describe "filtering closed shops", -> + enterprises = [{ + name: "open shop" + active: true + is_distributor: true + }, { + name: "closed shop" + active: false + is_distributor: true + }, { + name: "profile" + active: false + is_distributor: false + }, { + name: "errornous entry" + does_not_have: "required attributes" + } + ] + closedShops = null + + beforeEach -> + module 'Darkswarm' + inject ($filter) -> + closedShops = $filter('closedShops') + + it "filters closed shops, but ignores profiles and invalid entries", -> + expect(closedShops(enterprises, false)).toEqual [enterprises[0], enterprises[2], enterprises[3]] + + it "does not filter closed shops", -> + expect(closedShops(enterprises, true)).toEqual enterprises diff --git a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee index 78fd10f054..5ca05a344f 100644 --- a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee @@ -15,6 +15,11 @@ describe 'Checkout service', -> id: 123 test: "bar" method_type: "check" + }, + { + id: 666 + test: "qux" + method_type: "stripe" }] shippingMethods = [ { @@ -46,6 +51,7 @@ describe 'Checkout service', -> $provide.value "currentOrder", orderData $provide.value "shippingMethods", shippingMethods $provide.value "paymentMethods", paymentMethods + $provide.value "StripeInstancePublishableKey", "instance_publishable_key" null inject ($injector, _$httpBackend_, $rootScope)-> @@ -83,31 +89,47 @@ describe 'Checkout service', -> Checkout.order.payment_method_id = 99 expect(Checkout.paymentMethod()).toEqual paymentMethods[0] - it "Posts the Checkout to the server", -> - $httpBackend.expectPUT("/checkout", {order: Checkout.preprocess()}).respond 200, {path: "test"} - Checkout.submit() - $httpBackend.flush() - - describe "when there is an error", -> - it "redirects when a redirect is given", -> - $httpBackend.expectPUT("/checkout").respond 400, {path: 'path'} + describe "submitting", -> + it "Posts the Checkout to the server", -> + $httpBackend.expectPUT("/checkout.json", {order: Checkout.preprocess()}).respond 200, {path: "test"} Checkout.submit() $httpBackend.flush() - expect(Navigation.go).toHaveBeenCalledWith 'path' - it "sends flash messages to the flash service", -> - spyOn(FlashLoaderMock, "loadFlash") # Stubbing out writes to window.location - $httpBackend.expectPUT("/checkout").respond 400, {flash: {error: "frogs"}} - Checkout.submit() + describe "when there is an error", -> + it "redirects when a redirect is given", -> + $httpBackend.expectPUT("/checkout.json").respond 400, {path: 'path'} + Checkout.submit() + $httpBackend.flush() + expect(Navigation.go).toHaveBeenCalledWith 'path' - $httpBackend.flush() - expect(FlashLoaderMock.loadFlash).toHaveBeenCalledWith {error: "frogs"} + it "sends flash messages to the flash service", -> + spyOn(FlashLoaderMock, "loadFlash") # Stubbing out writes to window.location + $httpBackend.expectPUT("/checkout.json").respond 400, {flash: {error: "frogs"}} + Checkout.submit() - it "puts errors into the scope", -> - $httpBackend.expectPUT("/checkout").respond 400, {errors: {error: "frogs"}} - Checkout.submit() - $httpBackend.flush() - expect(Checkout.errors).toEqual {error: "frogs"} + $httpBackend.flush() + expect(FlashLoaderMock.loadFlash).toHaveBeenCalledWith {error: "frogs"} + + it "puts errors into the scope", -> + $httpBackend.expectPUT("/checkout.json").respond 400, {errors: {error: "frogs"}} + Checkout.submit() + $httpBackend.flush() + expect(Checkout.errors).toEqual {error: "frogs"} + + describe "when using the Stripe Connect gateway", -> + beforeEach inject ($injector, StripeElements) -> + Checkout.order.payment_method_id = 666 + + it "requests a Stripe token before submitting", inject (StripeElements) -> + spyOn(StripeElements, "requestToken") + Checkout.purchase() + expect(StripeElements.requestToken).toHaveBeenCalled() + + it "doesn't hit Stripe when reusing a credit card", inject (StripeElements) -> + spyOn(StripeElements, "requestToken") + Checkout.secrets.selected_card = 1 + Checkout.purchase() + expect(StripeElements.requestToken).not.toHaveBeenCalled() describe "data preprocessing", -> beforeEach -> @@ -155,3 +177,35 @@ describe 'Checkout service', -> Checkout.order.payment_method_id = 123 source_attributes = Checkout.preprocess().payments_attributes[0].source_attributes expect(source_attributes).not.toBeDefined() + + describe "when the payment method is the Stripe Connect gateway", -> + beforeEach -> + Checkout.order.payment_method_id = 666 + Checkout.secrets = + token: "stripe_token" + cc_type: "mastercard" + card: + last4: "1234" + exp_year: "2099" + exp_month: "10" + + it "creates source attributes for the submitted card", -> + source_attributes = Checkout.preprocess().payments_attributes[0].source_attributes + expect(source_attributes).toBeDefined() + expect(source_attributes.gateway_payment_profile_id).toBe "stripe_token" + expect(source_attributes.cc_type).toBe "mastercard" + expect(source_attributes.last_digits).toBe "1234" + expect(source_attributes.year).toBe "2099" + expect(source_attributes.month).toBe "10" + expect(source_attributes.first_name).toBe orderData.bill_address.firstname + expect(source_attributes.last_name).toBe orderData.bill_address.lastname + + describe "when a saved card from Stripe is used", -> + beforeEach -> + Checkout.order.payment_method_id = 666 + + it "passes the card ID in source attributes if a saved card is selected", -> + Checkout.secrets.selected_card = 1 + source_attributes = Checkout.preprocess() + expect(source_attributes).toBeDefined() + expect(source_attributes.existing_card_id).toBe 1 diff --git a/spec/javascripts/unit/darkswarm/services/credit_card_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/credit_card_spec.js.coffee new file mode 100644 index 0000000000..f6b6993859 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/credit_card_spec.js.coffee @@ -0,0 +1,31 @@ +describe 'CreditCard service', -> + CreditCard = null + + beforeEach -> + module 'Darkswarm' + module ($provide)-> + $provide.value "savedCreditCards", [] + $provide.value "railsFlash", null + null + + inject (_CreditCard_)-> + CreditCard = _CreditCard_ + + describe "process_params", -> + beforeEach -> + CreditCard.secrets = + card: + exp_month: "12" + exp_year: "2030" + last4: "1234" + cc_type: 'mastercard' + token: "token123" + + it "uses cc_type, rather than fetching the brand from the card", -> + # This is important for processing the card with activemerchant + process_params = CreditCard.process_params() + expect(process_params['exp_month']).toEqual "12" + expect(process_params['exp_year']).toEqual "2030" + expect(process_params['last4']).toEqual "1234" + expect(process_params['token']).toEqual "token123" + expect(process_params['cc_type']).toEqual "mastercard" diff --git a/spec/javascripts/unit/darkswarm/services/stripe_elements_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/stripe_elements_spec.js.coffee new file mode 100644 index 0000000000..0252298f82 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/services/stripe_elements_spec.js.coffee @@ -0,0 +1,62 @@ +describe 'StripeElements Service', -> + $httpBackend = $q = $rootScope = StripeElements = null + StripeMock = { createToken: null } + CardMock = { some: "card" } + + beforeEach -> + module 'Darkswarm' + module ($provide) -> + $provide.value "railsFlash", null + null + + inject (_StripeElements_, _$httpBackend_, _$q_, _$rootScope_) -> + $httpBackend = _$httpBackend_ + StripeElements = _StripeElements_ + $q = _$q_ + $rootScope = _$rootScope_ + + describe "requestToken", -> + secrets = {} + submit = null + response = null + + beforeEach inject ($window) -> + StripeElements.stripe = StripeMock + StripeElements.card = CardMock + + describe "with satifactory data", -> + beforeEach -> + submit = jasmine.createSpy() + response = { token: { id: "token", card: { brand: 'MasterCard', last4: "5678", exp_month: 10, exp_year: 2099 } } } + StripeMock.createToken = => $q.when(response) + + it "saves the response data to secrets, and submits the form", -> + StripeElements.requestToken(secrets, submit) + $rootScope.$digest() # required for #then to by called + expect(secrets.token).toEqual "token" + expect(secrets.cc_type).toEqual "master" + expect(submit).toHaveBeenCalled() + + describe "with unsatifactory data", -> + beforeEach -> + submit = jasmine.createSpy() + response = { token: {id: "token" }, error: { message: 'There was a problem' } } + StripeMock.createToken = => $q.when(response) + + it "doesn't submit the form, shows an error message instead", inject (Loading, RailsFlashLoader) -> + spyOn(Loading, "clear") + spyOn(RailsFlashLoader, "loadFlash") + StripeElements.requestToken(secrets, submit) + $rootScope.$digest() # required for #then to by called + expect(submit).not.toHaveBeenCalled() + expect(Loading.clear).toHaveBeenCalled() + expect(RailsFlashLoader.loadFlash).toHaveBeenCalledWith({error: "Error: There was a problem"}) + + describe 'mapCC', -> + it "maps the brand returned by Stripe to that required by activemerchant", -> + expect(StripeElements.mapCC('MasterCard')).toEqual "master" + expect(StripeElements.mapCC('Visa')).toEqual "visa" + expect(StripeElements.mapCC('American Express')).toEqual "american_express" + expect(StripeElements.mapCC('Discover')).toEqual "discover" + expect(StripeElements.mapCC('JCB')).toEqual "jcb" + expect(StripeElements.mapCC('Diners Club')).toEqual "diners_club" diff --git a/spec/lib/stripe/account_connector_spec.rb b/spec/lib/stripe/account_connector_spec.rb new file mode 100644 index 0000000000..1b5721ba33 --- /dev/null +++ b/spec/lib/stripe/account_connector_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' +require 'stripe/account_connector' +require 'stripe/oauth' + +module Stripe + describe AccountConnector do + describe "create_account" do + let(:user) { create(:user) } + let(:enterprise) { create(:enterprise) } + let(:payload) { { "junk" => "Ssfs" } } + let(:state) { JWT.encode(payload, Openfoodnetwork::Application.config.secret_token) } + let(:params) { { "state" => state } } + let(:connector) { AccountConnector.new(user, params) } + + before do + allow(Stripe).to receive(:api_key) { "sk_test_12345" } + end + + context "when the connection was cancelled by the user" do + before do + params[:action] = "connect_callback" + params[:error] = "access_denied" + end + + it "returns false and does not create a new StripeAccount" do + expect do + expect(connector.create_account).to be false + end.to_not change(StripeAccount, :count) + end + end + + context "when the connection was not cancelled by the user" do + context "when params have no 'code' key" do + it "raises a StripeError" do + expect do + expect{ connector.create_account }.to raise_error StripeError + end.to_not change(StripeAccount, :count) + end + end + + context "when params have a 'code' key" do + before { params["code"] = 'code' } + + context "and the decoded state param doesn't contain an 'enterprise_id' key" do + it "raises an AccessDenied error" do + expect do + expect{ connector.create_account }.to raise_error CanCan::AccessDenied + end.to_not change(StripeAccount, :count) + end + end + + context "and the decoded state param contains an 'enterprise_id' key" do + let(:payload) { { enterprise_id: enterprise.permalink } } + let(:token_response) { { "stripe_user_id" => "some_user_id", "stripe_publishable_key" => "some_key" } } + + before do + stub_request(:post, "https://connect.stripe.com/oauth/token"). + with(body: {"code"=>"code", "grant_type"=>"authorization_code"}). + to_return(status: 200, body: JSON.generate(token_response) ) + end + + context "but the user doesn't manage own or manage the corresponding enterprise" do + it "makes a request to cancel the Stripe connection and raises an error" do + expect(OAuth).to receive(:deauthorize).with(stripe_user_id: "some_user_id") + expect do + expect{ connector.create_account }.to raise_error CanCan::AccessDenied + end.to_not change(StripeAccount, :count) + end + end + + context "and the user manages the corresponding enterprise" do + before do + user.enterprise_roles.create(enterprise: enterprise) + end + + it "raises no errors" do + expect(OAuth).to_not receive(:deauthorize) + connector.create_account + end + + it "allows creations of a new Stripe Account from the callback params" do + expect{ connector.create_account }.to change(StripeAccount, :count).by(1) + account = StripeAccount.last + expect(account.stripe_user_id).to eq "some_user_id" + expect(account.stripe_publishable_key).to eq "some_key" + end + end + + context "and the user owns the corresponding enterprise" do + let(:user) { enterprise.owner } + + it "raises no errors" do + expect(OAuth).to_not receive(:deauthorize) + connector.create_account + end + + it "allows creations of a new Stripe Account from the callback params" do + expect{ connector.create_account }.to change(StripeAccount, :count).by(1) + account = StripeAccount.last + expect(account.stripe_user_id).to eq "some_user_id" + expect(account.stripe_publishable_key).to eq "some_key" + end + end + end + end + end + end + end +end diff --git a/spec/lib/stripe/webhook_handler_spec.rb b/spec/lib/stripe/webhook_handler_spec.rb new file mode 100644 index 0000000000..e83cf01805 --- /dev/null +++ b/spec/lib/stripe/webhook_handler_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' +require 'stripe/webhook_handler' + +module Stripe + describe WebhookHandler do + let(:event) { double(:event, type: 'some.event') } + let(:handler) { WebhookHandler.new(event) } + + describe "event_mappings" do + it { expect(handler.send(:event_mappings)).to be_a Hash } + end + + describe "known_event?" do + context "when event mappings know about the event type" do + before do + allow(handler).to receive(:event_mappings) { { 'some.event' => :something } } + end + + it { expect(handler.send(:known_event?)).to be true } + end + + context "when event mappings do not know about the event type" do + before do + allow(handler).to receive(:event_mappings) { { 'some.other.event' => :something } } + end + + it { expect(handler.send(:known_event?)).to be false } + end + end + + describe "handle" do + context "when the event is known" do + before do + allow(handler).to receive(:event_mappings) { { 'some.event' => :some_method } } + end + + it "calls the handler method, and returns the result" do + expect(handler).to receive(:some_method) { :result } + expect(handler.handle).to eq :result + end + end + + context "when the event is unknown" do + before do + allow(handler).to receive(:event_mappings) { { 'some.other.event' => :some_method } } + end + + it "does not call the handler method, and returns :unknown" do + expect(handler).to_not receive(:some_method) + expect(handler.handle).to be :unknown + end + end + end + + describe "deauthorize" do + context "when the event has no 'account' attribute" do + it "does destroy stripe accounts, returns :ignored" do + expect(handler).to_not receive(:destroy_stripe_accounts_linked_to) + expect(handler.send(:deauthorize)).to be :ignored + end + end + + context "when the event has an 'account' attribute" do + before do + allow(event).to receive(:account) { 'some.account' } + end + + context "when some stripe accounts are destroyed" do + before do + allow(handler).to receive(:destroy_stripe_accounts_linked_to).with('some.account') { [double(:destroyed_stripe_account)] } + end + + it { expect(handler.send(:deauthorize)).to be :success } + end + + context "when no stripe accounts are destroyed" do + before do + allow(handler).to receive(:destroy_stripe_accounts_linked_to).with('some.account') { [] } + end + + it { expect(handler.send(:deauthorize)).to be :ignored } + end + end + end + end +end diff --git a/spec/mailers/order_mailer_spec.rb b/spec/mailers/order_mailer_spec.rb index 6e88742fe7..18e864f7cd 100644 --- a/spec/mailers/order_mailer_spec.rb +++ b/spec/mailers/order_mailer_spec.rb @@ -19,6 +19,10 @@ describe Spree::OrderMailer do ship_address = create(:address, :address1 => "distributor address", :city => 'The Shire', :zipcode => "1234") @order1 = create(:order, :distributor => @distributor, :bill_address => @bill_address, ship_address: ship_address, :special_instructions => @shipping_instructions) ActionMailer::Base.deliveries = [] + Spree::MailMethod.create!( + environment: Rails.env, + preferred_mails_from: 'spree@example.com' + ) end describe "order confirmation for customers" do diff --git a/spec/mailers/producer_mailer_spec.rb b/spec/mailers/producer_mailer_spec.rb index c964342073..0ae398dd27 100644 --- a/spec/mailers/producer_mailer_spec.rb +++ b/spec/mailers/producer_mailer_spec.rb @@ -2,6 +2,12 @@ require 'spec_helper' require 'yaml' describe ProducerMailer do + before do + Spree::MailMethod.create!( + environment: Rails.env, + preferred_mails_from: 'spree@example.com' + ) + end let!(:zone) { create(:zone_with_member) } let!(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Spree::Calculator::DefaultTax.new, zone: zone, amount: 0.1) } let!(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) } diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 5e389b71bf..d5044449de 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -27,7 +27,7 @@ describe Spree::UserMailer do context 'subject includes' do it 'translated devise instructions' do - expect(@message.subject).to include "Password Reset Instructions" + expect(@message.subject).to include "Reset password instructions" end it 'Spree site name' do diff --git a/spec/models/enterprise_fee_spec.rb b/spec/models/enterprise_fee_spec.rb index e0df4ce5d0..6c4245c875 100644 --- a/spec/models/enterprise_fee_spec.rb +++ b/spec/models/enterprise_fee_spec.rb @@ -170,7 +170,7 @@ describe EnterpriseFee do order.adjustments.create({:amount => 12.34, :source => order, :originator => tax_rate, - :locked => true, + :state => 'closed', :label => 'hello' }, :without_protection => true) expect do diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index 8715e1ef5c..59d71c59ef 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -516,6 +516,12 @@ describe OrderCycle do let!(:order4) { create(:completed_order_with_totals, distributor: shop, user: user, order_cycle: create(:order_cycle)) } let!(:order5) { create(:completed_order_with_totals, distributor: shop, user: user, order_cycle: oc) } + before do + Spree::MailMethod.create!( + environment: Rails.env, + preferred_mails_from: 'spree@example.com' + ) + end before { order5.cancel } it "only returns items from non-cancelled orders in the OC, placed by the user at the shop" do diff --git a/spec/models/product_distribution_spec.rb b/spec/models/product_distribution_spec.rb index 7e1a6933bc..2fa2bab427 100644 --- a/spec/models/product_distribution_spec.rb +++ b/spec/models/product_distribution_spec.rb @@ -78,7 +78,7 @@ describe ProductDistribution do it "returns the adjustment when present" do pd = create(:product_distribution) line_item = create(:line_item) - adjustment = pd.enterprise_fee.create_locked_adjustment('foo', line_item.order, line_item, true) + adjustment = pd.enterprise_fee.create_adjustment('foo', line_item.order, line_item, true) pd.send(:adjustment_for, line_item).should == adjustment end @@ -86,8 +86,8 @@ describe ProductDistribution do it "raises an error when there are multiple adjustments for this enterprise fee" do pd = create(:product_distribution) line_item = create(:line_item) - pd.enterprise_fee.create_locked_adjustment('one', line_item.order, line_item, true) - pd.enterprise_fee.create_locked_adjustment('two', line_item.order, line_item, true) + pd.enterprise_fee.create_adjustment('one', line_item.order, line_item, true) + pd.enterprise_fee.create_adjustment('two', line_item.order, line_item, true) expect do pd.send(:adjustment_for, line_item) diff --git a/spec/models/spree/gateway/stripe_connect_spec.rb b/spec/models/spree/gateway/stripe_connect_spec.rb new file mode 100644 index 0000000000..4e39a7812e --- /dev/null +++ b/spec/models/spree/gateway/stripe_connect_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe Spree::Gateway::StripeConnect, type: :model do + let(:provider) do + double('provider').tap do |p| + p.stub(:purchase) + p.stub(:authorize) + p.stub(:capture) + end + end + + let(:stripe_account_id) { "acct_123" } + + before do + allow(Stripe).to receive(:api_key) { "sk_test_123456" } + allow(subject).to receive(:stripe_account_id) { stripe_account_id } + subject.stub(:options_for_purchase_or_auth).and_return(['money', 'cc', 'opts']) + subject.stub(:provider).and_return provider + end + + describe "#token_from_card_profile_ids" do + let(:creditcard) { double(:creditcard) } + context "when the credit card provided has a gateway_payment_profile_id" do + before do + allow(creditcard).to receive(:gateway_payment_profile_id) { "token_or_card_id123" } + allow(subject).to receive(:tokenize_instance_customer_card) { "tokenized" } + end + + context "when the credit card provided has a gateway_customer_profile_id" do + before { allow(creditcard).to receive(:gateway_customer_profile_id) { "customer_id123" } } + + it "requests a new token via tokenize_instance_customer_card" do + result = subject.send(:token_from_card_profile_ids, creditcard) + expect(result).to eq "tokenized" + end + end + + context "when the credit card provided does not have a gateway_customer_profile_id" do + before { allow(creditcard).to receive(:gateway_customer_profile_id) { nil } } + it "returns the gateway_payment_profile_id (assumed to be a token already)" do + result = subject.send(:token_from_card_profile_ids, creditcard) + expect(result).to eq "token_or_card_id123" + end + end + end + + context "when the credit card provided does not have a gateway_payment_profile_id" do + before { allow(creditcard).to receive(:gateway_payment_profile_id) { nil } } + before { allow(creditcard).to receive(:gateway_customer_profile_id) { "customer_id123" } } + + it "returns nil....?" do + result = subject.send(:token_from_card_profile_ids, creditcard) + expect(result).to be nil + end + end + end + + describe "#tokenize_instance_customer_card" do + let(:customer_id) { "customer123" } + let(:card_id) { "card123" } + let(:token_mock) { { id: "test_token123" } } + + before do + stub_request(:post, "https://api.stripe.com/v1/tokens") + .with(body: { "card" => "card123", "customer" => "customer123"}) + .to_return(body: JSON.generate(token_mock)) + end + + it "requests a new token for the customer and card from Stripe, and returns the id of the response" do + expect(subject.send(:tokenize_instance_customer_card, customer_id, card_id)).to eq token_mock[:id] + end + end +end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index e7c46c27cb..d722e234d2 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -140,7 +140,7 @@ describe Spree::Order do it "returns the sum of eligible enterprise fee adjustments" do ef = create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new ) ef.calculator.set_preference :amount, 123.45 - a = ef.create_locked_adjustment("adjustment", o, o, true) + a = ef.create_adjustment("adjustment", o, o, true) o.admin_and_handling_total.should == 123.45 end @@ -148,7 +148,7 @@ describe Spree::Order do it "does not include ineligible adjustments" do ef = create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new ) ef.calculator.set_preference :amount, 123.45 - a = ef.create_locked_adjustment("adjustment", o, o, true) + a = ef.create_adjustment("adjustment", o, o, true) a.update_column :eligible, false @@ -487,6 +487,13 @@ describe Spree::Order do describe "scopes" do describe "not_state" do + before do + Spree::MailMethod.create!( + environment: Rails.env, + preferred_mails_from: 'spree@example.com' + ) + end + it "finds only orders not in specified state" do o = FactoryGirl.create(:completed_order_with_totals) o.cancel! @@ -702,7 +709,7 @@ describe Spree::Order do it "removes transaction fees" do # Change the payment method - order.payment.update_attributes(payment_method_id: payment_method.id) + order.payments.first.update_attributes(payment_method_id: payment_method.id) order.save # Check if fees got updated @@ -743,4 +750,15 @@ describe Spree::Order do end end end + + describe "determining checkout steps for an order" do + let!(:enterprise) { create(:enterprise) } + let!(:order) { create(:order, distributor: enterprise) } + let!(:payment_method) { create(:stripe_payment_method, distributor_ids: [enterprise.id], preferred_enterprise_id: enterprise.id) } + let!(:payment) { create(:payment, order: order, payment_method: payment_method) } + + it "does not include the :confirm step" do + expect(order.checkout_steps).to_not include "confirm" + end + end end diff --git a/spec/models/spree/payment_method_spec.rb b/spec/models/spree/payment_method_spec.rb index 70808c231e..28707876d3 100644 --- a/spec/models/spree/payment_method_spec.rb +++ b/spec/models/spree/payment_method_spec.rb @@ -21,6 +21,7 @@ module Spree Spree::Gateway::Migs.clean_name.should == "MasterCard Internet Gateway Service (MIGS)" Spree::Gateway::Pin.clean_name.should == "Pin Payments" Spree::Gateway::PayPalExpress.clean_name.should == "PayPal Express" + Spree::Gateway::StripeConnect.clean_name.should == "Stripe" # Testing else condition Spree::Gateway::BogusSimple.clean_name.should == "BogusSimple" diff --git a/spec/models/spree/payment_spec.rb b/spec/models/spree/payment_spec.rb index 0877959cbb..36fb1ceaea 100644 --- a/spec/models/spree/payment_spec.rb +++ b/spec/models/spree/payment_spec.rb @@ -124,5 +124,89 @@ module Spree payment.refund! end end + + describe "applying transaction fees" do + let!(:order) { create(:order) } + let!(:line_item) { create(:line_item, order: order, quantity: 3, price: 5.00) } + + before do + order.reload.update! + end + + context "to Stripe payments" do + let(:shop) { create(:enterprise) } + let(:payment_method) { create(:stripe_payment_method, distributor_ids: [create(:distributor_enterprise).id], preferred_enterprise_id: shop.id) } + let(:payment) { create(:payment, order: order, payment_method: payment_method, amount: order.total) } + let(:calculator) { Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) } + + before do + payment_method.calculator = calculator + payment_method.save! + + allow(order).to receive(:pending_payments) { [payment] } + end + + context "when the payment fails" do + let(:failed_response) { ActiveMerchant::Billing::Response.new(false, "This is an error message") } + + before do + allow(payment_method).to receive(:purchase) { failed_response } + end + + it "makes the transaction fee ineligible and finalizes it" do + # Decided to wrap the save process in order.process_payments! + # since that is the context it is usually performed in + order.process_payments! + expect(order.payments.count).to eq 1 + expect(order.payments).to include payment + expect(payment.state).to eq "failed" + expect(payment.adjustment.eligible?).to be false + expect(payment.adjustment.finalized?).to be true + expect(order.adjustments.payment_fee.count).to eq 1 + expect(order.adjustments.payment_fee.eligible).to_not include payment.adjustment + end + end + + context "when the payment information is invalid" do + before do + allow(payment_method).to receive(:supports?) { false } + end + + it "makes the transaction fee ineligible and finalizes it" do + # Decided to wrap the save process in order.process_payments! + # since that is the context it is usually performed in + order.process_payments! + expect(order.payments.count).to eq 1 + expect(order.payments).to include payment + expect(payment.state).to eq "invalid" + expect(payment.adjustment.eligible?).to be false + expect(payment.adjustment.finalized?).to be true + expect(order.adjustments.payment_fee.count).to eq 1 + expect(order.adjustments.payment_fee.eligible).to_not include payment.adjustment + end + end + + context "when the payment is processed successfully" do + let(:successful_response) { ActiveMerchant::Billing::Response.new(true, "Yay!") } + + before do + allow(payment_method).to receive(:purchase) { successful_response } + end + + it "creates an appropriate adjustment" do + # Decided to wrap the save process in order.process_payments! + # since that is the context it is usually performed in + order.process_payments! + expect(order.payments.count).to eq 1 + expect(order.payments).to include payment + expect(payment.state).to eq "completed" + expect(payment.adjustment.eligible?).to be true + expect(order.adjustments.payment_fee.count).to eq 1 + expect(order.adjustments.payment_fee.eligible).to include payment.adjustment + expect(payment.adjustment.amount).to eq 1.5 + end + end + end + end end end diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 0b57414677..ea22d42ec0 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -1,6 +1,4 @@ describe Spree.user_class do - include AuthenticationWorkflow - describe "associations" do it { should have_many(:owned_enterprises) } @@ -95,56 +93,11 @@ describe Spree.user_class do end describe "as admin" do - let(:admin) { quick_login_as_admin } + let(:admin) { create(:admin_user) } it "returns all users" do expect(admin.known_users).to include u1, u2, u3 end end end - - describe "retrieving orders for /account page" do - let!(:u1) { create(:user) } - let!(:u2) { create(:user) } - let!(:distributor1) { create(:distributor_enterprise) } - let!(:distributor2) { create(:distributor_enterprise) } - let!(:d1o1) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id) } - let!(:d1o2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u1.id) } - let!(:d1_order_for_u2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u2.id) } - let!(:d1o3) { create(:order, state: 'cart', distributor: distributor1, user_id: u1.id) } - let!(:d2o1) { create(:completed_order_with_totals, distributor: distributor2, user_id: u2.id) } - let!(:accounts_distributor) {create :distributor_enterprise} - let!(:order_account_invoice) { create(:order, distributor: accounts_distributor, state: 'complete', user: u1) } - - let!(:completed_payment) { create(:payment, order: d1o1, state: 'completed') } - let!(:payment) { create(:payment, order: d1o2, state: 'checkout') } - - before do - Spree::Config.accounts_distributor_id = accounts_distributor.id - end - - it "returns enterprises that the user has ordered from, excluding accounts distributor" do - expect(u1.enterprises_ordered_from).to eq [distributor1.id] - end - - it "returns orders and payments for the user, organised by distributor" do - expect(u1.orders_by_distributor).to include distributor1 - expect(u1.orders_by_distributor.first.distributed_orders).to include d1o1 - end - - it "doesn't return irrelevant distributors" do - expect(u1.orders_by_distributor).not_to include distributor2 - end - it "doesn't return other users' orders" do - expect(u1.orders_by_distributor.first.distributed_orders).not_to include d1_order_for_u2 - end - - it "doesn't return uncompleted orders" do - expect(u1.orders_by_distributor.first.distributed_orders).not_to include d1o3 - end - - it "doesn't return payments that are still at checkout stage" do - expect(u1.orders_by_distributor.first.distributed_orders.map{|o| o.payments}.flatten).not_to include payment - end - end end diff --git a/spec/models/stripe_account_spec.rb b/spec/models/stripe_account_spec.rb new file mode 100644 index 0000000000..b4a1cbfb00 --- /dev/null +++ b/spec/models/stripe_account_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' +require 'stripe/oauth' + +describe StripeAccount do + describe "deauthorize_and_destroy" do + let!(:enterprise) { create(:enterprise) } + let!(:enterprise2) { create(:enterprise) } + let(:client_id) { 'ca_abc123' } + let(:stripe_user_id) { 'acct_abc123' } + let!(:stripe_account) { create(:stripe_account, enterprise: enterprise, stripe_user_id: stripe_user_id) } + + before do + allow(Stripe).to receive(:api_key) { "sk_test_12345" } + allow(Stripe).to receive(:client_id) { client_id } + end + + context "when the Stripe API disconnect fails" do + before do + stub_request(:post, "https://connect.stripe.com/oauth/deauthorize"). + with(body: { "client_id" => client_id, "stripe_user_id" => stripe_user_id }). + to_return(status: 400, body: JSON.generate(error: 'invalid_grant', error_description: "Some Message")) + end + + it "destroys the record and notifies Bugsnag" do + expect(Bugsnag).to receive(:notify) + stripe_account.deauthorize_and_destroy + expect(StripeAccount.all).to_not include(stripe_account) + end + end + + context "when the Stripe API disconnect succeeds" do + before do + stub_request(:post, "https://connect.stripe.com/oauth/deauthorize"). + with(body: { "client_id" => client_id, "stripe_user_id" => stripe_user_id }). + to_return(status: 200, body: JSON.generate(stripe_user_id: stripe_user_id)) + end + + it "destroys the record" do + stripe_account.deauthorize_and_destroy + expect(StripeAccount.all).not_to include(stripe_account) + end + end + + context "if the account is also associated with another Enterprise" do + let!(:another_stripe_account) { create(:stripe_account, enterprise: enterprise2, stripe_user_id: stripe_user_id) } + + it "Doesn't make a Stripe API disconnection request " do + expect(Stripe::OAuth).to_not receive(:deauthorize) + stripe_account.deauthorize_and_destroy + expect(StripeAccount.all).not_to include(stripe_account) + end + end + end +end diff --git a/spec/requests/checkout/failed_checkout_spec.rb b/spec/requests/checkout/failed_checkout_spec.rb new file mode 100644 index 0000000000..6246beadf0 --- /dev/null +++ b/spec/requests/checkout/failed_checkout_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe "checking out an order that initially fails", type: :request do + include ShopWorkflow + + let!(:shop) { create(:enterprise) } + let!(:order_cycle) { create(:simple_order_cycle) } + let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: order_cycle.coordinator, receiver: shop, incoming: false, pickup_time: "Monday") } + let!(:address) { create(:address) } + let!(:order) { create(:order, distributor: shop, order_cycle: order_cycle) } + let!(:line_item) { create(:line_item, order: order, quantity: 3, price: 5.00) } + let!(:payment_method) { create(:bogus_payment_method, distributor_ids: [shop.id], environment: Rails.env) } + let!(:check_payment_method) { create(:payment_method, distributor_ids: [shop.id], environment: Rails.env) } + let!(:shipping_method) { create(:shipping_method, distributor_ids: [shop.id]) } + let!(:shipment) { create(:shipment, order: order, shipping_method: shipping_method) } + let(:params) do + { format: :json, order: { + shipping_method_id: shipping_method.id, + payments_attributes: [{payment_method_id: payment_method.id}], + bill_address_attributes: address.attributes.slice("firstname", "lastname", "address1", "address2", "phone", "city", "zipcode", "state_id", "country_id"), + ship_address_attributes: address.attributes.slice("firstname", "lastname", "address1", "address2", "phone", "city", "zipcode", "state_id", "country_id") + } } + end + + before do + order.reload.update_totals + set_order order + end + + context "when shipping and payment fees apply" do + let(:calculator) { Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) } + + before do + payment_method.calculator = calculator.dup + payment_method.save! + check_payment_method.calculator = calculator.dup + check_payment_method.save! + shipping_method.calculator = calculator.dup + shipping_method.save! + end + + it "clears shipments and payments before rendering the checkout" do + put update_checkout_path, params + + # Checking out a BogusGateway without a source fails at :payment + # Shipments and payments should then be cleared before rendering checkout + expect(response.status).to be 400 + expect(flash[:error]).to eq I18n.t(:payment_processing_failed) + order.reload + expect(order.shipments.count).to be 0 + expect(order.payments.count).to be 0 + expect(order.adjustment_total).to eq 0 + + # Add another line item to change the fee totals + create(:line_item, order: order, quantity: 3, price: 5.00) + + # Use a check payment method, which should work + params[:order][:payments_attributes][0][:payment_method_id] = check_payment_method.id + put update_checkout_path, params + + expect(response.status).to be 200 + order.reload + expect(order.total).to eq 36 + expect(order.adjustment_total).to eq 6 + expect(order.item_total).to eq 30 + expect(order.shipments.count).to eq 1 + expect(order.payments.count).to eq 1 + end + end +end diff --git a/spec/requests/checkout/paypal_spec.rb b/spec/requests/checkout/paypal_spec.rb new file mode 100644 index 0000000000..0537770474 --- /dev/null +++ b/spec/requests/checkout/paypal_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe "checking out an order with a paypal express payment method", type: :request do + include ShopWorkflow + + let!(:address) { create(:address) } + let!(:shop) { create(:enterprise) } + let!(:shipping_method) { create(:shipping_method, distributor_ids: [shop.id]) } + let!(:order) { create(:order, distributor: shop, ship_address: address.dup, bill_address: address.dup) } + let!(:shipment) { create(:shipment, order: order, shipping_method: shipping_method) } + let!(:line_item) { create(:line_item, order: order, quantity: 3, price: 5.00) } + let!(:payment_method) { Spree::Gateway::PayPalExpress.create!(name: "PayPalExpress", distributor_ids: [create(:distributor_enterprise).id], environment: Rails.env) } + let(:params) { { token: 'lalalala', PayerID: 'payer1', payment_method_id: payment_method.id } } + let(:mocked_xml_response) { + " + + + Success + Something + + s0metran$act10n + + + " + } + + before do + order.reload.update_totals + order.shipping_method = shipping_method + expect(order.next).to be true # => address + expect(order.next).to be true # => delivery + expect(order.next).to be true # => payment + set_order order + + stub_request(:post, "https://api-3t.sandbox.paypal.com/2.0/") + .to_return(:status => 200, :body => mocked_xml_response ) + end + + context "with a flat percent calculator" do + let(:calculator) { Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) } + + before do + payment_method.calculator = calculator + payment_method.save! + order.payments.create!(payment_method_id: payment_method.id, amount: order.total) + end + + it "destroys the old payment and processes the order" do + # Sanity check to condition of the order before we confirm the payment + expect(order.payments.count).to eq 1 + expect(order.payments.first.state).to eq "checkout" + expect(order.adjustments.payment_fee.count).to eq 1 + expect(order.adjustments.payment_fee.first.amount).to eq 1.5 + + get spree.confirm_paypal_path, params + + # Processing was successful, order is complete + expect(response).to redirect_to spree.order_path(order, :token => order.token) + expect(order.reload.complete?).to be true + + # We have only one payment, and one transaction fee + expect(order.payments.count).to eq 1 + expect(order.payments.first.state).to eq "completed" + expect(order.adjustments.payment_fee.count).to eq 1 + expect(order.adjustments.payment_fee.first.amount).to eq 1.5 + end + end +end diff --git a/spec/requests/checkout/stripe_connect_spec.rb b/spec/requests/checkout/stripe_connect_spec.rb new file mode 100644 index 0000000000..d78b315e34 --- /dev/null +++ b/spec/requests/checkout/stripe_connect_spec.rb @@ -0,0 +1,223 @@ +require 'spec_helper' + +describe "checking out an order with a Stripe Connect payment method", type: :request do + include ShopWorkflow + include AuthenticationWorkflow + + let!(:order_cycle) { create(:simple_order_cycle) } + let!(:enterprise) { create(:distributor_enterprise) } + let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: order_cycle.coordinator, receiver: enterprise, incoming: false, pickup_time: "Monday") } + let!(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 0), distributors: [enterprise]) } + let!(:payment_method) { create(:stripe_payment_method, distributors: [enterprise], preferred_enterprise_id: enterprise.id) } + 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(:token) { "token123" } + let(:new_token) { "newtoken123" } + let(:card_id) { "card_XyZ456" } + let(:customer_id) { "cus_A123" } + let(:params) do + { format: :json, order: { + shipping_method_id: shipping_method.id, + payments_attributes: [{payment_method_id: payment_method.id, source_attributes: { gateway_payment_profile_id: token, cc_type: "visa", last_digits: "4242", month: 10, year: 2025, first_name: 'Jill', last_name: 'Jeffreys' }}], + bill_address_attributes: address.attributes.slice("firstname", "lastname", "address1", "address2", "phone", "city", "zipcode", "state_id", "country_id"), + ship_address_attributes: address.attributes.slice("firstname", "lastname", "address1", "address2", "phone", "city", "zipcode", "state_id", "country_id") + } } + end + + before do + 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 a new card is submitted" do + let(:store_response_mock) { { status: 200, body: JSON.generate(id: customer_id, default_card: card_id, sources: { data: [{id: "1"}] }) } } + let(:token_response_mock) { { status: 200, body: JSON.generate(id: new_token) } } + let(:charge_response_mock) { { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } } + + context "and the user doesn't request that the card is saved for later" do + before do + # Charges the card + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges") + .with(body: /#{token}.*#{order.number}/).to_return(charge_response_mock) + end + + context "and the charge request is successful" do + it "should process the payment without storing card details" do + put update_checkout_path, params + json_response = JSON.parse(response.body) + 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 token + 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 charge request returns an error message" do + let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "charge-failure"}) } } + + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq "charge-failure" + expect(order.payments.completed.count).to be 0 + end + end + end + + context "and the customer requests that the card is saved for later" do + before do + params[:order][:payments_attributes][0][:source_attributes][:save_requested_by_customer] = '1' + + # Saves the card against the user + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/customers") + .with(:body => { card: token, email: order.email}) + .to_return(store_response_mock) + + # Requests a token from the newly saved card + stub_request(:post, "https://api.stripe.com/v1/tokens") + .with(:body => { card: card_id, customer: customer_id}) + .to_return(token_response_mock) + + # Charges the card + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges") + .with(body: /#{token}.*#{order.number}/).to_return(charge_response_mock) + end + + context "and the store, token and charge requests are successful" do + it "should process the payment, and stores the card/customer details" do + put update_checkout_path, params + json_response = JSON.parse(response.body) + 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 card_id + 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 store request returns an error message" do + let(:store_response_mock) { { status: 402, body: JSON.generate(error: { message: "store-failure"}) } } + + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq I18n.t(:spree_gateway_error_flash_for_checkout, error: 'store-failure') + expect(order.payments.completed.count).to be 0 + end + end + + context "when the charge request returns an error message" do + let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "charge-failure"}) } } + + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq "charge-failure" + expect(order.payments.completed.count).to be 0 + end + end + + context "when the token request returns an error message" do + let(:token_response_mock) { { status: 402, body: JSON.generate(error: { message: "token-failure"}) } } + + # Note, no requests have been stubbed + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq "token-failure" + expect(order.payments.completed.count).to be 0 + end + end + end + end + + context "when an existing card is submitted" do + let(:credit_card) do + create( + :credit_card, + user_id: order.user_id, + gateway_payment_profile_id: card_id, + gateway_customer_profile_id: customer_id, + last_digits: "4321", + cc_type: "master", + first_name: "Sammy", + last_name: "Signpost", + month: 11, year: 2026 + ) + end + + let(:token_response_mock) { { status: 200, body: JSON.generate(id: new_token) } } + let(:charge_response_mock) { { status: 200, body: JSON.generate(id: "ch_1234", object: "charge", amount: 2000) } } + + before do + params[:order][:existing_card_id] = credit_card.id + quick_login_as(order.user) + + # Requests a token + stub_request(:post, "https://api.stripe.com/v1/tokens") + .with(:body => {"card" => card_id, "customer" => customer_id}) + .to_return(token_response_mock) + + # Charges the card + stub_request(:post, "https://sk_test_12345:@api.stripe.com/v1/charges") + .with(body: /#{token}.*#{order.number}/).to_return(charge_response_mock) + end + + context "and the charge and token requests are accepted" do + it "should process the payment, and keep the profile ids and other card details" do + put update_checkout_path, params + json_response = JSON.parse(response.body) + 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 card_id + 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 charge request returns an error message" do + let(:charge_response_mock) { { status: 402, body: JSON.generate(error: { message: "charge-failure"}) } } + + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq "charge-failure" + expect(order.payments.completed.count).to be 0 + end + end + + context "when the token request returns an error message" do + let(:token_response_mock) { { status: 402, body: JSON.generate(error: { message: "token-error"}) } } + + it "should not process the payment" do + put update_checkout_path, params + expect(response.status).to be 400 + json_response = JSON.parse(response.body) + expect(json_response["flash"]["error"]).to eq "token-error" + expect(order.payments.completed.count).to be 0 + end + end + end +end diff --git a/spec/requests/embedded_shopfronts_headers_spec.rb b/spec/requests/embedded_shopfronts_headers_spec.rb new file mode 100644 index 0000000000..a991d7362c --- /dev/null +++ b/spec/requests/embedded_shopfronts_headers_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe "setting response headers for embedded shopfronts", type: :request do + include AuthenticationWorkflow + + let(:enterprise) { create(:distributor_enterprise) } + let(:user) { enterprise.owner } + + before do + quick_login_as(user) + end + + context "with embedded shopfront disabled" do + before do + Spree::Config[:enable_embedded_shopfronts] = false + end + + it "disables iframes by default" do + get shops_path + expect(response.status).to be 200 + expect(response.headers['X-Frame-Options']).to eq 'DENY' + expect(response.headers['Content-Security-Policy']).to eq "frame-ancestors 'none'" + end + end + + context "with embedded shopfronts enabled" do + before do + Spree::Config[:enable_embedded_shopfronts] = true + end + + context "but no whitelist" do + before do + Spree::Config[:embedded_shopfronts_whitelist] = "" + end + + it "disables iframes" do + get shops_path + expect(response.status).to be 200 + expect(response.headers['X-Frame-Options']).to eq 'DENY' + expect(response.headers['Content-Security-Policy']).to eq "frame-ancestors 'none'" + end + end + + context "with a valid whitelist" do + before do + Spree::Config[:embedded_shopfronts_whitelist] = "test.com" + end + + it "allows iframes on certain pages when enabled in configuration" do + get shops_path + expect(response.status).to be 200 + expect(response.headers['X-Frame-Options']).to be_nil + expect(response.headers['Content-Security-Policy']).to eq "frame-ancestors test.com" + + get spree.admin_path + expect(response.status).to be 200 + expect(response.headers['X-Frame-Options']).to eq 'DENY' + expect(response.headers['Content-Security-Policy']).to eq "frame-ancestors 'none'" + end + end + end +end diff --git a/spec/serializers/credit_card_serializer_spec.rb b/spec/serializers/credit_card_serializer_spec.rb new file mode 100644 index 0000000000..baa9c2a3e9 --- /dev/null +++ b/spec/serializers/credit_card_serializer_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Api::CreditCardSerializer do + let(:card) { create(:credit_card) } + let(:serializer) { Api::CreditCardSerializer.new card } + + + it "serializes a credit card" do + expect(serializer.to_json).to match card.last_digits.to_s + end + + it "formats an identifying string with the card number masked" do + expect(serializer.formatted).to eq "Visa x-1111 Exp:12/2013" + end +end diff --git a/spec/serializers/order_serializer_spec.rb b/spec/serializers/order_serializer_spec.rb index 2c327537eb..cbbe13eb69 100644 --- a/spec/serializers/order_serializer_spec.rb +++ b/spec/serializers/order_serializer_spec.rb @@ -4,14 +4,21 @@ describe Api::OrderSerializer do let(:serializer) { Api::OrderSerializer.new order } let(:order) { create(:completed_order_with_totals) } + let!(:completed_payment) { create(:payment, order: order, state: 'completed', amount: order.total - 1) } + let!(:payment) { create(:payment, order: order, state: 'checkout', amount: 123.45) } it "serializes an order" do expect(serializer.to_json).to match order.number.to_s end it "convert the state attributes to translatable keys" do + # byebug if serializer.to_json =~ /balance_due/ expect(serializer.to_json).to match "complete" expect(serializer.to_json).to match "balance_due" end + it "only serializes completed payments" do + expect(serializer.to_json).to match completed_payment.amount.to_s + expect(serializer.to_json).to_not match payment.amount.to_s + end end diff --git a/spec/serializers/orders_by_distributor_serializer_spec.rb b/spec/serializers/orders_by_distributor_serializer_spec.rb deleted file mode 100644 index 5976e9ad48..0000000000 --- a/spec/serializers/orders_by_distributor_serializer_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe Api::OrdersByDistributorSerializer do - - # Banged lets ensure entered into test database - let!(:distributor1) { create(:distributor_enterprise) } - let!(:distributor2) { create(:distributor_enterprise) } - let!(:user) { create(:user)} - let!(:d1o1) { create(:completed_order_with_totals, distributor: distributor1, user_id: user.id, total: 10000)} - let!(:d1o2) { create(:completed_order_with_totals, distributor: distributor1, user_id: user.id, total: 5000)} - let!(:d2o1) { create(:completed_order_with_totals, distributor: distributor2, user_id: user.id)} - - before do - @data = Enterprise.includes(:distributed_orders).where(enterprises: {id: user.enterprises_ordered_from }, spree_orders: {state: :complete, user_id: user.id}).to_a - @serializer = ActiveModel::ArraySerializer.new(@data, {each_serializer: Api::OrdersByDistributorSerializer}) - end - - it "serializes orders" do - expect(@serializer.to_json).to match "distributed_orders" - end - - it "serializes the balance for each distributor" do - expect(@serializer.serializable_array[0].keys).to include :balance - # Would be good to test adding up balance properly but can't get a non-zero total from the factories... - expect(@serializer.serializable_array[0][:balance]).to eq "0.00" - end - -end diff --git a/spec/services/reset_order_service_spec.rb b/spec/services/reset_order_service_spec.rb new file mode 100644 index 0000000000..e02cdb25e3 --- /dev/null +++ b/spec/services/reset_order_service_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe ResetOrderService do + let(:current_token) { double(:current_token) } + let(:current_distributor) { double(:distributor) } + let(:current_order) do + double( + :current_order, + token: current_token, + distributor: current_distributor + ) + end + let(:tokenized_permission) { double(:tokenized_permission, save!: true) } + let(:new_order) do + double( + :new_order, + set_distributor!: true, + tokenized_permission: tokenized_permission, + ) + end + let(:controller) do + double( + :controller, + current_order: new_order, + expire_current_order: true + ) + end + let(:reset_order_service) { described_class.new(controller, current_order) } + + before do + allow(new_order) + .to receive(:tokenized_permission) + .and_return(tokenized_permission) + + allow(tokenized_permission).to receive(:token=) + end + + describe '#call' do + it 'creates a new order' do + reset_order_service.call + expect(controller).to have_received(:current_order).once.with(true) + end + + it 'sets the new order\'s distributor to the same as the old order' do + reset_order_service.call + + expect(new_order) + .to have_received(:set_distributor!) + .with(current_distributor) + end + + it 'sets the token of the tokenized permissions' do + reset_order_service.call + + expect(new_order.tokenized_permission) + .to have_received(:token=).with(current_token) + end + + it 'persists the tokenized permissions' do + reset_order_service.call + expect(tokenized_permission).to have_received(:save!) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 00e0b152ba..4ef7c63b40 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -23,12 +23,12 @@ WebMock.disable_net_connect!(:allow_localhost => true) # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} -require 'spree/core/testing_support/controller_requests' -require 'spree/core/testing_support/capybara_ext' +require 'spree/testing_support/controller_requests' +require 'spree/testing_support/capybara_ext' require 'spree/api/testing_support/setup' require 'spree/api/testing_support/helpers' require 'spree/api/testing_support/helpers_decorator' -require 'spree/core/testing_support/authorization_helpers' +require 'spree/testing_support/authorization_helpers' # Capybara config require 'capybara/poltergeist' @@ -47,6 +47,9 @@ Capybara.default_max_wait_time = 30 require "paperclip/matchers" +# Override setting in Spree engine: Spree::Core::MailSettings +ActionMailer::Base.default_url_options[:host] = 'test.host' + RSpec.configure do |config| # ## Mock Framework # @@ -98,7 +101,7 @@ RSpec.configure do |config| config.include Spree::UrlHelpers config.include Spree::CheckoutHelpers config.include Spree::MoneyHelper - config.include Spree::Core::TestingSupport::ControllerRequests, :type => :controller + config.include Spree::TestingSupport::ControllerRequests, :type => :controller config.include Devise::TestHelpers, :type => :controller config.extend Spree::Api::TestingSupport::Setup, :type => :controller config.include Spree::Api::TestingSupport::Helpers, :type => :controller diff --git a/spec/support/request/web_helper.rb b/spec/support/request/web_helper.rb index 1bd22b010c..8d9c66c148 100644 --- a/spec/support/request/web_helper.rb +++ b/spec/support/request/web_helper.rb @@ -113,6 +113,10 @@ module WebHelper DirtyFormDialog.new(page) end + def set_i18n_locale(locale = 'en') + page.evaluate_script("I18n.locale = '#{locale}'") + end + def get_i18n_locale page.evaluate_script("I18n.locale;") end diff --git a/spec/support/spree/money_helper.rb b/spec/support/spree/money_helper.rb index f0ca8f73d9..dc5a374c39 100644 --- a/spec/support/spree/money_helper.rb +++ b/spec/support/spree/money_helper.rb @@ -1,7 +1,7 @@ module Spree module MoneyHelper def with_currency(amount, options = {}) - Spree::Money.new(amount, {delimiter: ''}.merge(options)).to_s # Delimiter is to match js localizeCurrency + Spree::Money.new(amount, options).to_s end end end