diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 00000000000..4598eded4d4
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,165 @@
+version: 2 # use CircleCI 2.0
+
+defaults: &defaults
+ environment: &environment
+ CIRCLE_TEST_REPORTS: /tmp/test-results
+ CIRCLE_ARTIFACTS: /tmp/test-artifacts
+ BUNDLE_JOBS: 4
+ BUNDLE_RETRY: 3
+ BUNDLE_PATH: ~/spree/vendor/bundle
+ working_directory: ~/spree
+ docker:
+ - image: &image circleci/ruby:2.5-node-browsers
+
+run_tests: &run_tests
+ <<: *defaults
+ parallelism: 4
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - spree-bundle-v3-{{ checksum ".ruby-version" }}-{{ .Branch }}
+ - spree-bundle-v3-{{ checksum ".ruby-version" }}
+ - run:
+ name: Ensure Bundle Install
+ command: |
+ bundle check --path=~/spree/vendor/bundle || bundle install
+ ./build-ci.rb install
+ - run:
+ name: Run rspec in parallel
+ command: BUNDLE_GEMFILE=../Gemfile ./build-ci.rb test
+ - store_artifacts:
+ path: /tmp/test-artifacts
+ destination: test-artifacts
+ - store_artifacts:
+ path: /tmp/test-results
+ destination: raw-test-output
+ - store_test_results:
+ path: /tmp/test-results
+
+jobs:
+ bundle_install:
+ <<: *defaults
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - spree-bundle-v3-{{ checksum ".ruby-version" }}-{{ .Branch }}
+ - spree-bundle-v3-{{ checksum ".ruby-version" }}
+ - run:
+ name: Bundle Install
+ command: |
+ bundle check --path=~/spree/vendor/bundle || bundle install
+ ./build-ci.rb install
+ - save_cache:
+ key: spree-bundle-v3-{{ checksum ".ruby-version" }}-{{ .Branch }}
+ paths:
+ - ~/spree/vendor/bundle
+
+ run_tests_postgres: &run_tests_postgres
+ <<: *run_tests
+ environment: &postgres_environment
+ <<: *environment
+ DB: postgres
+ DB_HOST: localhost
+ DB_USERNAME: postgres
+ docker:
+ - image: *image
+ - image: circleci/postgres:10-alpine
+ environment:
+ POSTGRES_USER: postgres
+
+ run_tests_postgres_paperclip:
+ <<: *run_tests_postgres
+ environment:
+ <<: *postgres_environment
+ SPREE_USE_PAPERCLIP: true
+
+ run_tests_mysql:
+ <<: *run_tests
+ environment:
+ <<: *environment
+ DB: mysql
+ DB_HOST: 127.0.0.1
+ DB_USERNAME: root
+ COVERAGE: true
+ COVERAGE_DIR: /tmp/workspace/simplecov
+ docker:
+ - image: *image
+ - image: circleci/mysql:8-ram
+ command: [--default-authentication-plugin=mysql_native_password]
+ steps:
+ - checkout
+ - restore_cache:
+ keys:
+ - spree-bundle-v3-{{ checksum ".ruby-version" }}-{{ .Branch }}
+ - spree-bundle-v3-{{ checksum ".ruby-version" }}
+ - run:
+ name: Ensure Bundle Install
+ command: |
+ bundle check --path=~/spree/vendor/bundle || bundle install
+ ./build-ci.rb install
+ - run:
+ name: Run rspec in parallel
+ command: BUNDLE_GEMFILE=../Gemfile ./build-ci.rb test
+ - store_artifacts:
+ path: /tmp/test-artifacts
+ destination: test-artifacts
+ - store_artifacts:
+ path: /tmp/test-results
+ destination: raw-test-output
+ - store_test_results:
+ path: /tmp/test-results
+ - persist_to_workspace:
+ root: /tmp/workspace
+ paths:
+ - simplecov
+
+ send_test_coverage:
+ <<: *defaults
+ steps:
+ - checkout
+ - attach_workspace:
+ at: /tmp/workspace
+ - run:
+ name: Download cc-test-reporter
+ command: |
+ mkdir -p tmp/
+ curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./tmp/cc-test-reporter
+ chmod +x ./tmp/cc-test-reporter
+ - run:
+ name: Setup ENVs
+ command: |
+ export GIT_BRANCH="$CIRCLE_BRANCH"
+ export GIT_COMMIT_SHA="$CIRCLE_SHA1"
+ export GIT_COMMITTED_AT="$(date +%s)"
+ - run:
+ name: Format test coverage
+ command: |
+ ./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.api.json /tmp/workspace/simplecov/api/.resultset.json
+ ./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.backend.json /tmp/workspace/simplecov/backend/.resultset.json
+ ./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.core.json /tmp/workspace/simplecov/core/.resultset.json
+ ./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.frontend.json /tmp/workspace/simplecov/frontend/.resultset.json
+ - run:
+ name: Upload coverage results to Code Climate
+ command: |
+ ./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p 4 -o tmp/codeclimate.total.json
+ ./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json
+
+workflows:
+ version: 2
+ main:
+ jobs:
+ - bundle_install
+ - run_tests_postgres:
+ requires:
+ - bundle_install
+ - run_tests_mysql:
+ requires:
+ - bundle_install
+ - run_tests_postgres_paperclip:
+ requires:
+ - run_tests_postgres
+ - send_test_coverage:
+ requires:
+ - run_tests_mysql
diff --git a/.codeclimate.yml b/.codeclimate.yml
new file mode 100644
index 00000000000..9674ec51155
--- /dev/null
+++ b/.codeclimate.yml
@@ -0,0 +1,20 @@
+version: "2"
+plugins:
+ rubocop:
+ enabled: true
+ config:
+ file: .rubocop.yml
+ channel: "rubocop-0-60" # need to keep this value the same as rubocop version
+ # https://docs.codeclimate.com/v1.0/docs/rubocop#section-using-rubocop-s-newer-versions
+ eslint:
+ enabled: true
+ channel: "eslint-4" # need to keep this value the same as eslint version
+ # https://docs.codeclimate.com/v1.0/docs/eslint#section-eslint-versions
+ stylelint:
+ enabled: true
+exclude_patterns:
+ - "**/bin/"
+ - "**/script/"
+ - "**/vendor/"
+ - "**/spec/"
+ - "public/"
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000000..3f34120dc96
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,22 @@
+# http://EditorConfig.org
+# https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+tab_width = 2
+
+[**.rb]
+max_line_length = 150
+
+[**.js, **.coffee]
+max_line_length = 150
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000000..a6eabd061b4
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,9 @@
+**/vendor/**
+**/dummy/**
+**/sandbox/**
+**/guides/output/**
+*-min.js
+*.min.js
+jquery*.js
+/guides/content/assets/javascripts/css_browser_selector_dev.js
+/cmd/lib/spree_cmd/templates/**
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000000..f5c1086eb2d
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,17 @@
+---
+extends: standard
+
+globals:
+ Spree: true
+ analytics: true
+ ga: true
+ _: true
+ Select2: true
+ Handlebars: true
+ Raphael: true
+ SpreeAPI: true
+ SpreePaths: true
+
+env:
+ browser: true
+ jquery: true
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 00000000000..f4409735330
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,63 @@
+## Pull requests
+
+We gladly accept pull requests to add documentation, fix bugs and, in some circumstances,
+add new features to Spree.
+
+Here's a quick guide:
+
+1. Fork the repo
+
+2. Clone the fork to your local machine
+
+3. Run `bundle install` inside `spree` directory
+
+4. Create a sandbox environment
+
+ ```
+ rake sandbox
+ ```
+
+5. To run a sandbox application:
+
+ ```
+ cd sandbox
+ rails s
+ ```
+
+6. Create new branch then make changes and add tests for your changes. Only
+refactoring and documentation changes require no new tests. If you are adding
+functionality or fixing a bug, we need tests!
+
+7. Run the tests. [See instructions](https://github.com/spree/spree#running-tests)
+
+8. Push to your fork and submit a pull request. If the changes will apply cleanly
+to the master branch, you will only need to submit one pull request.
+
+ Don't do pull requests against `-stable` branches. Always target the master branch. Any bugfixes we'll backport to those branches.
+
+At this point, you're waiting on us. We like to at least comment on, if not
+accept, pull requests within three business days (and, typically, one business
+day). We may suggest some changes or improvements or alternatives.
+
+Some things that will increase the chance that your pull request is accepted,
+taken straight from the Ruby on Rails guide:
+
+* Use Rails idioms and helpers
+* Include tests that fail without your code, and pass with it
+* Update the documentation, the surrounding one, examples elsewhere, guides,
+ whatever is affected by your contribution
+
+Syntax:
+
+* Two spaces, no tabs.
+* No trailing whitespace. Blank lines should not have any space.
+* Use &&/|| over and/or.
+* `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`.
+* `a = b` and not `a=b`.
+* `a_method { |block| ... }` and not `a_method { | block | ... }`
+* Follow the conventions you see used in the source already.
+* -> symbol over lambda
+* Ruby 1.9 hash syntax `{ key: value }` over Ruby 1.8 hash syntax `{ :key => value }`
+* Alphabetize the class methods to keep them organized
+
+And in case we didn't emphasize it enough: we love tests!
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000000..94df01c52ad
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,39 @@
+
+
+
+## Context
+
+
+
+## Expected Behavior
+
+
+## Actual Behavior
+
+
+## Possible Fix
+
+
+## Steps to Reproduce
+
+
+1.
+2.
+3.
+4.
+
+## Your Environment
+
+* Version used:
+* Gemfile and Gemfile.lock as text in a Gist:
+* Any relevant stack traces ("Full trace" preferred):
+
+
+
+
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 4b5b2f47255..711ed2839e2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,70 @@
-starter-app/log/*.log
-starter-app/public/images/products
-starter-app/public/images/calendar_date_select
-starter-app/public/plugin_assets
-starter-app/public/javascripts/calendar_date_select
-starter-app/public/stylesheets/calendar_date_select
+\#*
+**/*.db
+**/*.log
+**/*.byebug_history
+*~
+.#*
+.DS_Store
+.bundle
+.dotest
+.idea
+.loadpath
+.project
+.vscode
+bin/*
+public/dispatch.cgi
+public/dispatch.fcgi
+public/dispatch.rb
+httpd
+pgsql
+config/*-public.asc
+config/database.yml
+config/mongrel_cluster.yml
+db/*.sql
+db/*.sqlite3*
+db/schema.rb
+doc/**/*
+Gemfile.lock
+*/Gemfile.lock
+lib/products_index_profiler.rb
+log/*.log
pkg
-starter-app/tmp
+public/assets
+public/attachments
+public/blank_iframe.html
+public/ckeditora
+sandbox/*
+spree_dev
+spree_test
+testapp
+**/spec/dummy
+tmp
+public/google_base.xml
+public/template_google_base.xml
+coverage/*
+var
+TAGS
+nbproject
+./vendor
+tags
+*.swp
+rerun.txt
+test_app
+.rvmrc
+**/coverage
+*/.sass-cache
+.localeapp
+.ruby-gemset
+vendor/bundle
+*/vendor/bundle
+.ignore
+
+# new guides
+guides/output
+guides/logs
+guides/.cache
+guides/public
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.yarn-integrity
diff --git a/.rubocop-disabled.yml b/.rubocop-disabled.yml
new file mode 100644
index 00000000000..35edf7aeb8f
--- /dev/null
+++ b/.rubocop-disabled.yml
@@ -0,0 +1,49 @@
+# disabled for rubocop-rspec
+
+RSpec/DescribeClass:
+ Enabled: false
+
+RSpec/VerifiedDoubles:
+ Enabled: false
+
+RSpec/MessageChain:
+ Enabled: false
+
+RSpec/AnyInstance:
+ Enabled: false
+
+RSpec/InstanceVariable:
+ Enabled: false
+
+RSpec/ContextWording:
+ Enabled: false
+
+RSpec/ExpectInHook:
+ Enabled: false
+
+RSpec/ExampleLength:
+ Enabled: false
+
+RSpec/MessageSpies:
+ Enabled: false
+
+RSpec/NamedSubject:
+ Enabled: false
+
+RSpec/MultipleExpectations:
+ Enabled: false
+
+RSpec/FilePath:
+ Enabled: false
+
+RSpec/LetSetup:
+ Enabled: false
+
+RSpec/SubjectStub:
+ Enabled: false
+
+RSpec/VoidExpect:
+ Enabled: false
+
+RSpec/BeforeAfterAll:
+ Enabled: false
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 00000000000..ef4c956aaa0
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,137 @@
+require: rubocop-rspec
+
+inherit_from: '.rubocop-disabled.yml'
+
+AllCops:
+ TargetRubyVersion: 2.3
+ Exclude:
+ - '**/sandbox/**/*'
+ - '**/db/migrate/*'
+ - '**/Gemfile'
+ - '**/Gemfile.lock'
+ - '**/Rakefile'
+ - '**/rails'
+ - 'guides/**/*'
+ - '**/*.gemspec'
+ - '**/dummy/**/*'
+ - '**/vendor/**/*'
+ - '**/spec_helper.rb'
+ - '**/templates/**/*'
+
+Layout/MultilineOperationIndentation:
+ EnforcedStyle: indented
+
+AlignParameters:
+ Enabled: false
+
+ClassLength:
+ CountComments: false
+ Max: 150
+
+ModuleLength:
+ CountComments: false
+ Max: 250
+ Exclude:
+ - '**/spec/**/*'
+
+Documentation:
+ Enabled: false
+
+Metrics/LineLength:
+ Max: 150
+ Exclude:
+ - '**/spec/**/*'
+
+MethodLength:
+ CountComments: false
+ Max: 50
+
+BlockLength:
+ CountComments: false
+ Max: 50
+ Exclude:
+ - '**/spec/**/*'
+ - '**/*.rake'
+ - '**/factories/**/*'
+ - '**/config/routes.rb'
+
+Metrics/AbcSize:
+ Max: 45
+
+Style/StringLiterals:
+ EnforcedStyle: single_quotes
+
+Layout/DotPosition:
+ EnforcedStyle: trailing
+ Enabled: true
+
+Style/FrozenStringLiteralComment:
+ Enabled: false
+
+Style/RegexpLiteral:
+ Enabled: false
+
+Style/WordArray:
+ Enabled: false
+
+Style/SymbolArray:
+ Enabled: false
+
+Style/GuardClause:
+ Enabled: false
+
+Style/TrailingCommaInArrayLiteral:
+ Enabled: false
+
+Style/TrailingCommaInHashLiteral:
+ Enabled: false
+
+Style/BarePercentLiterals:
+ Enabled: false
+
+Style/MutableConstant:
+ Enabled: false
+
+Style/PercentLiteralDelimiters:
+ Enabled: false
+
+Style/IfUnlessModifier:
+ Enabled: false
+
+Naming/VariableNumber:
+ Enabled: false
+
+Performance/RegexpMatch:
+ Enabled: false
+
+Style/UnneededPercentQ:
+ Enabled: false
+
+Lint/ParenthesesAsGroupedExpression:
+ Enabled: false
+
+Style/NumericPredicate:
+ Enabled: false
+
+Metrics/PerceivedComplexity:
+ Max: 10
+
+Metrics/CyclomaticComplexity:
+ Max: 10
+
+Style/ClassAndModuleChildren:
+ Enabled: false
+
+Style/AndOr:
+ Exclude:
+ - '**/*controller.rb'
+
+RSpec/NestedGroups:
+ Max: 7
+
+Lint/AmbiguousBlockAssociation:
+ Exclude:
+ - '**/spec/**/*'
+
+Style/NumericLiterals:
+ Enabled: false
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 00000000000..73462a5a134
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+2.5.1
diff --git a/.stylelintignore b/.stylelintignore
new file mode 100644
index 00000000000..52462a25269
--- /dev/null
+++ b/.stylelintignore
@@ -0,0 +1,5 @@
+**/vendor/**
+**/dummy/**
+**/sandbox/**
+**/guides/output/**
+/cmd/lib/spree_cmd/templates/**
diff --git a/.stylelintrc b/.stylelintrc
new file mode 100644
index 00000000000..0c4829ceac0
--- /dev/null
+++ b/.stylelintrc
@@ -0,0 +1,6 @@
+{
+ "extends": "stylelint-config-recommended",
+ "rules": {
+ "at-rule-no-unknown": null
+ }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000000..22645affa38
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,39 @@
+### Please leave CHANGELOG entries in the appropriate release notes guide. ##
+
+https://github.com/spree/spree/tree/master/guides/src/content/release_notes
+
+#### 3.7.x (Edge)
+
+https://github.com/spree/spree/blob/master/guides/src/content/release_notes/3_7_0.md
+
+#### 3.6.x
+
+https://github.com/spree/spree/blob/master/guides/src/content/release_notes/3_6_0.md
+
+#### 3.5.x
+
+https://github.com/spree/spree/blob/master/guides/src/content/release_notes/3_5_0.md
+
+#### 3.4.x
+
+https://github.com/spree/spree/blob/master/guides/src/content/release_notes/3_4_0.md
+
+#### 3.3.x
+
+https://github.com/spree/spree/blob/master/guides/src/content/release_notes/3_3_0.md
+
+#### 3.2.x
+
+https://github.com/spree/spree/blob/master/guides/src/content/release_notes/3_2_0.md
+
+#### 3.1.x
+
+https://github.com/spree/spree/blob/master/guides/src/content/release_notes/3_1_0.md
+
+#### 3.0.x
+
+https://github.com/spree/spree/blob/master/guides/src/content/release_notes/3_0_0.md
+
+#### 2.4.x
+
+https://github.com/spree/spree/blob/master/guides/src/content/release_notes/2_4_0.md
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000000..01b8644f13a
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,22 @@
+# Contributor Code of Conduct
+
+As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
+
+We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery
+* Personal attacks
+* Trolling or insulting/derogatory comments
+* Public or private harassment
+* Publishing other's private information, such as physical or electronic addresses, without explicit permission
+* Other unethical or unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
+
+This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/)
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000000..78b0b7184a5
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+# This dockerfile is used to build sandbox image for docker clouds. It's not meant to be used in projects
+FROM ruby:2.5.1
+RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
+RUN mkdir /spree
+WORKDIR /spree
+ADD . /spree
+RUN bundle install
+RUN bundle exec rake sandbox
+CMD ["sh", "docker-entrypoint.sh"]
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 00000000000..c74b2a60ce2
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,3 @@
+eval(File.read(File.dirname(__FILE__) + '/common_spree_dependencies.rb'))
+
+gemspec
diff --git a/License.txt b/License.txt
deleted file mode 100644
index fd599779576..00000000000
--- a/License.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-Copyright (c) 2007-2008, Schofield Tech Works LLC and other contributors
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
- * Neither the name of the Schofield Tech Works LLC nor the names of its
- contributors may be used to endorse or promote products derived from this
- software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
-CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
-EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
-PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
-PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100755
index 00000000000..d3e6c0a6716
--- /dev/null
+++ b/README.md
@@ -0,0 +1,307 @@
+
+
+
+* Join our Slack at [slack.spreecommerce.org](http://slack.spreecommerce.org/)
+* [Success Stories](https://spreecommerce.org/stories/)
+* [Integrations](https://spreecommerce.org/integrations/)
+* [Extensions](https://github.com/spree/spree#extensions)
+* [Documentation](http://guides.spreecommerce.org)
+* [Roadmap](https://github.com/spree/spree/milestones?direction=asc&sort=due_date&state=open)
+* [Maintenance Policy](https://github.com/spree/spree/wiki/Maintenance-Policy)
+
+[](https://badge.fury.io/rb/spree) [](https://circleci.com/gh/spree/spree/tree/master)
+[](https://codeclimate.com/github/spree/spree)
+[](https://codeclimate.com/github/spree/spree/test_coverage)
+[](http://slack.spreecommerce.org)
+
+**Spree** is a complete open source e-commerce solution built with Ruby on Rails. It
+was originally developed by Sean Schofield and is now maintained by [Spark Solutions](http://sparksolutions.co). We're open to [contributions](#contributing) and accepting new [Core Team](https://github.com/spree/spree/wiki/Core-Team) members.
+
+Spree consists of several different gems, each of which are maintained
+in a single repository and documented in a single set of
+[online documentation](http://guides.spreecommerce.org/).
+
+* **spree_api** (new REST API v2 and legacy REST API v1, [GraphQL support](https://github.com/spree/spree/issues/9176) coming soon)
+* **spree_frontend** (default Rails customer frontend)
+* **spree_backend** (Admin Panel)
+* **spree_cmd** (Command-line tools)
+* **spree_core** (Models, Services & Mailers, the basic components of Spree that it can't run without)
+* **spree_sample** (Sample data)
+
+You don't need to install all of the components. Only the **Core** is mandatory.
+
+Demo
+----
+
+[](https://heroku.com/deploy?template=https://github.com/spree/spree/tree/3-6-stable)
+
+If you want to run the demo on your local machine, you can use our docker image. It will download and run sample Spree application on http://localhost:3000
+```shell
+docker run --rm -it -p 3000:3000 spreecommerce/spree:3.6.4
+```
+
+Admin Panel credentials - login: `spree@example.com` / password: `spree123`
+
+
+Getting Started
+----------------------
+
+Add Spree gems to your `Gemfile`:
+
+### Rails 5.2
+
+```ruby
+gem 'spree', '~> 3.7.0.rc1'
+gem 'spree_auth_devise', '~> 3.4'
+gem 'spree_gateway', '~> 3.4'
+```
+
+Rails 5.2 versions come with [ActiveStorage support](https://spreecommerce.org/spree-3-5-and-3-6-with-rails-5-2-ruby-2-5-and-activestorage-support-released/). You can still use Paperclip (see [instructions](https://guides.spreecommerce.org/developer/images.html#paperclip)).
+
+### Rails 5.1
+
+```ruby
+gem 'spree', '~> 3.5.0'
+gem 'spree_auth_devise', '~> 3.4'
+gem 'spree_gateway', '~> 3.4'
+```
+
+### Rails 5.0
+
+```ruby
+gem 'spree', '~> 3.2.7'
+gem 'spree_auth_devise', '~> 3.4'
+gem 'spree_gateway', '~> 3.4'
+```
+
+
+Run `bundle install`
+
+Use the install generators to set up Spree:
+
+```shell
+rails g spree:install --user_class=Spree::User
+rails g spree:auth:install
+rails g spree_gateway:install
+```
+
+Installation options
+----------------------
+
+Alternatively, if you want to use the bleeding edge version of Spree, add this to your Gemfile:
+
+```ruby
+gem 'spree', github: 'spree/spree'
+gem 'spree_auth_devise', github: 'spree/spree_auth_devise'
+gem 'spree_gateway', github: 'spree/spree_gateway'
+```
+
+**Note: The master branch is not guaranteed to ever be in a fully functioning
+state. It is unwise to use this branch in a production system you care deeply
+about.**
+
+By default, the installation generator (`rails g spree:install`) will run
+migrations as well as adding seed and sample data and will copy frontend views
+for easy customization (if spree_frontend available). This can be disabled using
+
+```shell
+rails g spree:install --migrate=false --sample=false --seed=false --copy_views=false
+```
+
+You can always perform any of these steps later by using these commands.
+
+```shell
+bundle exec rake railties:install:migrations
+bundle exec rake db:migrate
+bundle exec rake db:seed
+bundle exec rake spree_sample:load
+```
+
+Browse Store
+----------------------
+
+http://localhost:3000
+
+Browse Admin Interface
+----------------------
+
+http://localhost:3000/admin
+
+If you have `spree_auth_devise` installed, you can generate a new admin user by running `rake spree_auth:admin:create`.
+
+Extensions
+----------------------
+
+Spree Extensions provide additional features not present in the Core system.
+
+
+| Extension | Spree 3.2+ support | Description |
+| --- | --- | --- |
+| [spree_gateway](https://github.com/spree/spree_gateway) | [](https://travis-ci.org/spree/spree_gateway) | Community supported Spree Payment Method Gateways
+| [spree_auth_devise](https://github.com/spree/spree_auth_devise) | [](https://travis-ci.org/spree/spree_auth_devise) | Provides authentication services for Spree, using the Devise gem.
+| [spree_i18n](https://github.com/spree-contrib/spree_i18n) | [](https://travis-ci.org/spree-contrib/spree_i18n) | I18n translation files for Spree Commerce
+| [spree-multi-domain](https://github.com/spree-contrib/spree-multi-domain) | [](https://travis-ci.org/spree-contrib/spree-multi-domain) | Multiple Spree stores on different domains - single unified backed for processing orders
+| [spree_multi_currency](https://github.com/spree-contrib/spree_multi_currency) | [](https://travis-ci.org/spree-contrib/spree_multi_currency) | Provides UI to allow configuring multiple currencies in Spree |
+| [spree_multi_vendor](https://github.com/spree-contrib/spree_multi_vendor) | [](https://travis-ci.org/spree-contrib/spree_multi_vendor) | Spree Multi Vendor Marketplace extension |
+| [spree-mollie-gateway](https://github.com/mollie/spree-mollie-gateway) | [](https://github.com/mollie/spree-mollie-gateway) | Official [Mollie](https://www.mollie.com) payment gateway for Spree Commerce. |
+| [spree_braintree_vzero](https://github.com/spree-contrib/spree_braintree_vzero) | [](https://travis-ci.org/spree-contrib/spree_braintree_vzero) | Official Spree Braintree v.zero + PayPal extension |
+| [spree_address_book](https://github.com/spree-contrib/spree_address_book) | [](https://travis-ci.org/spree-contrib/spree_address_book) | Adds address book for users to Spree |
+| [spree_digital](https://github.com/spree-contrib/spree_digital) | [](https://travis-ci.org/spree-contrib/spree_digital) | A Spree extension to enable downloadable products |
+| [spree_social](https://github.com/spree-contrib/spree_social) |[](https://travis-ci.org/spree-contrib/spree_social) | Building block for spree social networking features (provides authentication and account linkage) |
+| [spree_related_products](https://github.com/spree-contrib/spree_related_products) | [](https://travis-ci.org/spree-contrib/spree_related_products) | Related products extension for Spree
+| [spree_static_content](https://github.com/spree-contrib/spree_static_content) | [](https://travis-ci.org/spree-contrib/spree_static_content) | Manage static pages for Spree |
+| [spree-product-assembly](https://github.com/spree-contrib/spree-product-assembly) | [](https://travis-ci.org/spree-contrib/spree-product-assembly) | Adds oportunity to make bundle of products |
+| [spree_editor](https://github.com/spree-contrib/spree_editor) | [](https://travis-ci.org/spree-contrib/spree_editor) | Rich text editor for Spree with Image and File uploading in-place |
+| [spree_recently_viewed](https://github.com/spree-contrib/spree_recently_viewed) | [](https://travis-ci.org/spree-contrib/spree_recently_viewed) | Recently viewed products in Spree |
+| [spree_wishlist](https://github.com/spree-contrib/spree_wishlist) | [](https://travis-ci.org/spree-contrib/spree_wishlist) | Wishlist extension for Spree |
+| [spree_sitemap](https://github.com/spree-contrib/spree_sitemap) | [](https://travis-ci.org/spree-contrib/spree_sitemap) | Sitemap Generator for Spree |
+| [spree_volume_pricing](https://github.com/spree-contrib/spree_volume_pricing) | [](https://travis-ci.org/spree-contrib/spree_volume_pricing) | It determines the price for a particular product variant with predefined ranges of quantities
+| [better_spree_paypal_express](https://github.com/spree-contrib/better_spree_paypal_express) | [](https://travis-ci.org/spree-contrib/better_spree_paypal_express) | This is the official Paypal Express extension for Spree.
+| [spree_globalize](https://github.com/spree-contrib/spree_globalize) | [](https://travis-ci.org/spree-contrib/spree_globalize) | Adds support for model translations (multi-language stores)
+| [spree_avatax_certified](https://github.com/spree-contrib/spree_avatax_certified) | [](https://travis-ci.org/spree-contrib/spree_avatax_certified) | Improve your Spree store's sales tax decision automation with Avalara AvaTax
+| [spree_analytics_trackers](https://github.com/spree-contrib/spree_analytics_trackers) | [](https://travis-ci.org/spree-contrib/spree_analytics_trackers) | Adds support for Analytics Trackers (Google Analytics & Segment)
+
+Performance
+----------------------
+
+You may notice that your Spree store runs slowly in development environment. This can be because in development each asset (css and javascript) is loaded separately. You can disable it by adding the following line to `config/environments/development.rb`.
+
+```ruby
+config.assets.debug = false
+```
+
+
+Developing Spree
+----------------------
+
+Spree is meant to be run within the context of Rails application and the source code is essentially a collection of gems. You can easily create a sandbox
+application inside of your cloned source directory for testing purposes.
+
+
+Clone the Git repo
+
+```shell
+git clone git://github.com/spree/spree.git
+cd spree
+```
+
+Install the gem dependencies
+
+```shell
+bundle install
+```
+
+### Sandbox
+
+Create a sandbox Rails application for testing purposes which automatically perform all necessary database setup
+
+```shell
+bundle exec rake sandbox
+```
+
+Start the server
+
+```shell
+cd sandbox
+rails server
+```
+
+### Running Tests
+
+We use [CircleCI](https://circleci.com/) to run the tests for Spree.
+
+You can see the build statuses at [https://circleci.com/gh/spree/spree](https://circleci.com/gh/spree/spree).
+
+---
+
+Each gem contains its own series of tests, and for each directory, you need to
+do a quick one-time creation of a test application and then you can use it to run
+the tests. For example, to run the tests for the core project.
+```shell
+cd core
+BUNDLE_GEMFILE=../Gemfile bundle exec rake test_app
+bundle exec rspec spec
+```
+
+If you would like to run specs against a particular database you may specify the
+dummy app's database, which defaults to sqlite3.
+```shell
+DB=postgres bundle exec rake test_app
+```
+
+If you want to run specs for only a single spec file
+```shell
+bundle exec rspec spec/models/spree/state_spec.rb
+```
+
+If you want to run a particular line of spec
+```shell
+bundle exec rspec spec/models/spree/state_spec.rb:7
+```
+
+You can also enable fail fast in order to stop tests at the first failure
+```shell
+FAIL_FAST=true bundle exec rspec spec/models/state_spec.rb
+```
+
+If you want to run the simplecov code coverage report
+```shell
+COVERAGE=true bundle exec rspec spec
+```
+
+If you're working on multiple facets of Spree to test,
+please ensure that you have a postgres user:
+
+```shell
+createuser -s -r postgres
+```
+
+And also ensure that you have [ChromeDriver](http://chromedriver.chromium.org) installed as well.
+Please follow this
+[instruction](https://github.com/SeleniumHQ/selenium/wiki/ChromeDriver) to install it.
+
+To execute all the tests, you may want to run this command at the
+root of the Spree project to generate test applications and run
+specs for all the facets:
+```shell
+bash build.sh
+```
+
+
+Contributing
+----------------------
+
+Spree is an open source project and we encourage contributions. Please review the
+[contributing guidelines](https://github.com/spree/spree/blob/master/.github/CONTRIBUTING.md)
+before contributing.
+
+In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project.
+
+Here are some ways **you** can contribute:
+
+* by using prerelease versions / master branch
+* by reporting [bugs](https://github.com/spree/spree/issues/new)
+* by [translating to a new language](https://github.com/spree/spree_i18n/tree/master/config/locales)
+* by writing or editing [documentation](https://github.com/spree/spree/blob/master/.github/CONTRIBUTING.md)
+* by writing [specs](https://github.com/spree/spree/labels/need_specs)
+* by writing [needed code](https://github.com/spree/spree/labels/feature_request) or [finishing code](https://github.com/spree/spree/labels/address_feedback)
+* by [refactoring code](https://github.com/spree/spree/labels/address_feedback)
+* by reviewing [pull requests](https://github.com/spree/spree/pulls)
+* by verifying [issues](https://github.com/spree/spree/labels/unverified)
+
+License
+----------------------
+
+Spree is released under the [New BSD License](https://github.com/spree/spree/blob/master/license.md).
+
+
+About Spark Solutions
+----------------------
+[][spark]
+
+Spree is maintained by [Spark Solutions Sp. z o.o.][spark].
+
+We are passionate about open source software.
+We are [available for hire][spark].
+
+[spark]:http://sparksolutions.co?utm_source=github
diff --git a/README.txt b/README.txt
deleted file mode 100644
index dc263cd5f70..00000000000
--- a/README.txt
+++ /dev/null
@@ -1,49 +0,0 @@
-SUMMARY
-=======
-
-Spree is a complete open source commerce solution for Ruby on Rails. It was developed by Sean Schofield under the original name of Rails Cart before changing its name to Spree.
-
-QUICK START
-===========
-
-1.) Install spree Gem
-
-$ sudo gem install spree
-
-2.) Create Spree Application
-
-$ spree app_name
-
-3.) Create MySQL Database
-
-mysql> create database spree_dev;
-mysql> grant all privileges on spree_dev.* to 'spree'@'localhost' identified by 'spree';
-mysql> flush privileges;
-
-4.) Migrations
-
-$ cd app_name
-$ rake db:migrate
-
-5.) Bootstrap
-
-Spree requires an admin user and a few other pieces of structural data to be loaded into your database.
-
-rake spree:bootstrap
-
-6.) Sample Data (Optional)
-
-Optionally load some sample data so you have something to look at.
-
-rake spree:sample_data
-
-7.) Launch Application
-
-Browse Store
-
-http://localhost:xxxx/store
-
-Admin Interface (user: admin password: test)
-
-http://localhost:xxxx/admin
-
diff --git a/Rakefile b/Rakefile
index 3bb0e8592a4..6f379944a86 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,10 +1,98 @@
-# Add your own tasks in files placed in lib/tasks ending in .rake,
-# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+require 'rake'
+require 'rubygems/package_task'
+require 'thor/group'
+begin
+ require 'spree/testing_support/common_rake'
+rescue LoadError
+ raise "Could not find spree/testing_support/common_rake. You need to run this command using Bundler."
+end
-require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+SPREE_GEMS = %w(core api cmd backend frontend sample).freeze
-require 'rake'
-require 'rake/testtask'
-require 'rake/rdoctask'
+task default: :test
+
+desc "Runs all tests in all Spree engines"
+task test: :test_app do
+ SPREE_GEMS.each do |gem_name|
+ Dir.chdir("#{File.dirname(__FILE__)}/#{gem_name}") do
+ sh 'rspec'
+ end
+ end
+end
+
+desc "Generates a dummy app for testing for every Spree engine"
+task :test_app do
+ SPREE_GEMS.each do |gem_name|
+ Dir.chdir("#{File.dirname(__FILE__)}/#{gem_name}") do
+ sh 'rake test_app'
+ end
+ end
+end
+
+desc "clean the whole repository by removing all the generated files"
+task :clean do
+ rm_f "Gemfile.lock"
+ rm_rf "sandbox"
+ rm_rf "pkg"
+
+ SPREE_GEMS.each do |gem_name|
+ rm_f "#{gem_name}/Gemfile.lock"
+ rm_rf "#{gem_name}/pkg"
+ rm_rf "#{gem_name}/spec/dummy"
+ end
+end
+
+namespace :gem do
+ def version
+ require 'spree/core/version'
+ Spree.version
+ end
+
+ def for_each_gem
+ SPREE_GEMS.each do |gem_name|
+ yield "pkg/spree_#{gem_name}-#{version}.gem"
+ end
+ yield "pkg/spree-#{version}.gem"
+ end
+
+ desc "Build all spree gems"
+ task :build do
+ pkgdir = File.expand_path("../pkg", __FILE__)
+ FileUtils.mkdir_p pkgdir
+
+ SPREE_GEMS.each do |gem_name|
+ Dir.chdir(gem_name) do
+ sh "gem build spree_#{gem_name}.gemspec"
+ mv "spree_#{gem_name}-#{version}.gem", pkgdir
+ end
+ end
+
+ sh "gem build spree.gemspec"
+ mv "spree-#{version}.gem", pkgdir
+ end
+
+ desc "Install all spree gems"
+ task install: :build do
+ for_each_gem do |gem_path|
+ Bundler.with_clean_env do
+ sh "gem install #{gem_path}"
+ end
+ end
+ end
+
+ desc "Release all gems to rubygems"
+ task release: :build do
+ sh "git tag -a -m \"Version #{version}\" v#{version}"
+
+ for_each_gem do |gem_path|
+ sh "gem push '#{gem_path}'"
+ end
+ end
+end
-require 'tasks/rails'
+desc "Creates a sandbox application for simulating the Spree code in a deployed Rails app"
+task :sandbox do
+ Bundler.with_clean_env do
+ exec("lib/sandbox.sh")
+ end
+end
diff --git a/api/.gitignore b/api/.gitignore
new file mode 100644
index 00000000000..d87d4be66f4
--- /dev/null
+++ b/api/.gitignore
@@ -0,0 +1,17 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
diff --git a/api/Gemfile b/api/Gemfile
new file mode 100644
index 00000000000..f0bccbfd978
--- /dev/null
+++ b/api/Gemfile
@@ -0,0 +1,5 @@
+eval(File.read(File.dirname(__FILE__) + '/../common_spree_dependencies.rb'))
+
+gem 'spree_core', path: '../core'
+
+gemspec
diff --git a/api/LICENSE b/api/LICENSE
new file mode 100644
index 00000000000..3b57c94ed0e
--- /dev/null
+++ b/api/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2007-2015, Spree Commerce, Inc. and other contributors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name Spree nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/api/Rakefile b/api/Rakefile
new file mode 100644
index 00000000000..b9121c35249
--- /dev/null
+++ b/api/Rakefile
@@ -0,0 +1,16 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rspec/core/rake_task'
+require 'spree/testing_support/common_rake'
+require 'rails/all'
+
+RSpec::Core::RakeTask.new
+
+task default: :spec
+
+desc "Generates a dummy app for testing"
+task :test_app do
+ ENV['LIB_NAME'] = 'spree/api'
+ Rake::Task['common:test_app'].invoke
+end
diff --git a/api/app/assets/javascripts/spree/api/main.js b/api/app/assets/javascripts/spree/api/main.js
new file mode 100644
index 00000000000..dc4e5d1bca1
--- /dev/null
+++ b/api/app/assets/javascripts/spree/api/main.js
@@ -0,0 +1,36 @@
+//= require spree
+
+var SpreeAPI = {
+ oauthToken: null, // user Bearer token to authorize operations for the given user
+ orderToken: null // order token to authorize operations on current order (cart)
+}
+
+SpreeAPI.Storefront = {}
+SpreeAPI.Platform = {}
+
+// API routes
+Spree.routes.api_v2_storefront_cart_create = Spree.pathFor('api/v2/storefront/cart')
+Spree.routes.api_v2_storefront_cart_add_item = Spree.pathFor('api/v2/storefront/cart/add_item')
+Spree.routes.api_v2_storefront_cart_apply_coupon_code = Spree.pathFor('api/v2/storefront/cart/apply_coupon_code')
+
+// helpers
+SpreeAPI.handle500error = function () {
+ alert('Internal Server Error')
+}
+
+SpreeAPI.prepareHeaders = function (headers) {
+ if (typeof headers === 'undefined') {
+ headers = {}
+ }
+
+ // if signed in we need to pass the Bearer authorization token
+ // so backend will recognize that actions are authorized in scope of this user
+ if (SpreeAPI.oauthToken) {
+ headers['Authorization'] = 'Bearer ' + SpreeAPI.oauthToken
+ }
+
+ // default headers, required for POST/PATCH/DELETE requests
+ headers['Accept'] = 'application/json'
+ headers['Content-Type'] = 'application/json'
+ return headers
+}
diff --git a/api/app/assets/javascripts/spree/api/storefront/cart.js b/api/app/assets/javascripts/spree/api/storefront/cart.js
new file mode 100644
index 00000000000..8bebe2e706a
--- /dev/null
+++ b/api/app/assets/javascripts/spree/api/storefront/cart.js
@@ -0,0 +1,49 @@
+//= require spree/api/main
+
+SpreeAPI.Storefront.createCart = function (successCallback, failureCallback) {
+ fetch(Spree.routes.api_v2_storefront_cart_create, {
+ method: 'POST',
+ headers: SpreeAPI.prepareHeaders()
+ }).then(function (response) {
+ switch (response.status) {
+ case 422:
+ response.json().then(function (json) { failureCallback(json.error) })
+ break
+ case 500:
+ SpreeAPI.handle500error()
+ break
+ case 201:
+ response.json().then(function (json) {
+ SpreeAPI.orderToken = json.data.attributes.token
+ successCallback()
+ })
+ break
+ }
+ })
+}
+
+SpreeAPI.Storefront.addToCart = function (variantId, quantity, options, successCallback, failureCallback) {
+ fetch(Spree.routes.api_v2_storefront_cart_add_item, {
+ method: 'POST',
+ headers: SpreeAPI.prepareHeaders({ 'X-Spree-Order-Token': SpreeAPI.orderToken }),
+ body: JSON.stringify({
+ variant_id: variantId,
+ quantity: quantity,
+ options: options
+ })
+ }).then(function (response) {
+ switch (response.status) {
+ case 422:
+ response.json().then(function (json) { failureCallback(json.error) })
+ break
+ case 500:
+ SpreeAPI.handle500error()
+ break
+ case 200:
+ response.json().then(function (json) {
+ successCallback(json.data)
+ })
+ break
+ }
+ })
+}
diff --git a/api/app/controllers/concerns/spree/api/v2/storefront/order_concern.rb b/api/app/controllers/concerns/spree/api/v2/storefront/order_concern.rb
new file mode 100644
index 00000000000..3a6b9bfcf8e
--- /dev/null
+++ b/api/app/controllers/concerns/spree/api/v2/storefront/order_concern.rb
@@ -0,0 +1,48 @@
+module Spree
+ module Api
+ module V2
+ module Storefront
+ module OrderConcern
+ private
+
+ def render_order(result)
+ if result.success?
+ render_serialized_payload { serialized_current_order }
+ else
+ render_error_payload(result.error)
+ end
+ end
+
+ def ensure_order
+ raise ActiveRecord::RecordNotFound if spree_current_order.nil?
+ end
+
+ def order_token
+ request.headers['X-Spree-Order-Token'] || params[:order_token]
+ end
+
+ def spree_current_order
+ @spree_current_order ||= find_spree_current_order
+ end
+
+ def find_spree_current_order
+ Spree::Api::Dependencies.storefront_current_order_finder.constantize.new.execute(
+ store: spree_current_store,
+ user: spree_current_user,
+ token: order_token,
+ currency: current_currency
+ )
+ end
+
+ def serialize_order(order)
+ resource_serializer.new(order.reload, include: resource_includes, fields: sparse_fields).serializable_hash
+ end
+
+ def serialized_current_order
+ serialize_order(spree_current_order)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/base_controller.rb b/api/app/controllers/spree/api/base_controller.rb
new file mode 100644
index 00000000000..2d405dadc29
--- /dev/null
+++ b/api/app/controllers/spree/api/base_controller.rb
@@ -0,0 +1,165 @@
+require_dependency 'spree/api/controller_setup'
+
+module Spree
+ module Api
+ class BaseController < ActionController::Base
+ protect_from_forgery unless: -> { request.format.json? || request.format.xml? }
+
+ include Spree::Api::ControllerSetup
+ include Spree::Core::ControllerHelpers::Store
+ include Spree::Core::ControllerHelpers::StrongParameters
+
+ attr_accessor :current_api_user
+
+ before_action :set_content_type
+ before_action :load_user
+ before_action :authorize_for_order, if: proc { order_token.present? }
+ before_action :authenticate_user
+ before_action :load_user_roles
+
+ rescue_from ActionController::ParameterMissing, with: :error_during_processing
+ rescue_from ActiveRecord::RecordInvalid, with: :error_during_processing
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found
+ rescue_from CanCan::AccessDenied, with: :unauthorized
+ rescue_from Spree::Core::GatewayError, with: :gateway_error
+
+ helper Spree::Api::ApiHelpers
+
+ # users should be able to set price when importing orders via api
+ def permitted_line_item_attributes
+ if @current_user_roles.include?('admin')
+ super + [:price, :variant_id, :sku]
+ else
+ super
+ end
+ end
+
+ def content_type
+ case params[:format]
+ when 'json'
+ 'application/json; charset=utf-8'
+ when 'xml'
+ 'text/xml; charset=utf-8'
+ end
+ end
+
+ private
+
+ def set_content_type
+ headers['Content-Type'] = content_type
+ end
+
+ def load_user
+ @current_api_user = Spree.user_class.find_by(spree_api_key: api_key.to_s)
+ end
+
+ def authenticate_user
+ return if @current_api_user
+
+ if requires_authentication? && api_key.blank? && order_token.blank?
+ must_specify_api_key and return
+ elsif order_token.blank? && (requires_authentication? || api_key.present?)
+ invalid_api_key and return
+ else
+ # An anonymous user
+ @current_api_user = Spree.user_class.new
+ end
+ end
+
+ def invalid_api_key
+ render 'spree/api/errors/invalid_api_key', status: 401
+ end
+
+ def must_specify_api_key
+ render 'spree/api/errors/must_specify_api_key', status: 401
+ end
+
+ def load_user_roles
+ @current_user_roles = @current_api_user ? @current_api_user.spree_roles.pluck(:name) : []
+ end
+
+ def unauthorized
+ render 'spree/api/errors/unauthorized', status: 401 and return
+ end
+
+ def error_during_processing(exception)
+ Rails.logger.error exception.message
+ Rails.logger.error exception.backtrace.join("\n")
+
+ unprocessable_entity(exception.message)
+ end
+
+ def unprocessable_entity(message)
+ render plain: { exception: message }.to_json, status: 422
+ end
+
+ def gateway_error(exception)
+ @order.errors.add(:base, exception.message)
+ invalid_resource!(@order)
+ end
+
+ def requires_authentication?
+ Spree::Api::Config[:requires_authentication]
+ end
+
+ def not_found
+ render 'spree/api/errors/not_found', status: 404 and return
+ end
+
+ def current_ability
+ Spree::Dependencies.ability_class.constantize.new(current_api_user)
+ end
+
+ def invalid_resource!(resource)
+ @resource = resource
+ render 'spree/api/errors/invalid_resource', status: 422
+ end
+
+ def api_key
+ request.headers['X-Spree-Token'] || params[:token]
+ end
+ helper_method :api_key
+
+ def order_token
+ request.headers['X-Spree-Order-Token'] || params[:order_token]
+ end
+
+ def find_product(id)
+ @product = product_scope.friendly.distinct(false).find(id.to_s)
+ rescue ActiveRecord::RecordNotFound
+ @product = product_scope.find_by(id: id)
+ not_found unless @product
+ end
+
+ def product_scope
+ if @current_user_roles.include?('admin')
+ scope = Product.with_deleted.accessible_by(current_ability, :read).includes(*product_includes)
+
+ scope = scope.not_deleted unless params[:show_deleted]
+ scope = scope.not_discontinued unless params[:show_discontinued]
+ else
+ scope = Product.accessible_by(current_ability, :read).active.includes(*product_includes)
+ end
+
+ scope
+ end
+
+ def variants_associations
+ [{ option_values: :option_type }, :default_price, :images]
+ end
+
+ def product_includes
+ [:option_types, :taxons, product_properties: :property, variants: variants_associations, master: variants_associations]
+ end
+
+ def order_id
+ params[:order_id] || params[:checkout_id] || params[:order_number]
+ end
+
+ def authorize_for_order
+ @order = Spree::Order.find_by(number: order_id)
+ authorize! :read, @order, order_token
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/errors_controller.rb b/api/app/controllers/spree/api/errors_controller.rb
new file mode 100644
index 00000000000..9105b9fab9e
--- /dev/null
+++ b/api/app/controllers/spree/api/errors_controller.rb
@@ -0,0 +1,9 @@
+module Spree
+ module Api
+ class ErrorsController < ActionController::Base
+ def render_404
+ render 'spree/api/errors/not_found', status: 404
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/addresses_controller.rb b/api/app/controllers/spree/api/v1/addresses_controller.rb
new file mode 100644
index 00000000000..459384ec464
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/addresses_controller.rb
@@ -0,0 +1,46 @@
+module Spree
+ module Api
+ module V1
+ class AddressesController < Spree::Api::BaseController
+ before_action :find_order
+
+ def show
+ authorize! :read, @order, order_token
+ @address = find_address
+ respond_with(@address)
+ end
+
+ def update
+ authorize! :update, @order, order_token
+ @address = find_address
+
+ if @address.update_attributes(address_params)
+ respond_with(@address, default_template: :show)
+ else
+ invalid_resource!(@address)
+ end
+ end
+
+ private
+
+ def address_params
+ params.require(:address).permit(permitted_address_attributes)
+ end
+
+ def find_order
+ @order = Spree::Order.find_by!(number: order_id)
+ end
+
+ def find_address
+ if @order.bill_address_id == params[:id].to_i
+ @order.bill_address
+ elsif @order.ship_address_id == params[:id].to_i
+ @order.ship_address
+ else
+ raise CanCan::AccessDenied
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/checkouts_controller.rb b/api/app/controllers/spree/api/v1/checkouts_controller.rb
new file mode 100644
index 00000000000..a3cb0954697
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/checkouts_controller.rb
@@ -0,0 +1,112 @@
+module Spree
+ module Api
+ module V1
+ class CheckoutsController < Spree::Api::BaseController
+ before_action :associate_user, only: :update
+ before_action :load_order_with_lock, only: [:next, :advance, :update]
+
+ include Spree::Core::ControllerHelpers::Auth
+ include Spree::Core::ControllerHelpers::Order
+ # This before_action comes from Spree::Core::ControllerHelpers::Order
+ skip_before_action :set_current_order
+
+ def next
+ authorize! :update, @order, order_token
+ @order.next!
+ respond_with(@order, default_template: 'spree/api/v1/orders/show', status: 200)
+ rescue StateMachines::InvalidTransition
+ respond_with(@order, default_template: 'spree/api/v1/orders/could_not_transition', status: 422)
+ end
+
+ def advance
+ authorize! :update, @order, order_token
+ while @order.next; end
+ respond_with(@order, default_template: 'spree/api/v1/orders/show', status: 200)
+ end
+
+ def update
+ authorize! :update, @order, order_token
+
+ if @order.update_from_params(params, permitted_checkout_attributes, request.headers.env)
+ if current_api_user.has_spree_role?('admin') && user_id.present?
+ @order.associate_user!(Spree.user_class.find(user_id))
+ end
+
+ log_state_changes if params[:state]
+
+ return if after_update_attributes
+
+ if @order.completed? || @order.next
+ state_callback(:after)
+ respond_with(@order, default_template: 'spree/api/v1/orders/show')
+ else
+ respond_with(@order, default_template: 'spree/api/v1/orders/could_not_transition', status: 422)
+ end
+ else
+ invalid_resource!(@order)
+ end
+ end
+
+ private
+
+ def user_id
+ params[:order][:user_id] if params[:order]
+ end
+
+ # Should be overriden if you have areas of your checkout that don't match
+ # up to a step within checkout_steps, such as a registration step
+ def skip_state_validation?
+ false
+ end
+
+ def load_order(lock = false)
+ @order = Spree::Order.lock(lock).find_by!(number: params[:id])
+ raise_insufficient_quantity and return if @order.insufficient_stock_lines.present?
+ @order.state = params[:state] if params[:state]
+ state_callback(:before)
+ end
+
+ def load_order_with_lock
+ load_order(true)
+ end
+
+ def raise_insufficient_quantity
+ respond_with(@order, default_template: 'spree/api/v1/orders/insufficient_quantity', status: 422)
+ end
+
+ def state_callback(before_or_after = :before)
+ method_name = :"#{before_or_after}_#{@order.state}"
+ send(method_name) if respond_to?(method_name, true)
+ end
+
+ def after_update_attributes
+ if params[:order] && params[:order][:coupon_code].present?
+ handler = PromotionHandler::Coupon.new(@order)
+ handler.apply
+
+ if handler.error.present?
+ @coupon_message = handler.error
+ respond_with(@order, default_template: 'spree/api/v1/orders/could_not_apply_coupon', status: 422)
+ return true
+ end
+ end
+ false
+ end
+
+ def log_state_changes
+ if @order.previous_changes[:state]
+ @order.log_state_changes(
+ state_name: 'order',
+ old_state: @order.previous_changes[:state].first,
+ new_state: @order.previous_changes[:state].last
+ )
+ end
+ end
+
+ def order_id
+ super || params[:id]
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/classifications_controller.rb b/api/app/controllers/spree/api/v1/classifications_controller.rb
new file mode 100644
index 00000000000..4cdc52a315d
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/classifications_controller.rb
@@ -0,0 +1,20 @@
+module Spree
+ module Api
+ module V1
+ class ClassificationsController < Spree::Api::BaseController
+ def update
+ authorize! :update, Product
+ authorize! :update, Taxon
+ classification = Spree::Classification.find_by(
+ product_id: params[:product_id],
+ taxon_id: params[:taxon_id]
+ )
+ # Because position we get back is 0-indexed.
+ # acts_as_list is 1-indexed.
+ classification.insert_at(params[:position].to_i + 1)
+ head :ok
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/countries_controller.rb b/api/app/controllers/spree/api/v1/countries_controller.rb
new file mode 100644
index 00000000000..bbe89599ec8
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/countries_controller.rb
@@ -0,0 +1,22 @@
+module Spree
+ module Api
+ module V1
+ class CountriesController < Spree::Api::BaseController
+ skip_before_action :authenticate_user
+
+ def index
+ @countries = Country.accessible_by(current_ability, :read).ransack(params[:q]).result.
+ order('name ASC').
+ page(params[:page]).per(params[:per_page])
+ country = Country.order('updated_at ASC').last
+ respond_with(@countries) if stale?(country)
+ end
+
+ def show
+ @country = Country.accessible_by(current_ability, :read).find(params[:id])
+ respond_with(@country)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/credit_cards_controller.rb b/api/app/controllers/spree/api/v1/credit_cards_controller.rb
new file mode 100644
index 00000000000..ecc56b2a111
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/credit_cards_controller.rb
@@ -0,0 +1,26 @@
+module Spree
+ module Api
+ module V1
+ class CreditCardsController < Spree::Api::BaseController
+ before_action :user
+
+ def index
+ @credit_cards = user.
+ credit_cards.
+ accessible_by(current_ability, :read).
+ with_payment_profile.
+ ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ respond_with(@credit_cards)
+ end
+
+ private
+
+ def user
+ if params[:user_id].present?
+ @user ||= Spree.user_class.accessible_by(current_ability, :read).find(params[:user_id])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/customer_returns_controller.rb b/api/app/controllers/spree/api/v1/customer_returns_controller.rb
new file mode 100644
index 00000000000..41f11e6fff0
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/customer_returns_controller.rb
@@ -0,0 +1,25 @@
+module Spree
+ module Api
+ module V1
+ class CustomerReturnsController < Spree::Api::BaseController
+ def index
+ collection(Spree::CustomerReturn)
+ respond_with(@collection)
+ end
+
+ private
+
+ def collection(resource)
+ return @collection if @collection.present?
+
+ params[:q] ||= {}
+
+ @collection = resource.all
+ # @search needs to be defined as this is passed to search_form_for
+ @search = @collection.ransack(params[:q])
+ @collection = @search.result.order(created_at: :desc).page(params[:page]).per(params[:per_page])
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/images_controller.rb b/api/app/controllers/spree/api/v1/images_controller.rb
new file mode 100644
index 00000000000..f587f18cc97
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/images_controller.rb
@@ -0,0 +1,58 @@
+module Spree
+ module Api
+ module V1
+ class ImagesController < Spree::Api::BaseController
+ def index
+ @images = scope.images.accessible_by(current_ability, :read)
+ respond_with(@images)
+ end
+
+ def show
+ @image = Image.accessible_by(current_ability, :read).find(params[:id])
+ respond_with(@image)
+ end
+
+ def new; end
+
+ def create
+ authorize! :create, Image
+ @image = scope.images.new(image_params)
+ if @image.save
+ respond_with(@image, status: 201, default_template: :show)
+ else
+ invalid_resource!(@image)
+ end
+ end
+
+ def update
+ @image = scope.images.accessible_by(current_ability, :update).find(params[:id])
+ if @image.update_attributes(image_params)
+ respond_with(@image, default_template: :show)
+ else
+ invalid_resource!(@image)
+ end
+ end
+
+ def destroy
+ @image = scope.images.accessible_by(current_ability, :destroy).find(params[:id])
+ @image.destroy
+ respond_with(@image, status: 204)
+ end
+
+ private
+
+ def image_params
+ params.require(:image).permit(permitted_image_attributes)
+ end
+
+ def scope
+ if params[:product_id]
+ Spree::Product.friendly.find(params[:product_id])
+ elsif params[:variant_id]
+ Spree::Variant.find(params[:variant_id])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/inventory_units_controller.rb b/api/app/controllers/spree/api/v1/inventory_units_controller.rb
new file mode 100644
index 00000000000..4fe13923b98
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/inventory_units_controller.rb
@@ -0,0 +1,54 @@
+module Spree
+ module Api
+ module V1
+ class InventoryUnitsController < Spree::Api::BaseController
+ before_action :prepare_event, only: :update
+
+ def show
+ @inventory_unit = inventory_unit
+ respond_with(@inventory_unit)
+ end
+
+ def update
+ authorize! :update, inventory_unit.order
+
+ inventory_unit.transaction do
+ if inventory_unit.update_attributes(inventory_unit_params)
+ fire
+ render :show, status: 200
+ else
+ invalid_resource!(inventory_unit)
+ end
+ end
+ end
+
+ private
+
+ def inventory_unit
+ @inventory_unit ||= InventoryUnit.accessible_by(current_ability, :read).find(params[:id])
+ end
+
+ def prepare_event
+ return unless @event = params[:fire]
+
+ can_event = "can_#{@event}?"
+
+ unless inventory_unit.respond_to?(can_event) &&
+ inventory_unit.send(can_event)
+ render plain: { exception: "cannot transition to #{@event}" }.to_json,
+ status: 200
+ false
+ end
+ end
+
+ def fire
+ inventory_unit.send("#{@event}!") if @event
+ end
+
+ def inventory_unit_params
+ params.require(:inventory_unit).permit(permitted_inventory_unit_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/line_items_controller.rb b/api/app/controllers/spree/api/v1/line_items_controller.rb
new file mode 100644
index 00000000000..c2941d8f02f
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/line_items_controller.rb
@@ -0,0 +1,70 @@
+module Spree
+ module Api
+ module V1
+ class LineItemsController < Spree::Api::BaseController
+ class_attribute :line_item_options
+
+ self.line_item_options = []
+
+ def new; end
+
+ def create
+ variant = Spree::Variant.find(params[:line_item][:variant_id])
+
+ @line_item = Spree::Dependencies.cart_add_item_service.constantize.call(order: order,
+ variant: variant,
+ quantity: params[:line_item][:quantity],
+ options: line_item_params[:options]).value
+ if @line_item.errors.empty?
+ respond_with(@line_item, status: 201, default_template: :show)
+ else
+ invalid_resource!(@line_item)
+ end
+ end
+
+ def update
+ @line_item = find_line_item
+
+ if Spree::Dependencies.cart_update_service.constantize.call(order: @order, params: line_items_attributes).success?
+ @line_item.reload
+ respond_with(@line_item, default_template: :show)
+ else
+ invalid_resource!(@line_item)
+ end
+ end
+
+ def destroy
+ @line_item = find_line_item
+ Spree::Dependencies.cart_remove_line_item_service.constantize.call(order: @order, line_item: @line_item)
+
+ respond_with(@line_item, status: 204)
+ end
+
+ private
+
+ def order
+ @order ||= Spree::Order.includes(:line_items).find_by!(number: order_id)
+ authorize! :update, @order, order_token
+ end
+
+ def find_line_item
+ id = params[:id].to_i
+ order.line_items.detect { |line_item| line_item.id == id } or
+ raise ActiveRecord::RecordNotFound
+ end
+
+ def line_items_attributes
+ { line_items_attributes: {
+ id: params[:id],
+ quantity: params[:line_item][:quantity],
+ options: line_item_params[:options] || {}
+ } }
+ end
+
+ def line_item_params
+ params.require(:line_item).permit(:quantity, :variant_id, options: line_item_options)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/option_types_controller.rb b/api/app/controllers/spree/api/v1/option_types_controller.rb
new file mode 100644
index 00000000000..384f640f207
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/option_types_controller.rb
@@ -0,0 +1,60 @@
+module Spree
+ module Api
+ module V1
+ class OptionTypesController < Spree::Api::BaseController
+ def index
+ @option_types = if params[:ids]
+ Spree::OptionType.
+ includes(:option_values).
+ accessible_by(current_ability, :read).
+ where(id: params[:ids].split(','))
+ else
+ Spree::OptionType.
+ includes(:option_values).
+ accessible_by(current_ability, :read).
+ load.ransack(params[:q]).result
+ end
+ respond_with(@option_types)
+ end
+
+ def show
+ @option_type = Spree::OptionType.accessible_by(current_ability, :read).find(params[:id])
+ respond_with(@option_type)
+ end
+
+ def new; end
+
+ def create
+ authorize! :create, Spree::OptionType
+ @option_type = Spree::OptionType.new(option_type_params)
+ if @option_type.save
+ render :show, status: 201
+ else
+ invalid_resource!(@option_type)
+ end
+ end
+
+ def update
+ @option_type = Spree::OptionType.accessible_by(current_ability, :update).find(params[:id])
+ if @option_type.update_attributes(option_type_params)
+ render :show
+ else
+ invalid_resource!(@option_type)
+ end
+ end
+
+ def destroy
+ @option_type = Spree::OptionType.accessible_by(current_ability, :destroy).find(params[:id])
+ @option_type.destroy
+ render plain: nil, status: 204
+ end
+
+ private
+
+ def option_type_params
+ params.require(:option_type).permit(permitted_option_type_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/option_values_controller.rb b/api/app/controllers/spree/api/v1/option_values_controller.rb
new file mode 100644
index 00000000000..fb8e8f0ee58
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/option_values_controller.rb
@@ -0,0 +1,62 @@
+module Spree
+ module Api
+ module V1
+ class OptionValuesController < Spree::Api::BaseController
+ def index
+ @option_values = if params[:ids]
+ scope.where(id: params[:ids])
+ else
+ scope.ransack(params[:q]).result.distinct
+ end
+ respond_with(@option_values)
+ end
+
+ def show
+ @option_value = scope.find(params[:id])
+ respond_with(@option_value)
+ end
+
+ def new; end
+
+ def create
+ authorize! :create, Spree::OptionValue
+ @option_value = scope.new(option_value_params)
+ if @option_value.save
+ render :show, status: 201
+ else
+ invalid_resource!(@option_value)
+ end
+ end
+
+ def update
+ @option_value = scope.accessible_by(current_ability, :update).find(params[:id])
+ if @option_value.update_attributes(option_value_params)
+ render :show
+ else
+ invalid_resource!(@option_value)
+ end
+ end
+
+ def destroy
+ @option_value = scope.accessible_by(current_ability, :destroy).find(params[:id])
+ @option_value.destroy
+ render plain: nil, status: 204
+ end
+
+ private
+
+ def scope
+ @scope ||= if params[:option_type_id]
+ Spree::OptionType.find(params[:option_type_id]).option_values.accessible_by(current_ability, :read)
+ else
+ Spree::OptionValue.accessible_by(current_ability, :read).load
+ end
+ end
+
+ def option_value_params
+ params.require(:option_value).permit(permitted_option_value_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/orders_controller.rb b/api/app/controllers/spree/api/v1/orders_controller.rb
new file mode 100644
index 00000000000..88d7e971ef5
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/orders_controller.rb
@@ -0,0 +1,156 @@
+module Spree
+ module Api
+ module V1
+ class OrdersController < Spree::Api::BaseController
+ skip_before_action :authenticate_user, only: :apply_coupon_code
+
+ before_action :find_order, except: [:create, :mine, :current, :index, :update, :remove_coupon_code]
+
+ # Dynamically defines our stores checkout steps to ensure we check authorization on each step.
+ Order.checkout_steps.keys.each do |step|
+ define_method step do
+ find_order
+ authorize! :update, @order, params[:token]
+ end
+ end
+
+ def cancel
+ authorize! :update, @order, params[:token]
+ @order.canceled_by(current_api_user)
+ respond_with(@order, default_template: :show)
+ end
+
+ def approve
+ authorize! :approve, @order, params[:token]
+ @order.approved_by(current_api_user)
+ respond_with(@order, default_template: :show)
+ end
+
+ def create
+ authorize! :create, Spree::Order
+ if can?(:admin, Spree::Order)
+ order_user = if @current_user_roles.include?('admin') && order_params[:user_id]
+ Spree.user_class.find(order_params[:user_id])
+ else
+ current_api_user
+ end
+
+ import_params = if @current_user_roles.include?('admin')
+ params[:order].present? ? params[:order].permit! : {}
+ else
+ order_params
+ end
+
+ @order = Spree::Core::Importer::Order.import(order_user, import_params)
+
+ respond_with(@order, default_template: :show, status: 201)
+ else
+ @order = Spree::Order.create!(user: current_api_user, store: current_store)
+ if Cart::Update.call(order: @order, params: order_params).success?
+ respond_with(@order, default_template: :show, status: 201)
+ else
+ invalid_resource!(@order)
+ end
+ end
+ end
+
+ def empty
+ authorize! :update, @order, order_token
+ @order.empty!
+ render plain: nil, status: 204
+ end
+
+ def index
+ authorize! :index, Order
+ @orders = Order.ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ respond_with(@orders)
+ end
+
+ def show
+ authorize! :show, @order, order_token
+ respond_with(@order)
+ end
+
+ def update
+ find_order(true)
+ authorize! :update, @order, order_token
+
+ if Cart::Update.call(order: @order, params: order_params).success?
+ user_id = params[:order][:user_id]
+ if current_api_user.has_spree_role?('admin') && user_id
+ @order.associate_user!(Spree.user_class.find(user_id))
+ end
+ respond_with(@order, default_template: :show)
+ else
+ invalid_resource!(@order)
+ end
+ end
+
+ def current
+ @order = find_current_order
+ if @order
+ respond_with(@order, default_template: :show, locals: { root_object: @order })
+ else
+ head :no_content
+ end
+ end
+
+ def mine
+ if current_api_user.persisted?
+ @orders = current_api_user.orders.reverse_chronological.ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ else
+ render 'spree/api/errors/unauthorized', status: :unauthorized
+ end
+ end
+
+ def apply_coupon_code
+ find_order
+ authorize! :update, @order, order_token
+ @order.coupon_code = params[:coupon_code]
+ @handler = PromotionHandler::Coupon.new(@order).apply
+ status = @handler.successful? ? 200 : 422
+ render 'spree/api/v1/promotions/handler', status: status
+ end
+
+ def remove_coupon_code
+ find_order(true)
+ authorize! :update, @order, order_token
+ @handler = Spree::PromotionHandler::Coupon.new(@order).remove(params[:coupon_code])
+ status = @handler.successful? ? 200 : 404
+ render 'spree/api/v1/promotions/handler', status: status
+ end
+
+ private
+
+ def order_params
+ if params[:order]
+ normalize_params
+ params.require(:order).permit(permitted_order_attributes)
+ else
+ {}
+ end
+ end
+
+ def normalize_params
+ params[:order][:payments_attributes] = params[:order].delete(:payments) if params[:order][:payments]
+ params[:order][:shipments_attributes] = params[:order].delete(:shipments) if params[:order][:shipments]
+ params[:order][:line_items_attributes] = params[:order].delete(:line_items) if params[:order][:line_items]
+ params[:order][:ship_address_attributes] = params[:order].delete(:ship_address) if params[:order][:ship_address]
+ params[:order][:bill_address_attributes] = params[:order].delete(:bill_address) if params[:order][:bill_address]
+ end
+
+ def find_order(lock = false)
+ @order = Spree::Order.lock(lock).find_by!(number: params[:id])
+ end
+
+ def find_current_order
+ current_api_user ? current_api_user.orders.incomplete.order(:created_at).last : nil
+ end
+
+ def order_id
+ super || params[:id]
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/payments_controller.rb b/api/app/controllers/spree/api/v1/payments_controller.rb
new file mode 100644
index 00000000000..6106de94fe5
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/payments_controller.rb
@@ -0,0 +1,82 @@
+module Spree
+ module Api
+ module V1
+ class PaymentsController < Spree::Api::BaseController
+ before_action :find_order
+ before_action :find_payment, only: [:update, :show, :authorize, :purchase, :capture, :void]
+
+ def index
+ @payments = @order.payments.ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ respond_with(@payments)
+ end
+
+ def new
+ @payment_methods = Spree::PaymentMethod.available
+ respond_with(@payment_methods)
+ end
+
+ def create
+ @order.validate_payments_attributes([payment_params])
+ @payment = @order.payments.build(payment_params)
+ if @payment.save
+ respond_with(@payment, status: 201, default_template: :show)
+ else
+ invalid_resource!(@payment)
+ end
+ end
+
+ def update
+ authorize! params[:action], @payment
+ if !@payment.editable?
+ render 'update_forbidden', status: 403
+ elsif @payment.update_attributes(payment_params)
+ respond_with(@payment, default_template: :show)
+ else
+ invalid_resource!(@payment)
+ end
+ end
+
+ def show
+ respond_with(@payment)
+ end
+
+ def authorize
+ perform_payment_action(:authorize)
+ end
+
+ def capture
+ perform_payment_action(:capture)
+ end
+
+ def purchase
+ perform_payment_action(:purchase)
+ end
+
+ def void
+ perform_payment_action(:void_transaction)
+ end
+
+ private
+
+ def find_order
+ @order = Spree::Order.find_by!(number: order_id)
+ authorize! :read, @order, order_token
+ end
+
+ def find_payment
+ @payment = @order.payments.find_by!(number: params[:id])
+ end
+
+ def perform_payment_action(action, *args)
+ authorize! action, Payment
+ @payment.send("#{action}!", *args)
+ respond_with(@payment, default_template: :show)
+ end
+
+ def payment_params
+ params.require(:payment).permit(permitted_payment_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/product_properties_controller.rb b/api/app/controllers/spree/api/v1/product_properties_controller.rb
new file mode 100644
index 00000000000..6f012e96da1
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/product_properties_controller.rb
@@ -0,0 +1,73 @@
+module Spree
+ module Api
+ module V1
+ class ProductPropertiesController < Spree::Api::BaseController
+ before_action :find_product, :authorize_product!
+ before_action :product_property, only: [:show, :update, :destroy]
+
+ def index
+ @product_properties = @product.product_properties.accessible_by(current_ability, :read).
+ ransack(params[:q]).result.
+ page(params[:page]).per(params[:per_page])
+ respond_with(@product_properties)
+ end
+
+ def show
+ respond_with(@product_property)
+ end
+
+ def new; end
+
+ def create
+ authorize! :create, ProductProperty
+ @product_property = @product.product_properties.new(product_property_params)
+ if @product_property.save
+ respond_with(@product_property, status: 201, default_template: :show)
+ else
+ invalid_resource!(@product_property)
+ end
+ end
+
+ def update
+ authorize! :update, @product_property
+
+ if @product_property.update_attributes(product_property_params)
+ respond_with(@product_property, status: 200, default_template: :show)
+ else
+ invalid_resource!(@product_property)
+ end
+ end
+
+ def destroy
+ authorize! :destroy, @product_property
+ @product_property.destroy
+ respond_with(@product_property, status: 204)
+ end
+
+ private
+
+ def find_product
+ super(params[:product_id])
+ end
+
+ def authorize_product!
+ authorize! :read, @product
+ end
+
+ def product_property
+ if @product
+ @product_property ||= @product.product_properties.find_by(id: params[:id])
+ @product_property ||= @product.product_properties.includes(:property).where(spree_properties: { name: params[:id] }).first
+ raise ActiveRecord::RecordNotFound unless @product_property
+
+ authorize! :read, @product_property
+ end
+ end
+
+ def product_property_params
+ params.require(:product_property).permit(permitted_product_properties_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/products_controller.rb b/api/app/controllers/spree/api/v1/products_controller.rb
new file mode 100644
index 00000000000..cdee655e9a1
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/products_controller.rb
@@ -0,0 +1,131 @@
+module Spree
+ module Api
+ module V1
+ class ProductsController < Spree::Api::BaseController
+ before_action :find_product, only: [:update, :show, :destroy]
+
+ def index
+ @products = if params[:ids]
+ product_scope.where(id: params[:ids].split(',').flatten)
+ else
+ product_scope.ransack(params[:q]).result
+ end
+
+ @products = @products.distinct.page(params[:page]).per(params[:per_page])
+ expires_in 15.minutes, public: true
+ headers['Surrogate-Control'] = "max-age=#{15.minutes}"
+ respond_with(@products)
+ end
+
+ def show
+ expires_in 15.minutes, public: true
+ headers['Surrogate-Control'] = "max-age=#{15.minutes}"
+ headers['Surrogate-Key'] = 'product_id=1'
+ respond_with(@product)
+ end
+
+ # Takes besides the products attributes either an array of variants or
+ # an array of option types.
+ #
+ # By submitting an array of variants the option types will be created
+ # using the *name* key in options hash. e.g
+ #
+ # product: {
+ # ...
+ # variants: {
+ # price: 19.99,
+ # sku: "hey_you",
+ # options: [
+ # { name: "size", value: "small" },
+ # { name: "color", value: "black" }
+ # ]
+ # }
+ # }
+ #
+ # Or just pass in the option types hash:
+ #
+ # product: {
+ # ...
+ # option_types: ['size', 'color']
+ # }
+ #
+ # By passing the shipping category name you can fetch or create that
+ # shipping category on the fly. e.g.
+ #
+ # product: {
+ # ...
+ # shipping_category: "Free Shipping Items"
+ # }
+ #
+ def new; end
+
+ def create
+ authorize! :create, Product
+ params[:product][:available_on] ||= Time.current
+ set_up_shipping_category
+
+ options = { variants_attrs: variants_params, options_attrs: option_types_params }
+ @product = Core::Importer::Product.new(nil, product_params, options).create
+
+ if @product.persisted?
+ respond_with(@product, status: 201, default_template: :show)
+ else
+ invalid_resource!(@product)
+ end
+ end
+
+ def update
+ authorize! :update, @product
+
+ options = { variants_attrs: variants_params, options_attrs: option_types_params }
+ @product = Core::Importer::Product.new(@product, product_params, options).update
+
+ if @product.errors.empty?
+ respond_with(@product.reload, status: 200, default_template: :show)
+ else
+ invalid_resource!(@product)
+ end
+ end
+
+ def destroy
+ authorize! :destroy, @product
+ @product.destroy
+ respond_with(@product, status: 204)
+ end
+
+ private
+
+ def product_params
+ params.require(:product).permit(permitted_product_attributes)
+ end
+
+ def variants_params
+ variants_key = if params[:product].key? :variants
+ :variants
+ else
+ :variants_attributes
+ end
+
+ params.require(:product).permit(
+ variants_key => [permitted_variant_attributes, :id]
+ ).delete(variants_key) || []
+ end
+
+ def option_types_params
+ params[:product].fetch(:option_types, [])
+ end
+
+ def find_product
+ super(params[:id])
+ end
+
+ def set_up_shipping_category
+ if shipping_category = params[:product].delete(:shipping_category)
+ id = ShippingCategory.find_or_create_by(name: shipping_category).id
+ params[:product][:shipping_category_id] = id
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/promotions_controller.rb b/api/app/controllers/spree/api/v1/promotions_controller.rb
new file mode 100644
index 00000000000..92eea86979e
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/promotions_controller.rb
@@ -0,0 +1,30 @@
+module Spree
+ module Api
+ module V1
+ class PromotionsController < Spree::Api::BaseController
+ before_action :requires_admin
+ before_action :load_promotion
+
+ def show
+ if @promotion
+ respond_with(@promotion, default_template: :show)
+ else
+ raise ActiveRecord::RecordNotFound
+ end
+ end
+
+ private
+
+ def requires_admin
+ return if @current_user_roles.include?('admin')
+
+ unauthorized and return
+ end
+
+ def load_promotion
+ @promotion = Spree::Promotion.find_by(id: params[:id]) || Spree::Promotion.with_coupon_code(params[:id])
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/properties_controller.rb b/api/app/controllers/spree/api/v1/properties_controller.rb
new file mode 100644
index 00000000000..cb2c9240fa2
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/properties_controller.rb
@@ -0,0 +1,70 @@
+module Spree
+ module Api
+ module V1
+ class PropertiesController < Spree::Api::BaseController
+ before_action :find_property, only: [:show, :update, :destroy]
+
+ def index
+ @properties = Spree::Property.accessible_by(current_ability, :read)
+
+ @properties = if params[:ids]
+ @properties.where(id: params[:ids].split(',').flatten)
+ else
+ @properties.ransack(params[:q]).result
+ end
+
+ @properties = @properties.page(params[:page]).per(params[:per_page])
+ respond_with(@properties)
+ end
+
+ def show
+ respond_with(@property)
+ end
+
+ def new; end
+
+ def create
+ authorize! :create, Property
+ @property = Spree::Property.new(property_params)
+ if @property.save
+ respond_with(@property, status: 201, default_template: :show)
+ else
+ invalid_resource!(@property)
+ end
+ end
+
+ def update
+ if @property
+ authorize! :update, @property
+ @property.update_attributes(property_params)
+ respond_with(@property, status: 200, default_template: :show)
+ else
+ invalid_resource!(@property)
+ end
+ end
+
+ def destroy
+ if @property
+ authorize! :destroy, @property
+ @property.destroy
+ respond_with(@property, status: 204)
+ else
+ invalid_resource!(@property)
+ end
+ end
+
+ private
+
+ def find_property
+ @property = Spree::Property.accessible_by(current_ability, :read).find(params[:id])
+ rescue ActiveRecord::RecordNotFound
+ @property = Spree::Property.accessible_by(current_ability, :read).find_by!(name: params[:id])
+ end
+
+ def property_params
+ params.require(:property).permit(permitted_property_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/reimbursements_controller.rb b/api/app/controllers/spree/api/v1/reimbursements_controller.rb
new file mode 100644
index 00000000000..09685edd189
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/reimbursements_controller.rb
@@ -0,0 +1,25 @@
+module Spree
+ module Api
+ module V1
+ class ReimbursementsController < Spree::Api::BaseController
+ def index
+ collection(Spree::Reimbursement)
+ respond_with(@collection)
+ end
+
+ private
+
+ def collection(resource)
+ return @collection if @collection.present?
+
+ params[:q] ||= {}
+
+ @collection = resource.all
+ # @search needs to be defined as this is passed to search_form_for
+ @search = @collection.ransack(params[:q])
+ @collection = @search.result.order(created_at: :desc).page(params[:page]).per(params[:per_page])
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/return_authorizations_controller.rb b/api/app/controllers/spree/api/v1/return_authorizations_controller.rb
new file mode 100644
index 00000000000..21cc75d808a
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/return_authorizations_controller.rb
@@ -0,0 +1,70 @@
+module Spree
+ module Api
+ module V1
+ class ReturnAuthorizationsController < Spree::Api::BaseController
+ def create
+ authorize! :create, ReturnAuthorization
+ @return_authorization = order.return_authorizations.build(return_authorization_params)
+ if @return_authorization.save
+ respond_with(@return_authorization, status: 201, default_template: :show)
+ else
+ invalid_resource!(@return_authorization)
+ end
+ end
+
+ def destroy
+ @return_authorization = order.return_authorizations.accessible_by(current_ability, :destroy).find(params[:id])
+ @return_authorization.destroy
+ respond_with(@return_authorization, status: 204)
+ end
+
+ def index
+ authorize! :admin, ReturnAuthorization
+ @return_authorizations = order.return_authorizations.accessible_by(current_ability, :read).
+ ransack(params[:q]).result.
+ page(params[:page]).per(params[:per_page])
+ respond_with(@return_authorizations)
+ end
+
+ def new
+ authorize! :admin, ReturnAuthorization
+ end
+
+ def show
+ authorize! :admin, ReturnAuthorization
+ @return_authorization = order.return_authorizations.accessible_by(current_ability, :read).find(params[:id])
+ respond_with(@return_authorization)
+ end
+
+ def update
+ @return_authorization = order.return_authorizations.accessible_by(current_ability, :update).find(params[:id])
+ if @return_authorization.update_attributes(return_authorization_params)
+ respond_with(@return_authorization, default_template: :show)
+ else
+ invalid_resource!(@return_authorization)
+ end
+ end
+
+ def cancel
+ @return_authorization = order.return_authorizations.accessible_by(current_ability, :update).find(params[:id])
+ if @return_authorization.cancel
+ respond_with @return_authorization, default_template: :show
+ else
+ invalid_resource!(@return_authorization)
+ end
+ end
+
+ private
+
+ def order
+ @order ||= Spree::Order.find_by!(number: order_id)
+ authorize! :read, @order
+ end
+
+ def return_authorization_params
+ params.require(:return_authorization).permit(permitted_return_authorization_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/shipments_controller.rb b/api/app/controllers/spree/api/v1/shipments_controller.rb
new file mode 100644
index 00000000000..15542933ce6
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/shipments_controller.rb
@@ -0,0 +1,186 @@
+module Spree
+ module Api
+ module V1
+ class ShipmentsController < Spree::Api::BaseController
+ before_action :find_and_update_shipment, only: [:ship, :ready, :add, :remove]
+ before_action :load_transfer_params, only: [:transfer_to_location, :transfer_to_shipment]
+
+ def mine
+ if current_api_user.persisted?
+ @shipments = Spree::Shipment.
+ reverse_chronological.
+ joins(:order).
+ where(spree_orders: { user_id: current_api_user.id }).
+ includes(mine_includes).
+ ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ else
+ render 'spree/api/errors/unauthorized', status: :unauthorized
+ end
+ end
+
+ def create
+ @order = Spree::Order.find_by!(number: params.fetch(:shipment).fetch(:order_id))
+ authorize! :read, @order
+ authorize! :create, Shipment
+ quantity = params[:quantity].to_i
+ @shipment = @order.shipments.create(stock_location_id: params.fetch(:stock_location_id))
+
+ @line_item = Spree::Dependencies.cart_add_item_service.constantize.call(order: @order,
+ variant: variant,
+ quantity: quantity,
+ options: { shipment: @shipment }).value
+
+ respond_with(@shipment.reload, default_template: :show)
+ end
+
+ def update
+ @shipment = Spree::Shipment.accessible_by(current_ability, :update).readonly(false).find_by!(number: params[:id])
+ @shipment.update_attributes_and_order(shipment_params)
+
+ respond_with(@shipment.reload, default_template: :show)
+ end
+
+ def ready
+ unless @shipment.ready?
+ if @shipment.can_ready?
+ @shipment.ready!
+ else
+ render 'spree/api/v1/shipments/cannot_ready_shipment', status: 422 and return
+ end
+ end
+ respond_with(@shipment, default_template: :show)
+ end
+
+ def ship
+ @shipment.ship! unless @shipment.shipped?
+ respond_with(@shipment, default_template: :show)
+ end
+
+ def add
+ quantity = params[:quantity].to_i
+
+ Spree::Dependencies.cart_add_item_service.constantize.call(order: @shipment.order,
+ variant: variant,
+ quantity: quantity,
+ options: { shipment: @shipment })
+
+ respond_with(@shipment, default_template: :show)
+ end
+
+ def remove
+ quantity = if params.key?(:quantity)
+ params[:quantity].to_i
+ else
+ @shipment.inventory_units_for(variant).sum(:quantity)
+ end
+
+ Spree::Dependencies.cart_remove_item_service.constantize.call(order: @shipment.order,
+ variant: variant,
+ quantity: quantity,
+ options: { shipment: @shipment })
+
+ if @shipment.inventory_units.any?
+ @shipment.reload
+ else
+ @shipment.destroy!
+ end
+
+ respond_with(@shipment, default_template: :show)
+ end
+
+ def transfer_to_location
+ @stock_location = Spree::StockLocation.find(params[:stock_location_id])
+
+ unless @quantity > 0
+ unprocessable_entity("#{Spree.t(:shipment_transfer_errors_occured, scope: 'api')} \n #{Spree.t(:negative_quantity, scope: 'api')}")
+ return
+ end
+
+ @original_shipment.transfer_to_location(@variant, @quantity, @stock_location)
+ render json: { success: true, message: Spree.t(:shipment_transfer_success) }, status: 201
+ end
+
+ def transfer_to_shipment
+ @target_shipment = Spree::Shipment.find_by!(number: params[:target_shipment_number])
+
+ error =
+ if @quantity < 0 && @target_shipment == @original_shipment
+ "#{Spree.t(:negative_quantity, scope: 'api')}, \n#{Spree.t('wrong_shipment_target', scope: 'api')}"
+ elsif @target_shipment == @original_shipment
+ Spree.t(:wrong_shipment_target, scope: 'api')
+ elsif @quantity < 0
+ Spree.t(:negative_quantity, scope: 'api')
+ end
+
+ if error
+ unprocessable_entity("#{Spree.t(:shipment_transfer_errors_occured, scope: 'api')} \n#{error}")
+ else
+ @original_shipment.transfer_to_shipment(@variant, @quantity, @target_shipment)
+ render json: { success: true, message: Spree.t(:shipment_transfer_success) }, status: 201
+ end
+ end
+
+ private
+
+ def load_transfer_params
+ @original_shipment = Spree::Shipment.find_by!(number: params[:original_shipment_number])
+ @variant = Spree::Variant.find(params[:variant_id])
+ @quantity = params[:quantity].to_i
+ authorize! :read, @original_shipment
+ authorize! :create, Shipment
+ end
+
+ def find_and_update_shipment
+ @shipment = Spree::Shipment.accessible_by(current_ability, :update).readonly(false).find_by!(number: params[:id])
+ @shipment.update_attributes(shipment_params)
+ @shipment.reload
+ end
+
+ def shipment_params
+ if params[:shipment] && !params[:shipment].empty?
+ params.require(:shipment).permit(permitted_shipment_attributes)
+ else
+ {}
+ end
+ end
+
+ def variant
+ @variant ||= Spree::Variant.unscoped.find(params.fetch(:variant_id))
+ end
+
+ def mine_includes
+ {
+ order: {
+ bill_address: {
+ state: {},
+ country: {}
+ },
+ ship_address: {
+ state: {},
+ country: {}
+ },
+ adjustments: {},
+ payments: {
+ order: {},
+ payment_method: {}
+ }
+ },
+ inventory_units: {
+ line_item: {
+ product: {},
+ variant: {}
+ },
+ variant: {
+ product: {},
+ default_price: {},
+ option_values: {
+ option_type: {}
+ }
+ }
+ }
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/states_controller.rb b/api/app/controllers/spree/api/v1/states_controller.rb
new file mode 100644
index 00000000000..b006335d516
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/states_controller.rb
@@ -0,0 +1,36 @@
+module Spree
+ module Api
+ module V1
+ class StatesController < Spree::Api::BaseController
+ skip_before_action :authenticate_user
+
+ def index
+ @states = scope.ransack(params[:q]).result.includes(:country)
+
+ if params[:page] || params[:per_page]
+ @states = @states.page(params[:page]).per(params[:per_page])
+ end
+
+ state = @states.last
+ respond_with(@states) if stale?(state)
+ end
+
+ def show
+ @state = scope.find(params[:id])
+ respond_with(@state)
+ end
+
+ private
+
+ def scope
+ if params[:country_id]
+ @country = Country.accessible_by(current_ability, :read).find(params[:country_id])
+ @country.states.accessible_by(current_ability, :read).order('name ASC')
+ else
+ State.accessible_by(current_ability, :read).order('name ASC')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/stock_items_controller.rb b/api/app/controllers/spree/api/v1/stock_items_controller.rb
new file mode 100644
index 00000000000..355fabf4500
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/stock_items_controller.rb
@@ -0,0 +1,82 @@
+module Spree
+ module Api
+ module V1
+ class StockItemsController < Spree::Api::BaseController
+ before_action :stock_location, except: [:update, :destroy]
+
+ def index
+ @stock_items = scope.ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ respond_with(@stock_items)
+ end
+
+ def show
+ @stock_item = scope.find(params[:id])
+ respond_with(@stock_item)
+ end
+
+ def create
+ authorize! :create, StockItem
+
+ count_on_hand = 0
+ if params[:stock_item].key?(:count_on_hand)
+ count_on_hand = params[:stock_item][:count_on_hand].to_i
+ end
+
+ @stock_item = scope.new(stock_item_params)
+ if @stock_item.save
+ @stock_item.adjust_count_on_hand(count_on_hand)
+ respond_with(@stock_item, status: 201, default_template: :show)
+ else
+ invalid_resource!(@stock_item)
+ end
+ end
+
+ def update
+ @stock_item = StockItem.accessible_by(current_ability, :update).find(params[:id])
+
+ if params[:stock_item].key?(:backorderable)
+ @stock_item.backorderable = params[:stock_item][:backorderable]
+ @stock_item.save
+ end
+
+ count_on_hand = 0
+ if params[:stock_item].key?(:count_on_hand)
+ count_on_hand = params[:stock_item][:count_on_hand].to_i
+ params[:stock_item].delete(:count_on_hand)
+ end
+
+ updated = params[:stock_item][:force] ? @stock_item.set_count_on_hand(count_on_hand)
+ : @stock_item.adjust_count_on_hand(count_on_hand)
+
+ if updated
+ respond_with(@stock_item, status: 200, default_template: :show)
+ else
+ invalid_resource!(@stock_item)
+ end
+ end
+
+ def destroy
+ @stock_item = StockItem.accessible_by(current_ability, :destroy).find(params[:id])
+ @stock_item.destroy
+ respond_with(@stock_item, status: 204)
+ end
+
+ private
+
+ def stock_location
+ render 'spree/api/v1/shared/stock_location_required', status: 422 and return unless params[:stock_location_id]
+ @stock_location ||= StockLocation.accessible_by(current_ability, :read).find(params[:stock_location_id])
+ end
+
+ def scope
+ includes = { variant: [{ option_values: :option_type }, :product] }
+ @stock_location.stock_items.accessible_by(current_ability, :read).includes(includes)
+ end
+
+ def stock_item_params
+ params.require(:stock_item).permit(permitted_stock_item_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/stock_locations_controller.rb b/api/app/controllers/spree/api/v1/stock_locations_controller.rb
new file mode 100644
index 00000000000..18481955d88
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/stock_locations_controller.rb
@@ -0,0 +1,52 @@
+module Spree
+ module Api
+ module V1
+ class StockLocationsController < Spree::Api::BaseController
+ def index
+ authorize! :read, StockLocation
+ @stock_locations = StockLocation.accessible_by(current_ability, :read).order('name ASC').ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ respond_with(@stock_locations)
+ end
+
+ def show
+ respond_with(stock_location)
+ end
+
+ def create
+ authorize! :create, StockLocation
+ @stock_location = StockLocation.new(stock_location_params)
+ if @stock_location.save
+ respond_with(@stock_location, status: 201, default_template: :show)
+ else
+ invalid_resource!(@stock_location)
+ end
+ end
+
+ def update
+ authorize! :update, stock_location
+ if stock_location.update_attributes(stock_location_params)
+ respond_with(stock_location, status: 200, default_template: :show)
+ else
+ invalid_resource!(stock_location)
+ end
+ end
+
+ def destroy
+ authorize! :destroy, stock_location
+ stock_location.destroy
+ respond_with(stock_location, status: 204)
+ end
+
+ private
+
+ def stock_location
+ @stock_location ||= StockLocation.accessible_by(current_ability, :read).find(params[:id])
+ end
+
+ def stock_location_params
+ params.require(:stock_location).permit(permitted_stock_location_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/stock_movements_controller.rb b/api/app/controllers/spree/api/v1/stock_movements_controller.rb
new file mode 100644
index 00000000000..c60efff28be
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/stock_movements_controller.rb
@@ -0,0 +1,45 @@
+module Spree
+ module Api
+ module V1
+ class StockMovementsController < Spree::Api::BaseController
+ before_action :stock_location, except: [:update, :destroy]
+
+ def index
+ authorize! :read, StockMovement
+ @stock_movements = scope.ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ respond_with(@stock_movements)
+ end
+
+ def show
+ @stock_movement = scope.find(params[:id])
+ respond_with(@stock_movement)
+ end
+
+ def create
+ authorize! :create, StockMovement
+ @stock_movement = scope.new(stock_movement_params)
+ if @stock_movement.save
+ respond_with(@stock_movement, status: 201, default_template: :show)
+ else
+ invalid_resource!(@stock_movement)
+ end
+ end
+
+ private
+
+ def stock_location
+ render 'spree/api/v1/shared/stock_location_required', status: 422 and return unless params[:stock_location_id]
+ @stock_location ||= StockLocation.accessible_by(current_ability, :read).find(params[:stock_location_id])
+ end
+
+ def scope
+ @stock_location.stock_movements.accessible_by(current_ability, :read)
+ end
+
+ def stock_movement_params
+ params.require(:stock_movement).permit(permitted_stock_movement_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/stores_controller.rb b/api/app/controllers/spree/api/v1/stores_controller.rb
new file mode 100644
index 00000000000..4b5f36d3426
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/stores_controller.rb
@@ -0,0 +1,56 @@
+module Spree
+ module Api
+ module V1
+ class StoresController < Spree::Api::BaseController
+ before_action :get_store, except: [:index, :create]
+
+ def index
+ authorize! :read, Store
+ @stores = Store.accessible_by(current_ability, :read).all
+ respond_with(@stores)
+ end
+
+ def create
+ authorize! :create, Store
+ @store = Store.new(store_params)
+ @store.code = params[:store][:code]
+ if @store.save
+ respond_with(@store, status: 201, default_template: :show)
+ else
+ invalid_resource!(@store)
+ end
+ end
+
+ def update
+ authorize! :update, @store
+ if @store.update_attributes(store_params)
+ respond_with(@store, status: 200, default_template: :show)
+ else
+ invalid_resource!(@store)
+ end
+ end
+
+ def show
+ authorize! :read, @store
+ respond_with(@store)
+ end
+
+ def destroy
+ authorize! :destroy, @store
+ @store.destroy
+ respond_with(@store, status: 204)
+ end
+
+ private
+
+ def get_store
+ @store = Store.find(params[:id])
+ end
+
+ def store_params
+ params.require(:store).permit(permitted_store_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/tags_controller.rb b/api/app/controllers/spree/api/v1/tags_controller.rb
new file mode 100644
index 00000000000..04ac12921dd
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/tags_controller.rb
@@ -0,0 +1,28 @@
+module Spree
+ module Api
+ module V1
+ class TagsController < Spree::Api::BaseController
+ def index
+ @tags =
+ if params[:ids]
+ Tag.where(id: params[:ids].split(',').flatten)
+ else
+ Tag.ransack(params[:q]).result
+ end
+
+ @tags = @tags.page(params[:page]).per(params[:per_page])
+
+ expires_in 15.minutes, public: true
+ headers['Surrogate-Control'] = "max-age=#{15.minutes}"
+ respond_with(@tags)
+ end
+
+ private
+
+ def tags_params
+ params.require(:tag).permit(permitted_tags_attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/taxonomies_controller.rb b/api/app/controllers/spree/api/v1/taxonomies_controller.rb
new file mode 100644
index 00000000000..8f86482570a
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/taxonomies_controller.rb
@@ -0,0 +1,67 @@
+module Spree
+ module Api
+ module V1
+ class TaxonomiesController < Spree::Api::BaseController
+ def index
+ respond_with(taxonomies)
+ end
+
+ def show
+ respond_with(taxonomy)
+ end
+
+ # Because JSTree wants parameters in a *slightly* different format
+ def jstree
+ show
+ end
+
+ def new; end
+
+ def create
+ authorize! :create, Taxonomy
+ @taxonomy = Taxonomy.new(taxonomy_params)
+ if @taxonomy.save
+ respond_with(@taxonomy, status: 201, default_template: :show)
+ else
+ invalid_resource!(@taxonomy)
+ end
+ end
+
+ def update
+ authorize! :update, taxonomy
+ if taxonomy.update_attributes(taxonomy_params)
+ respond_with(taxonomy, status: 200, default_template: :show)
+ else
+ invalid_resource!(taxonomy)
+ end
+ end
+
+ def destroy
+ authorize! :destroy, taxonomy
+ taxonomy.destroy
+ respond_with(taxonomy, status: 204)
+ end
+
+ private
+
+ def taxonomies
+ @taxonomies = Taxonomy.accessible_by(current_ability, :read).order('name').includes(root: :children).
+ ransack(params[:q]).result.
+ page(params[:page]).per(params[:per_page])
+ end
+
+ def taxonomy
+ @taxonomy ||= Taxonomy.accessible_by(current_ability, :read).find(params[:id])
+ end
+
+ def taxonomy_params
+ if params[:taxonomy] && !params[:taxonomy].empty?
+ params.require(:taxonomy).permit(permitted_taxonomy_attributes)
+ else
+ {}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/taxons_controller.rb b/api/app/controllers/spree/api/v1/taxons_controller.rb
new file mode 100644
index 00000000000..1d359fe101d
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/taxons_controller.rb
@@ -0,0 +1,95 @@
+module Spree
+ module Api
+ module V1
+ class TaxonsController < Spree::Api::BaseController
+ def index
+ @taxons = if taxonomy
+ taxonomy.root.children
+ elsif params[:ids]
+ Spree::Taxon.includes(:children).accessible_by(current_ability, :read).where(id: params[:ids].split(','))
+ else
+ Spree::Taxon.includes(:children).accessible_by(current_ability, :read).order(:taxonomy_id, :lft)
+ end
+ @taxons = @taxons.ransack(params[:q]).result
+ @taxons = @taxons.page(params[:page]).per(params[:per_page])
+ respond_with(@taxons)
+ end
+
+ def show
+ @taxon = taxon
+ respond_with(@taxon)
+ end
+
+ def jstree
+ show
+ end
+
+ def new; end
+
+ def create
+ authorize! :create, Taxon
+ @taxon = Spree::Taxon.new(taxon_params)
+ @taxon.taxonomy_id = params[:taxonomy_id]
+ taxonomy = Spree::Taxonomy.find_by(id: params[:taxonomy_id])
+
+ if taxonomy.nil?
+ @taxon.errors.add(:taxonomy_id, I18n.t('spree.api.invalid_taxonomy_id'))
+ invalid_resource!(@taxon) and return
+ end
+
+ @taxon.parent_id = taxonomy.root.id unless params[:taxon][:parent_id]
+
+ if @taxon.save
+ respond_with(@taxon, status: 201, default_template: :show)
+ else
+ invalid_resource!(@taxon)
+ end
+ end
+
+ def update
+ authorize! :update, taxon
+ if taxon.update_attributes(taxon_params)
+ respond_with(taxon, status: 200, default_template: :show)
+ else
+ invalid_resource!(taxon)
+ end
+ end
+
+ def destroy
+ authorize! :destroy, taxon
+ taxon.destroy
+ respond_with(taxon, status: 204)
+ end
+
+ def products
+ # Returns the products sorted by their position with the classification
+ # Products#index does not do the sorting.
+ taxon = Spree::Taxon.find(params[:id])
+ @products = taxon.products.ransack(params[:q]).result
+ @products = @products.page(params[:page]).per(params[:per_page] || 500)
+ render 'spree/api/v1/products/index'
+ end
+
+ private
+
+ def taxonomy
+ if params[:taxonomy_id].present?
+ @taxonomy ||= Spree::Taxonomy.accessible_by(current_ability, :read).find(params[:taxonomy_id])
+ end
+ end
+
+ def taxon
+ @taxon ||= taxonomy.taxons.accessible_by(current_ability, :read).find(params[:id])
+ end
+
+ def taxon_params
+ if params[:taxon] && !params[:taxon].empty?
+ params.require(:taxon).permit(permitted_taxon_attributes)
+ else
+ {}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/users_controller.rb b/api/app/controllers/spree/api/v1/users_controller.rb
new file mode 100644
index 00000000000..78e60148042
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/users_controller.rb
@@ -0,0 +1,67 @@
+module Spree
+ module Api
+ module V1
+ class UsersController < Spree::Api::BaseController
+ rescue_from Spree::Core::DestroyWithOrdersError, with: :error_during_processing
+
+ def index
+ @users = Spree.user_class.accessible_by(current_ability, :read)
+
+ @users = if params[:ids]
+ @users.ransack(id_in: params[:ids].split(','))
+ else
+ @users.ransack(params[:q])
+ end
+
+ @users = @users.result.page(params[:page]).per(params[:per_page])
+ expires_in 15.minutes, public: true
+ headers['Surrogate-Control'] = "max-age=#{15.minutes}"
+ respond_with(@users)
+ end
+
+ def show
+ respond_with(user)
+ end
+
+ def new; end
+
+ def create
+ authorize! :create, Spree.user_class
+ @user = Spree.user_class.new(user_params)
+ if @user.save
+ respond_with(@user, status: 201, default_template: :show)
+ else
+ invalid_resource!(@user)
+ end
+ end
+
+ def update
+ authorize! :update, user
+ if user.update_attributes(user_params)
+ respond_with(user, status: 200, default_template: :show)
+ else
+ invalid_resource!(user)
+ end
+ end
+
+ def destroy
+ authorize! :destroy, user
+ user.destroy
+ respond_with(user, status: 204)
+ end
+
+ private
+
+ def user
+ @user ||= Spree.user_class.accessible_by(current_ability, :read).find(params[:id])
+ end
+
+ def user_params
+ params.require(:user).permit(permitted_user_attributes |
+ [bill_address_attributes: permitted_address_attributes,
+ ship_address_attributes: permitted_address_attributes])
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/variants_controller.rb b/api/app/controllers/spree/api/v1/variants_controller.rb
new file mode 100644
index 00000000000..ff69928599d
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/variants_controller.rb
@@ -0,0 +1,81 @@
+module Spree
+ module Api
+ module V1
+ class VariantsController < Spree::Api::BaseController
+ before_action :product
+
+ def create
+ authorize! :create, Variant
+ @variant = scope.new(variant_params)
+ if @variant.save
+ respond_with(@variant, status: 201, default_template: :show)
+ else
+ invalid_resource!(@variant)
+ end
+ end
+
+ def destroy
+ @variant = scope.accessible_by(current_ability, :destroy).find(params[:id])
+ @variant.destroy
+ respond_with(@variant, status: 204)
+ end
+
+ # The lazyloaded associations here are pretty much attached to which nodes
+ # we render on the view so we better update it any time a node is included
+ # or removed from the views.
+ def index
+ @variants = scope.includes(*variant_includes).for_currency_and_available_price_amount.
+ ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ respond_with(@variants)
+ end
+
+ def new; end
+
+ def show
+ @variant = scope.includes(*variant_includes).find(params[:id])
+ respond_with(@variant)
+ end
+
+ def update
+ @variant = scope.accessible_by(current_ability, :update).find(params[:id])
+ if @variant.update_attributes(variant_params)
+ respond_with(@variant, status: 200, default_template: :show)
+ else
+ invalid_resource!(@product)
+ end
+ end
+
+ private
+
+ def product
+ if params[:product_id]
+ @product ||= Spree::Product.accessible_by(current_ability, :read).
+ friendly.find(params[:product_id])
+ end
+ end
+
+ def scope
+ variants = if @product
+ @product.variants_including_master
+ else
+ Variant
+ end
+
+ if current_ability.can?(:manage, Variant) && params[:show_deleted]
+ variants = variants.with_deleted
+ end
+
+ variants.eligible.accessible_by(current_ability, :read)
+ end
+
+ def variant_params
+ params.require(:variant).permit(permitted_variant_attributes)
+ end
+
+ def variant_includes
+ [{ option_values: :option_type }, :product, :default_price, :images, { stock_items: :stock_location }]
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v1/zones_controller.rb b/api/app/controllers/spree/api/v1/zones_controller.rb
new file mode 100644
index 00000000000..4e1eade91bf
--- /dev/null
+++ b/api/app/controllers/spree/api/v1/zones_controller.rb
@@ -0,0 +1,55 @@
+module Spree
+ module Api
+ module V1
+ class ZonesController < Spree::Api::BaseController
+ def create
+ authorize! :create, Zone
+ @zone = Spree::Zone.new(zone_params)
+ if @zone.save
+ respond_with(@zone, status: 201, default_template: :show)
+ else
+ invalid_resource!(@zone)
+ end
+ end
+
+ def destroy
+ authorize! :destroy, zone
+ zone.destroy
+ respond_with(zone, status: 204)
+ end
+
+ def index
+ @zones = Zone.accessible_by(current_ability, :read).order('name ASC').ransack(params[:q]).result.page(params[:page]).per(params[:per_page])
+ respond_with(@zones)
+ end
+
+ def show
+ respond_with(zone)
+ end
+
+ def update
+ authorize! :update, zone
+ if zone.update_attributes(zone_params)
+ respond_with(zone, status: 200, default_template: :show)
+ else
+ invalid_resource!(zone)
+ end
+ end
+
+ private
+
+ def zone_params
+ attrs = params.require(:zone).permit!
+ if attrs[:zone_members]
+ attrs[:zone_members_attributes] = attrs.delete(:zone_members)
+ end
+ attrs
+ end
+
+ def zone
+ @zone ||= Spree::Zone.accessible_by(current_ability, :read).find(params[:id])
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v2/base_controller.rb b/api/app/controllers/spree/api/v2/base_controller.rb
new file mode 100644
index 00000000000..8e3d0c15c57
--- /dev/null
+++ b/api/app/controllers/spree/api/v2/base_controller.rb
@@ -0,0 +1,98 @@
+module Spree
+ module Api
+ module V2
+ class BaseController < ActionController::API
+ include CanCan::ControllerAdditions
+ include Spree::Core::ControllerHelpers::StrongParameters
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+ rescue_from CanCan::AccessDenied, with: :access_denied
+
+ private
+
+ def collection_paginator
+ Spree::Api::Dependencies.storefront_collection_paginator.constantize
+ end
+
+ def render_serialized_payload(status = 200)
+ render json: yield, status: status
+ rescue ArgumentError => exception
+ render_error_payload(exception.message, 400)
+ end
+
+ def render_error_payload(error, status = 422)
+ if error.is_a?(Struct)
+ render json: { error: error.to_s, errors: error.to_h }, status: status
+ elsif error.is_a?(String)
+ render json: { error: error }, status: status
+ end
+ end
+
+ def spree_current_store
+ @spree_current_store ||= Spree::Store.current(request.env['SERVER_NAME'])
+ end
+
+ def spree_current_user
+ @spree_current_user ||= Spree.user_class.find_by(id: doorkeeper_token.resource_owner_id) if doorkeeper_token
+ end
+
+ def spree_authorize!(action, subject, *args)
+ authorize!(action, subject, *args)
+ end
+
+ def require_spree_current_user
+ raise CanCan::AccessDenied if spree_current_user.nil?
+ end
+
+ # Needs to be overriden so that we use Spree's Ability rather than anyone else's.
+ def current_ability
+ @current_ability ||= Spree::Dependencies.ability_class.constantize.new(spree_current_user)
+ end
+
+ def request_includes
+ # if API user want's to receive only the bare-minimum
+ # the API will return only the main resource without any included
+ if params[:include]&.blank?
+ []
+ elsif params[:include].present?
+ params[:include].split(',')
+ end
+ end
+
+ def resource_includes
+ (request_includes || default_resource_includes).map(&:intern)
+ end
+
+ # overwrite this method in your controllers to set JSON API default include value
+ # https://jsonapi.org/format/#fetching-includes
+ # eg.:
+ # %w[images variants]
+ # ['variant.images', 'line_items']
+ def default_resource_includes
+ []
+ end
+
+ def sparse_fields
+ return unless params[:fields]&.respond_to?(:each)
+
+ fields = {}
+ params[:fields].
+ select { |_, v| v.is_a?(String) }.
+ each { |type, values| fields[type.intern] = values.split(',').map(&:intern) }
+ fields.presence
+ end
+
+ def current_currency
+ spree_current_store.default_currency || Spree::Config[:currency]
+ end
+
+ def record_not_found
+ render_error_payload(I18n.t(:resource_not_found, scope: 'spree.api'), 404)
+ end
+
+ def access_denied(exception)
+ render_error_payload(exception.message, 403)
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v2/storefront/account/credit_cards_controller.rb b/api/app/controllers/spree/api/v2/storefront/account/credit_cards_controller.rb
new file mode 100644
index 00000000000..761bdf9678b
--- /dev/null
+++ b/api/app/controllers/spree/api/v2/storefront/account/credit_cards_controller.rb
@@ -0,0 +1,59 @@
+module Spree
+ module Api
+ module V2
+ module Storefront
+ module Account
+ class CreditCardsController < ::Spree::Api::V2::BaseController
+ before_action :require_spree_current_user
+
+ def index
+ render_serialized_payload { serialize_collection(resource) }
+ end
+
+ def show
+ render_serialized_payload { serialize_resource(resource) }
+ end
+
+ private
+
+ def resource
+ resource_finder.new.execute(scope: scope, params: params)
+ end
+
+ def collection_serializer
+ Spree::Api::Dependencies.storefront_credit_card_serializer.constantize
+ end
+
+ def resource_serializer
+ Spree::Api::Dependencies.storefront_credit_card_serializer.constantize
+ end
+
+ def resource_finder
+ Spree::Api::Dependencies.storefront_credit_card_finder.constantize
+ end
+
+ def serialize_collection(collection)
+ collection_serializer.new(
+ collection,
+ include: resource_includes,
+ fields: sparse_fields
+ ).serializable_hash
+ end
+
+ def serialize_resource(resource)
+ resource_serializer.new(
+ resource,
+ include: resource_includes,
+ fields: sparse_fields
+ ).serializable_hash
+ end
+
+ def scope
+ spree_current_user.credit_cards.accessible_by(current_ability, :read)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v2/storefront/account/orders_controller.rb b/api/app/controllers/spree/api/v2/storefront/account/orders_controller.rb
new file mode 100644
index 00000000000..e8e505924f4
--- /dev/null
+++ b/api/app/controllers/spree/api/v2/storefront/account/orders_controller.rb
@@ -0,0 +1,93 @@
+module Spree
+ module Api
+ module V2
+ module Storefront
+ module Account
+ class OrdersController < ::Spree::Api::V2::BaseController
+ include Spree::Api::V2::CollectionOptionsHelpers
+ before_action :require_spree_current_user
+
+ def index
+ render_serialized_payload { serialize_collection(paginated_collection) }
+ end
+
+ def show
+ spree_authorize! :show, resource
+
+ render_serialized_payload { serialize_resource(resource) }
+ end
+
+ private
+
+ def paginated_collection
+ collection_paginator.new(sorted_collection, params).call
+ end
+
+ def sorted_collection
+ collection_sorter.new(collection, params).call
+ end
+
+ def collection
+ collection_finder.new(user: spree_current_user).execute
+ end
+
+ def resource
+ resource = resource_finder.new(user: spree_current_user, number: params[:id]).execute.take
+ raise ActiveRecord::RecordNotFound if resource.nil?
+
+ resource
+ end
+
+ def serialize_collection(collection)
+ collection_serializer.new(
+ collection,
+ collection_options(collection)
+ ).serializable_hash
+ end
+
+ def serialize_resource(resource)
+ resource_serializer.new(
+ resource,
+ include: resource_includes,
+ sparse_fields: sparse_fields
+ ).serializable_hash
+ end
+
+ def collection_serializer
+ Spree::Api::Dependencies.storefront_cart_serializer.constantize
+ end
+
+ def resource_serializer
+ Spree::Api::Dependencies.storefront_cart_serializer.constantize
+ end
+
+ def collection_finder
+ Spree::Api::Dependencies.storefront_completed_order_finder.constantize
+ end
+
+ def resource_finder
+ Spree::Api::Dependencies.storefront_completed_order_finder.constantize
+ end
+
+ def collection_sorter
+ Spree::Api::Dependencies.storefront_order_sorter.constantize
+ end
+
+ def collection_paginator
+ Spree::Api::Dependencies.storefront_collection_paginator.constantize
+ end
+
+ def collection_options(collection)
+ {
+ links: collection_links(collection),
+ meta: collection_meta(collection),
+ include: resource_includes,
+ sparse_fields: sparse_fields
+ }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v2/storefront/account_controller.rb b/api/app/controllers/spree/api/v2/storefront/account_controller.rb
new file mode 100644
index 00000000000..9131f101e54
--- /dev/null
+++ b/api/app/controllers/spree/api/v2/storefront/account_controller.rb
@@ -0,0 +1,33 @@
+module Spree
+ module Api
+ module V2
+ module Storefront
+ class AccountController < ::Spree::Api::V2::BaseController
+ before_action :require_spree_current_user
+
+ def show
+ render_serialized_payload { serialize_resource(resource) }
+ end
+
+ private
+
+ def resource
+ spree_current_user
+ end
+
+ def serialize_resource(resource)
+ resource_serializer.new(
+ resource,
+ include: resource_includes,
+ fields: sparse_fields
+ ).serializable_hash
+ end
+
+ def resource_serializer
+ Spree::Api::Dependencies.storefront_user_serializer.constantize
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v2/storefront/cart_controller.rb b/api/app/controllers/spree/api/v2/storefront/cart_controller.rb
new file mode 100644
index 00000000000..6257908b8b0
--- /dev/null
+++ b/api/app/controllers/spree/api/v2/storefront/cart_controller.rb
@@ -0,0 +1,139 @@
+module Spree
+ module Api
+ module V2
+ module Storefront
+ class CartController < ::Spree::Api::V2::BaseController
+ include Spree::Api::V2::Storefront::OrderConcern
+ before_action :ensure_order, except: :create
+
+ def create
+ spree_authorize! :create, Spree::Order
+
+ order_params = {
+ user: spree_current_user,
+ store: spree_current_store,
+ currency: current_currency
+ }
+
+ order = spree_current_order if spree_current_order.present?
+ order ||= create_service.call(order_params).value
+
+ render_serialized_payload(201) { serialize_order(order) }
+ end
+
+ def add_item
+ variant = Spree::Variant.find(params[:variant_id])
+
+ spree_authorize! :update, spree_current_order, order_token
+ spree_authorize! :show, variant
+
+ result = add_item_service.call(
+ order: spree_current_order,
+ variant: variant,
+ quantity: params[:quantity],
+ options: params[:options]
+ )
+
+ render_order(result)
+ end
+
+ def remove_line_item
+ spree_authorize! :update, spree_current_order, order_token
+
+ remove_line_item_service.call(
+ order: spree_current_order,
+ line_item: line_item
+ )
+
+ render_serialized_payload { serialized_current_order }
+ end
+
+ def empty
+ spree_authorize! :update, spree_current_order, order_token
+
+ # TODO: we should extract this logic into service and let
+ # developers overwrite it
+ spree_current_order.empty!
+
+ render_serialized_payload { serialized_current_order }
+ end
+
+ def set_quantity
+ return render_error_item_quantity unless params[:quantity].to_i > 0
+
+ spree_authorize! :update, spree_current_order, order_token
+
+ result = set_item_quantity_service.call(order: spree_current_order, line_item: line_item, quantity: params[:quantity])
+
+ render_order(result)
+ end
+
+ def show
+ spree_authorize! :show, spree_current_order, order_token
+
+ render_serialized_payload { serialized_current_order }
+ end
+
+ def apply_coupon_code
+ spree_authorize! :update, spree_current_order, order_token
+
+ spree_current_order.coupon_code = params[:coupon_code]
+ result = coupon_handler.new(spree_current_order).apply
+
+ if result.error.blank?
+ render_serialized_payload { serialized_current_order }
+ else
+ render_error_payload(result.error)
+ end
+ end
+
+ def remove_coupon_code
+ spree_authorize! :update, spree_current_order, order_token
+
+ result = coupon_handler.new(spree_current_order).remove(params[:coupon_code])
+
+ if result.error.blank?
+ render_serialized_payload { serialized_current_order }
+ else
+ render_error_payload(result.error)
+ end
+ end
+
+ private
+
+ def resource_serializer
+ Spree::Api::Dependencies.storefront_cart_serializer.constantize
+ end
+
+ def create_service
+ Spree::Api::Dependencies.storefront_cart_create_service.constantize
+ end
+
+ def add_item_service
+ Spree::Api::Dependencies.storefront_cart_add_item_service.constantize
+ end
+
+ def set_item_quantity_service
+ Spree::Api::Dependencies.storefront_cart_set_item_quantity_service.constantize
+ end
+
+ def remove_line_item_service
+ Spree::Api::Dependencies.storefront_cart_remove_line_item_service.constantize
+ end
+
+ def coupon_handler
+ Spree::Api::Dependencies.storefront_coupon_handler.constantize
+ end
+
+ def line_item
+ @line_item ||= spree_current_order.line_items.find(params[:line_item_id])
+ end
+
+ def render_error_item_quantity
+ render json: { error: I18n.t(:wrong_quantity, scope: 'spree.api.v2.cart') }, status: 422
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v2/storefront/checkout_controller.rb b/api/app/controllers/spree/api/v2/storefront/checkout_controller.rb
new file mode 100644
index 00000000000..66c22474744
--- /dev/null
+++ b/api/app/controllers/spree/api/v2/storefront/checkout_controller.rb
@@ -0,0 +1,132 @@
+module Spree
+ module Api
+ module V2
+ module Storefront
+ class CheckoutController < ::Spree::Api::V2::BaseController
+ include Spree::Api::V2::Storefront::OrderConcern
+ before_action :ensure_order
+
+ def next
+ spree_authorize! :update, spree_current_order, order_token
+
+ result = next_service.call(order: spree_current_order)
+
+ render_order(result)
+ end
+
+ def advance
+ spree_authorize! :update, spree_current_order, order_token
+
+ result = advance_service.call(order: spree_current_order)
+
+ render_order(result)
+ end
+
+ def complete
+ spree_authorize! :update, spree_current_order, order_token
+
+ result = complete_service.call(order: spree_current_order)
+
+ render_order(result)
+ end
+
+ def update
+ spree_authorize! :update, spree_current_order, order_token
+
+ result = update_service.call(
+ order: spree_current_order,
+ params: params,
+ # defined in https://github.com/spree/spree/blob/master/core/lib/spree/core/controller_helpers/strong_parameters.rb#L19
+ permitted_attributes: permitted_checkout_attributes,
+ request_env: request.headers.env
+ )
+
+ render_order(result)
+ end
+
+ def add_store_credit
+ spree_authorize! :update, spree_current_order, order_token
+
+ result = add_store_credit_service.call(
+ order: spree_current_order,
+ amount: params[:amount].try(:to_f)
+ )
+
+ render_order(result)
+ end
+
+ def remove_store_credit
+ spree_authorize! :update, spree_current_order, order_token
+
+ result = remove_store_credit_service.call(order: spree_current_order)
+ render_order(result)
+ end
+
+ def shipping_rates
+ result = shipping_rates_service.call(order: spree_current_order)
+
+ render_serialized_payload { serialize_shipping_rates(result.value) }
+ end
+
+ def payment_methods
+ render_serialized_payload { serialize_payment_methods(spree_current_order.available_payment_methods) }
+ end
+
+ private
+
+ def resource_serializer
+ Spree::Api::Dependencies.storefront_cart_serializer.constantize
+ end
+
+ def next_service
+ Spree::Api::Dependencies.storefront_checkout_next_service.constantize
+ end
+
+ def advance_service
+ Spree::Api::Dependencies.storefront_checkout_advance_service.constantize
+ end
+
+ def add_store_credit_service
+ Spree::Api::Dependencies.storefront_checkout_add_store_credit_service.constantize
+ end
+
+ def remove_store_credit_service
+ Spree::Api::Dependencies.storefront_checkout_remove_store_credit_service.constantize
+ end
+
+ def complete_service
+ Spree::Api::Dependencies.storefront_checkout_complete_service.constantize
+ end
+
+ def update_service
+ Spree::Api::Dependencies.storefront_checkout_update_service.constantize
+ end
+
+ def payment_methods_serializer
+ Spree::Api::Dependencies.storefront_payment_method_serializer.constantize
+ end
+
+ def shipping_rates_service
+ Spree::Api::Dependencies.storefront_checkout_get_shipping_rates_service.constantize
+ end
+
+ def shipping_rates_serializer
+ Spree::Api::Dependencies.storefront_shipment_serializer.constantize
+ end
+
+ def serialize_payment_methods(payment_methods)
+ payment_methods_serializer.new(payment_methods).serializable_hash
+ end
+
+ def serialize_shipping_rates(shipments)
+ shipping_rates_serializer.new(
+ shipments,
+ include: [:shipping_rates],
+ params: { show_rates: true }
+ ).serializable_hash
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v2/storefront/countries_controller.rb b/api/app/controllers/spree/api/v2/storefront/countries_controller.rb
new file mode 100644
index 00000000000..c290adb3c38
--- /dev/null
+++ b/api/app/controllers/spree/api/v2/storefront/countries_controller.rb
@@ -0,0 +1,61 @@
+module Spree
+ module Api
+ module V2
+ module Storefront
+ class CountriesController < ::Spree::Api::V2::BaseController
+ include Spree::Api::V2::CollectionOptionsHelpers
+
+ def index
+ render_serialized_payload { serialize_collection(collection) }
+ end
+
+ def show
+ render_serialized_payload { serialize_resource(resource) }
+ end
+
+ private
+
+ def serialize_collection(collection)
+ collection_serializer.new(collection).serializable_hash
+ end
+
+ def serialize_resource(resource)
+ resource_serializer.new(
+ resource,
+ include: resource_includes,
+ fields: sparse_fields,
+ params: { include_states: true }
+ ).serializable_hash
+ end
+
+ def collection
+ collection_finder.new(scope, params).call
+ end
+
+ def resource
+ return scope.default if params[:iso] == 'default'
+
+ scope.find_by(iso: params[:iso]&.upcase) ||
+ scope.find_by(iso3: params[:iso]&.upcase)
+ end
+
+ def resource_serializer
+ Spree::Api::Dependencies.storefront_country_serializer.constantize
+ end
+
+ def collection_serializer
+ Spree::Api::Dependencies.storefront_country_serializer.constantize
+ end
+
+ def collection_finder
+ Spree::Api::Dependencies.storefront_country_finder.constantize
+ end
+
+ def scope
+ Spree::Country.accessible_by(current_ability, :read)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v2/storefront/products_controller.rb b/api/app/controllers/spree/api/v2/storefront/products_controller.rb
new file mode 100644
index 00000000000..5a8e95edec3
--- /dev/null
+++ b/api/app/controllers/spree/api/v2/storefront/products_controller.rb
@@ -0,0 +1,93 @@
+module Spree
+ module Api
+ module V2
+ module Storefront
+ class ProductsController < ::Spree::Api::V2::BaseController
+ include Spree::Api::V2::CollectionOptionsHelpers
+
+ def index
+ render_serialized_payload { serialize_collection(paginated_collection) }
+ end
+
+ def show
+ render_serialized_payload { serialize_resource(resource) }
+ end
+
+ private
+
+ def serialize_collection(collection)
+ collection_serializer.new(
+ collection,
+ collection_options(collection)
+ ).serializable_hash
+ end
+
+ def serialize_resource(resource)
+ resource_serializer.new(
+ resource,
+ include: resource_includes,
+ fields: sparse_fields
+ ).serializable_hash
+ end
+
+ def paginated_collection
+ collection_paginator.new(sorted_collection, params).call
+ end
+
+ def sorted_collection
+ collection_sorter.new(collection, params, current_currency).call
+ end
+
+ def collection
+ collection_finder.new(scope: scope, params: params, current_currency: current_currency).execute
+ end
+
+ def resource
+ scope.find_by(slug: params[:id]) || scope.find(params[:id])
+ end
+
+ def collection_sorter
+ Spree::Api::Dependencies.storefront_products_sorter.constantize
+ end
+
+ def collection_finder
+ Spree::Api::Dependencies.storefront_products_finder.constantize
+ end
+
+ def collection_serializer
+ Spree::Api::Dependencies.storefront_product_serializer.constantize
+ end
+
+ def resource_serializer
+ Spree::Api::Dependencies.storefront_product_serializer.constantize
+ end
+
+ def collection_options(collection)
+ {
+ links: collection_links(collection),
+ meta: collection_meta(collection),
+ include: resource_includes,
+ fields: sparse_fields
+ }
+ end
+
+ def scope
+ Spree::Product.accessible_by(current_ability, :read).includes(scope_includes)
+ end
+
+ def scope_includes
+ {
+ master: :default_price,
+ variants: [],
+ variant_images: [],
+ taxons: [],
+ product_properties: :property,
+ option_types: :option_values,
+ variants_including_master: %i[default_price option_values]
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/controllers/spree/api/v2/storefront/taxons_controller.rb b/api/app/controllers/spree/api/v2/storefront/taxons_controller.rb
new file mode 100644
index 00000000000..c962c35b9fe
--- /dev/null
+++ b/api/app/controllers/spree/api/v2/storefront/taxons_controller.rb
@@ -0,0 +1,85 @@
+module Spree
+ module Api
+ module V2
+ module Storefront
+ class TaxonsController < ::Spree::Api::V2::BaseController
+ include Spree::Api::V2::CollectionOptionsHelpers
+
+ def index
+ render_serialized_payload { serialize_collection(paginated_collection) }
+ end
+
+ def show
+ render_serialized_payload { serialize_resource(resource) }
+ end
+
+ private
+
+ def serialize_collection(collection)
+ collection_serializer.new(
+ collection,
+ collection_options(collection)
+ ).serializable_hash
+ end
+
+ def serialize_resource(resource)
+ resource_serializer.new(
+ resource,
+ include: resource_includes,
+ fields: sparse_fields
+ ).serializable_hash
+ end
+
+ def collection_serializer
+ Spree::Api::Dependencies.storefront_taxon_serializer.constantize
+ end
+
+ def resource_serializer
+ Spree::Api::Dependencies.storefront_taxon_serializer.constantize
+ end
+
+ def collection_finder
+ Spree::Api::Dependencies.storefront_taxon_finder.constantize
+ end
+
+ def collection_options(collection)
+ {
+ links: collection_links(collection),
+ meta: collection_meta(collection),
+ include: resource_includes,
+ fields: sparse_fields
+ }
+ end
+
+ def paginated_collection
+ collection_paginator.new(collection, params).call
+ end
+
+ def collection
+ collection_finder.new(scope: scope, params: params).execute
+ end
+
+ def resource
+ scope.find_by(permalink: params[:id]) || scope.find(params[:id])
+ end
+
+ def scope
+ Spree::Taxon.accessible_by(current_ability, :read).includes(scope_includes)
+ end
+
+ def scope_includes
+ node_includes = %i[icon products parent taxonomy]
+
+ {
+ parent: node_includes,
+ children: node_includes,
+ taxonomy: [root: node_includes],
+ products: [],
+ icon: []
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/helpers/spree/api/api_helpers.rb b/api/app/helpers/spree/api/api_helpers.rb
new file mode 100644
index 00000000000..ce086f53553
--- /dev/null
+++ b/api/app/helpers/spree/api/api_helpers.rb
@@ -0,0 +1,188 @@
+module Spree
+ module Api
+ module ApiHelpers
+ ATTRIBUTES = [
+ :product_attributes,
+ :product_property_attributes,
+ :variant_attributes,
+ :image_attributes,
+ :option_value_attributes,
+ :order_attributes,
+ :line_item_attributes,
+ :option_type_attributes,
+ :payment_attributes,
+ :payment_method_attributes,
+ :shipment_attributes,
+ :taxonomy_attributes,
+ :taxon_attributes,
+ :address_attributes,
+ :country_attributes,
+ :state_attributes,
+ :adjustment_attributes,
+ :inventory_unit_attributes,
+ :return_authorization_attributes,
+ :creditcard_attributes,
+ :payment_source_attributes,
+ :user_attributes,
+ :property_attributes,
+ :stock_location_attributes,
+ :stock_movement_attributes,
+ :stock_item_attributes,
+ :promotion_attributes,
+ :store_attributes,
+ :tag_attributes,
+ :customer_return_attributes,
+ :reimbursement_attributes
+ ]
+
+ mattr_reader *ATTRIBUTES
+
+ def required_fields_for(model)
+ required_fields = model._validators.select do |_field, validations|
+ validations.any? { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }
+ end.map(&:first) # get fields that are invalid
+ # Permalinks presence is validated, but are really automatically generated
+ # Therefore we shouldn't tell API clients that they MUST send one through
+ required_fields.map!(&:to_s).delete('permalink')
+ # Do not require slugs, either
+ required_fields.delete('slug')
+ required_fields
+ end
+
+ @@product_attributes = [
+ :id, :name, :description, :price, :display_price, :available_on,
+ :slug, :meta_description, :meta_keywords, :shipping_category_id,
+ :taxon_ids, :total_on_hand
+ ]
+
+ @@product_property_attributes = [
+ :id, :product_id, :property_id, :value, :property_name
+ ]
+
+ @@variant_attributes = [
+ :id, :name, :sku, :price, :weight, :height, :width, :depth, :is_master,
+ :slug, :description, :track_inventory
+ ]
+
+ @@image_attributes = [
+ :id, :position, :attachment_content_type, :attachment_file_name, :type,
+ :attachment_updated_at, :attachment_width, :attachment_height, :alt
+ ]
+
+ @@option_value_attributes = [
+ :id, :name, :presentation, :option_type_name, :option_type_id,
+ :option_type_presentation
+ ]
+
+ @@order_attributes = [
+ :id, :number, :item_total, :total, :ship_total, :state, :adjustment_total,
+ :user_id, :created_at, :updated_at, :completed_at, :payment_total,
+ :shipment_state, :payment_state, :email, :special_instructions, :channel,
+ :included_tax_total, :additional_tax_total, :display_included_tax_total,
+ :display_additional_tax_total, :tax_total, :currency, :considered_risky,
+ :canceler_id
+ ]
+
+ @@line_item_attributes = [:id, :quantity, :price, :variant_id]
+
+ @@option_type_attributes = [:id, :name, :presentation, :position]
+
+ @@payment_attributes = [
+ :id, :source_type, :source_id, :amount, :display_amount,
+ :payment_method_id, :state, :avs_response, :created_at,
+ :updated_at, :number
+ ]
+
+ @@payment_method_attributes = [:id, :name, :description]
+
+ @@shipment_attributes = [:id, :tracking, :number, :cost, :shipped_at, :state]
+
+ @@taxonomy_attributes = [:id, :name]
+
+ @@taxon_attributes = [
+ :id, :name, :pretty_name, :permalink, :parent_id,
+ :taxonomy_id, :meta_title, :meta_description
+ ]
+
+ @@inventory_unit_attributes = [
+ :id, :lock_version, :state, :variant_id, :shipment_id,
+ :return_authorization_id
+ ]
+
+ @@return_authorization_attributes = [
+ :id, :number, :state, :order_id, :memo, :created_at, :updated_at
+ ]
+
+ @@address_attributes = [
+ :id, :firstname, :lastname, :full_name, :address1, :address2, :city,
+ :zipcode, :phone, :company, :alternative_phone, :country_id, :state_id,
+ :state_name, :state_text
+ ]
+
+ @@country_attributes = [:id, :iso_name, :iso, :iso3, :name, :numcode]
+
+ @@state_attributes = [:id, :name, :abbr, :country_id]
+
+ @@adjustment_attributes = [
+ :id, :source_type, :source_id, :adjustable_type, :adjustable_id,
+ :originator_type, :originator_id, :amount, :label, :mandatory,
+ :locked, :eligible, :created_at, :updated_at
+ ]
+
+ @@creditcard_attributes = [
+ :id, :month, :year, :cc_type, :last_digits, :name,
+ :gateway_customer_profile_id, :gateway_payment_profile_id
+ ]
+
+ @@payment_source_attributes = [
+ :id, :month, :year, :cc_type, :last_digits, :name
+ ]
+
+ @@user_attributes = [:id, :email, :created_at, :updated_at]
+
+ @@property_attributes = [:id, :name, :presentation]
+
+ @@stock_location_attributes = [
+ :id, :name, :address1, :address2, :city, :state_id, :state_name,
+ :country_id, :zipcode, :phone, :active
+ ]
+
+ @@stock_movement_attributes = [:id, :quantity, :stock_item_id]
+
+ @@stock_item_attributes = [
+ :id, :count_on_hand, :backorderable, :lock_version, :stock_location_id,
+ :variant_id
+ ]
+
+ @@promotion_attributes = [
+ :id, :name, :description, :expires_at, :starts_at, :type, :usage_limit,
+ :match_policy, :code, :advertise, :path
+ ]
+
+ @@store_attributes = [
+ :id, :name, :url, :meta_description, :meta_keywords, :seo_title,
+ :mail_from_address, :default_currency, :code, :default
+ ]
+
+ @@tag_attributes = [:id, :name]
+
+ @@customer_return_attributes = [
+ :id, :number, :order_id, :fully_reimbursed?, :pre_tax_total,
+ :created_at, :updated_at
+ ]
+
+ @@reimbursement_attributes = [
+ :id, :reimbursement_status, :customer_return_id, :order_id,
+ :number, :total, :created_at, :updated_at
+ ]
+
+ def variant_attributes
+ if @current_user_roles&.include?('admin')
+ @@variant_attributes + [:cost_price]
+ else
+ @@variant_attributes
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/helpers/spree/api/v2/collection_options_helpers.rb b/api/app/helpers/spree/api/v2/collection_options_helpers.rb
new file mode 100644
index 00000000000..daa4dd44020
--- /dev/null
+++ b/api/app/helpers/spree/api/v2/collection_options_helpers.rb
@@ -0,0 +1,37 @@
+module Spree
+ module Api
+ module V2
+ module CollectionOptionsHelpers
+ def collection_links(collection)
+ {
+ self: request.original_url,
+ next: pagination_url(collection.next_page || collection.total_pages),
+ prev: pagination_url(collection.prev_page || 1),
+ last: pagination_url(collection.total_pages),
+ first: pagination_url(1)
+ }
+ end
+
+ def collection_meta(collection)
+ {
+ count: collection.size,
+ total_count: collection.total_count,
+ total_pages: collection.total_pages
+ }
+ end
+
+ # leaving this method in public scope so it's still possible to modify
+ # those params to support non-standard non-JSON API parameters
+ def collection_permitted_params
+ params.permit(:format, :page, :per_page, :sort, :include, :fields, filter: {})
+ end
+
+ private
+
+ def pagination_url(page)
+ url_for(collection_permitted_params.merge(page: page))
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/models/concerns/spree/user_api_authentication.rb b/api/app/models/concerns/spree/user_api_authentication.rb
new file mode 100644
index 00000000000..600d5c107b1
--- /dev/null
+++ b/api/app/models/concerns/spree/user_api_authentication.rb
@@ -0,0 +1,19 @@
+module Spree
+ module UserApiAuthentication
+ def generate_spree_api_key!
+ self.spree_api_key = generate_spree_api_key
+ save!
+ end
+
+ def clear_spree_api_key!
+ self.spree_api_key = nil
+ save!
+ end
+
+ private
+
+ def generate_spree_api_key
+ SecureRandom.hex(24)
+ end
+ end
+end
diff --git a/api/app/models/concerns/spree/user_api_methods.rb b/api/app/models/concerns/spree/user_api_methods.rb
new file mode 100644
index 00000000000..2f5f813d593
--- /dev/null
+++ b/api/app/models/concerns/spree/user_api_methods.rb
@@ -0,0 +1,7 @@
+module Spree
+ module UserApiMethods
+ extend ActiveSupport::Concern
+
+ include Spree::UserApiAuthentication
+ end
+end
diff --git a/api/app/models/doorkeeper/access_grant_decorator.rb b/api/app/models/doorkeeper/access_grant_decorator.rb
new file mode 100644
index 00000000000..17c5b3c1c31
--- /dev/null
+++ b/api/app/models/doorkeeper/access_grant_decorator.rb
@@ -0,0 +1,3 @@
+Doorkeeper::AccessGrant.class_eval do
+ self.table_name = 'spree_oauth_access_grants'
+end
diff --git a/api/app/models/doorkeeper/access_token_decorator.rb b/api/app/models/doorkeeper/access_token_decorator.rb
new file mode 100644
index 00000000000..72f839cb74b
--- /dev/null
+++ b/api/app/models/doorkeeper/access_token_decorator.rb
@@ -0,0 +1,3 @@
+Doorkeeper::AccessToken.class_eval do
+ self.table_name = 'spree_oauth_access_tokens'
+end
diff --git a/api/app/models/doorkeeper/application_decorator.rb b/api/app/models/doorkeeper/application_decorator.rb
new file mode 100644
index 00000000000..bf4c378d909
--- /dev/null
+++ b/api/app/models/doorkeeper/application_decorator.rb
@@ -0,0 +1,3 @@
+Doorkeeper::Application.class_eval do
+ self.table_name = 'spree_oauth_applications'
+end
diff --git a/api/app/models/spree/api_configuration.rb b/api/app/models/spree/api_configuration.rb
new file mode 100644
index 00000000000..28bc94d2219
--- /dev/null
+++ b/api/app/models/spree/api_configuration.rb
@@ -0,0 +1,5 @@
+module Spree
+ class ApiConfiguration < Preferences::Configuration
+ preference :requires_authentication, :boolean, default: true
+ end
+end
diff --git a/api/app/models/spree/api_dependencies.rb b/api/app/models/spree/api_dependencies.rb
new file mode 100644
index 00000000000..9d1c6f48ab2
--- /dev/null
+++ b/api/app/models/spree/api_dependencies.rb
@@ -0,0 +1,77 @@
+module Spree
+ class ApiDependencies
+ include Spree::DependenciesHelper
+
+ INJECTION_POINTS = [
+ :storefront_cart_create_service, :storefront_cart_add_item_service, :storefront_cart_remove_line_item_service,
+ :storefront_cart_remove_item_service, :storefront_cart_set_item_quantity_service, :storefront_cart_recalculate_service,
+ :storefront_cart_update, :storefront_coupon_handler, :storefront_checkout_next_service, :storefront_checkout_advance_service,
+ :storefront_checkout_update_service, :storefront_checkout_complete_service, :storefront_checkout_add_store_credit_service,
+ :storefront_checkout_remove_store_credit_service, :storefront_checkout_get_shipping_rates_service,
+ :storefront_cart_compare_line_items_service, :storefront_cart_serializer, :storefront_credit_card_serializer,
+ :storefront_credit_card_finder, :storefront_shipment_serializer, :storefront_payment_method_serializer, :storefront_country_finder,
+ :storefront_country_serializer, :storefront_current_order_finder, :storefront_completed_order_finder, :storefront_order_sorter,
+ :storefront_collection_paginator, :storefront_user_serializer, :storefront_products_sorter, :storefront_products_finder,
+ :storefront_product_serializer, :storefront_taxon_serializer, :storefront_taxon_finder, :storefront_find_by_variant_finder,
+ :storefront_cart_update_service
+ ].freeze
+
+ attr_accessor *INJECTION_POINTS
+
+ def initialize
+ set_storefront_defaults
+ end
+
+ private
+
+ def set_storefront_defaults
+ # cart services
+ @storefront_cart_create_service = Spree::Dependencies.cart_create_service
+ @storefront_cart_add_item_service = Spree::Dependencies.cart_add_item_service
+ @storefront_cart_compare_line_items_service = Spree::Dependencies.cart_compare_line_items_service
+ @storefront_cart_update_service = Spree::Dependencies.cart_update_service
+ @storefront_cart_remove_line_item_service = Spree::Dependencies.cart_remove_line_item_service
+ @storefront_cart_remove_item_service = Spree::Dependencies.cart_remove_item_service
+ @storefront_cart_set_item_quantity_service = Spree::Dependencies.cart_set_item_quantity_service
+ @storefront_cart_recalculate_service = Spree::Dependencies.cart_recalculate_service
+
+ # coupon code handler
+ @storefront_coupon_handler = Spree::Dependencies.coupon_handler
+
+ # checkout services
+ @storefront_checkout_next_service = Spree::Dependencies.checkout_next_service
+ @storefront_checkout_advance_service = Spree::Dependencies.checkout_advance_service
+ @storefront_checkout_update_service = Spree::Dependencies.checkout_update_service
+ @storefront_checkout_complete_service = Spree::Dependencies.checkout_complete_service
+ @storefront_checkout_add_store_credit_service = Spree::Dependencies.checkout_add_store_credit_service
+ @storefront_checkout_remove_store_credit_service = Spree::Dependencies.checkout_remove_store_credit_service
+ @storefront_checkout_get_shipping_rates_service = Spree::Dependencies.checkout_get_shipping_rates_service
+
+ # serializers
+ @storefront_cart_serializer = 'Spree::V2::Storefront::CartSerializer'
+ @storefront_credit_card_serializer = 'Spree::V2::Storefront::CreditCardSerializer'
+ @storefront_country_serializer = 'Spree::V2::Storefront::CountrySerializer'
+ @storefront_user_serializer = 'Spree::V2::Storefront::UserSerializer'
+ @storefront_shipment_serializer = 'Spree::V2::Storefront::ShipmentSerializer'
+ @storefront_taxon_serializer = 'Spree::V2::Storefront::TaxonSerializer'
+ @storefront_payment_method_serializer = 'Spree::V2::Storefront::PaymentMethodSerializer'
+ @storefront_product_serializer = 'Spree::V2::Storefront::ProductSerializer'
+
+ # sorters
+ @storefront_order_sorter = Spree::Dependencies.order_sorter
+ @storefront_products_sorter = Spree::Dependencies.products_sorter
+
+ # paginators
+ @storefront_collection_paginator = Spree::Dependencies.collection_paginator
+
+ # finders
+ @storefront_country_finder = Spree::Dependencies.country_finder
+ @storefront_current_order_finder = Spree::Dependencies.current_order_finder
+ @storefront_completed_order_finder = Spree::Dependencies.completed_order_finder
+ @storefront_credit_card_finder = Spree::Dependencies.credit_card_finder
+ @storefront_find_by_variant_finder = Spree::Dependencies.line_item_by_variant_finder
+ @storefront_products_finder = Spree::Dependencies.products_finder
+ @storefront_taxon_finder = Spree::Dependencies.taxon_finder
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/address_serializer.rb b/api/app/serializers/spree/v2/storefront/address_serializer.rb
new file mode 100644
index 00000000000..5eb18132461
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/address_serializer.rb
@@ -0,0 +1,15 @@
+module Spree
+ module V2
+ module Storefront
+ class AddressSerializer < BaseSerializer
+ set_type :address
+
+ attributes :firstname, :lastname, :address1, :address2, :city, :zipcode, :phone, :state_name,
+ :company, :country_name, :country_iso3, :country_iso
+
+ attribute :state_code, &:state_abbr
+ attribute :state_name, &:state_name_text
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/base_serializer.rb b/api/app/serializers/spree/v2/storefront/base_serializer.rb
new file mode 100644
index 00000000000..acb6e19bdf0
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/base_serializer.rb
@@ -0,0 +1,9 @@
+module Spree
+ module V2
+ module Storefront
+ class BaseSerializer
+ include FastJsonapi::ObjectSerializer
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/cart_serializer.rb b/api/app/serializers/spree/v2/storefront/cart_serializer.rb
new file mode 100644
index 00000000000..00c849db7f6
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/cart_serializer.rb
@@ -0,0 +1,40 @@
+module Spree
+ module V2
+ module Storefront
+ class CartSerializer < BaseSerializer
+ set_type :cart
+
+ attributes :number, :item_total, :total, :ship_total, :adjustment_total, :created_at,
+ :updated_at, :completed_at, :included_tax_total, :additional_tax_total, :display_additional_tax_total,
+ :display_included_tax_total, :tax_total, :currency, :state, :token, :email,
+ :display_item_total, :display_ship_total, :display_adjustment_total, :display_tax_total,
+ :promo_total, :display_promo_total, :item_count, :special_instructions, :display_total
+
+ has_many :line_items
+ has_many :variants
+ has_many :promotions, id_method_name: :promotion_id do |cart|
+ # we only want to display applied and valid promotions
+ # sometimes Order can have multiple promotions but the promo engine
+ # will only apply those that are more beneficial for the customer
+ # TODO: we should probably move this code out of the serializer
+ promotion_ids = cart.all_adjustments.eligible.nonzero.promotion.map { |a| a.source.promotion_id }.uniq
+
+ cart.order_promotions.where(promotion_id: promotion_ids).uniq(&:promotion_id)
+ end
+ has_many :payments do |cart|
+ cart.payments.valid
+ end
+ has_many :shipments
+
+ belongs_to :user
+ belongs_to :billing_address,
+ id_method_name: :bill_address_id,
+ serializer: :address
+
+ belongs_to :shipping_address,
+ id_method_name: :ship_address_id,
+ serializer: :address
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/country_serializer.rb b/api/app/serializers/spree/v2/storefront/country_serializer.rb
new file mode 100644
index 00000000000..30775fc4ef1
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/country_serializer.rb
@@ -0,0 +1,18 @@
+module Spree
+ module V2
+ module Storefront
+ class CountrySerializer < BaseSerializer
+ set_type :country
+
+ attributes :iso, :iso3, :iso_name, :name, :states_required,
+ :zipcode_required
+
+ attribute :default do |object|
+ object.default?
+ end
+
+ has_many :states, if: proc { |_record, params| params && params[:include_states] }
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/credit_card_serializer.rb b/api/app/serializers/spree/v2/storefront/credit_card_serializer.rb
new file mode 100644
index 00000000000..c1b9b7f69c6
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/credit_card_serializer.rb
@@ -0,0 +1,13 @@
+module Spree
+ module V2
+ module Storefront
+ class CreditCardSerializer < BaseSerializer
+ set_type :credit_card
+
+ attributes :cc_type, :last_digits, :month, :year, :name, :default
+
+ belongs_to :payment_method
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/image_serializer.rb b/api/app/serializers/spree/v2/storefront/image_serializer.rb
new file mode 100644
index 00000000000..8dcac025c40
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/image_serializer.rb
@@ -0,0 +1,11 @@
+module Spree
+ module V2
+ module Storefront
+ class ImageSerializer < BaseSerializer
+ set_type :image
+
+ attributes :viewable_type, :viewable_id, :styles
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/line_item_serializer.rb b/api/app/serializers/spree/v2/storefront/line_item_serializer.rb
new file mode 100644
index 00000000000..0683ae33855
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/line_item_serializer.rb
@@ -0,0 +1,18 @@
+module Spree
+ module V2
+ module Storefront
+ class LineItemSerializer < BaseSerializer
+ set_type :line_item
+
+ attributes :name, :quantity, :price, :slug, :options_text, :currency,
+ :display_price, :total, :display_total, :adjustment_total,
+ :display_adjustment_total, :additional_tax_total,
+ :discounted_amount, :display_discounted_amount,
+ :display_additional_tax_total, :promo_total, :display_promo_total,
+ :included_tax_total, :display_included_tax_total
+
+ belongs_to :variant
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/option_type_serializer.rb b/api/app/serializers/spree/v2/storefront/option_type_serializer.rb
new file mode 100644
index 00000000000..ecb31681247
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/option_type_serializer.rb
@@ -0,0 +1,13 @@
+module Spree
+ module V2
+ module Storefront
+ class OptionTypeSerializer < BaseSerializer
+ set_type :option_type
+
+ attributes :name, :presentation, :position
+
+ has_many :option_values
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/option_value_serializer.rb b/api/app/serializers/spree/v2/storefront/option_value_serializer.rb
new file mode 100644
index 00000000000..8275f01a2fe
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/option_value_serializer.rb
@@ -0,0 +1,13 @@
+module Spree
+ module V2
+ module Storefront
+ class OptionValueSerializer < BaseSerializer
+ set_type :option_value
+
+ attributes :name, :presentation, :position
+
+ belongs_to :option_type
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/payment_method_serializer.rb b/api/app/serializers/spree/v2/storefront/payment_method_serializer.rb
new file mode 100644
index 00000000000..de1abf90fee
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/payment_method_serializer.rb
@@ -0,0 +1,11 @@
+module Spree
+ module V2
+ module Storefront
+ class PaymentMethodSerializer < BaseSerializer
+ set_type :payment_method
+
+ attributes :type, :name, :description
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/payment_serializer.rb b/api/app/serializers/spree/v2/storefront/payment_serializer.rb
new file mode 100644
index 00000000000..78e4f0bd978
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/payment_serializer.rb
@@ -0,0 +1,15 @@
+module Spree
+ module V2
+ module Storefront
+ class PaymentSerializer < BaseSerializer
+ set_type :payment
+
+ has_one :source, polymorphic: true
+ has_one :payment_method
+
+ attributes :amount, :response_code, :number, :cvv_response_code, :cvv_response_message,
+ :payment_method_id, :payment_method_name
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/product_property_serializer.rb b/api/app/serializers/spree/v2/storefront/product_property_serializer.rb
new file mode 100644
index 00000000000..166f3ac0776
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/product_property_serializer.rb
@@ -0,0 +1,14 @@
+module Spree
+ module V2
+ module Storefront
+ class ProductPropertySerializer < BaseSerializer
+ set_type :product_property
+
+ attribute :value
+
+ attribute :name, &:property_name
+ attribute :description, &:property_presentation
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/product_serializer.rb b/api/app/serializers/spree/v2/storefront/product_serializer.rb
new file mode 100644
index 00000000000..3af2b073c04
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/product_serializer.rb
@@ -0,0 +1,35 @@
+module Spree
+ module V2
+ module Storefront
+ class ProductSerializer < BaseSerializer
+ set_type :product
+
+ attributes :name, :description, :price, :currency, :display_price,
+ :available_on, :slug, :meta_description, :meta_keywords,
+ :updated_at
+
+ attribute :purchasable, &:purchasable?
+ attribute :in_stock, &:in_stock?
+ attribute :backorderable, &:backorderable?
+
+ has_many :variants
+ has_many :option_types
+ has_many :product_properties
+ has_many :taxons
+
+ # all images from all variants
+ has_many :images,
+ object_method_name: :variant_images,
+ id_method_name: :variant_image_ids,
+ record_type: :image,
+ serializer: :image
+
+ has_one :default_variant,
+ object_method_name: :default_variant,
+ id_method_name: :default_variant_id,
+ record_type: :variant,
+ serializer: :variant
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/promotion_serializer.rb b/api/app/serializers/spree/v2/storefront/promotion_serializer.rb
new file mode 100644
index 00000000000..bdc64fbd3a6
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/promotion_serializer.rb
@@ -0,0 +1,12 @@
+module Spree
+ module V2
+ module Storefront
+ class PromotionSerializer < BaseSerializer
+ set_id :promotion_id
+ set_type :promotion
+
+ attributes :name, :description, :amount, :display_amount, :code
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/shipment_serializer.rb b/api/app/serializers/spree/v2/storefront/shipment_serializer.rb
new file mode 100644
index 00000000000..d7a2c8cf4bd
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/shipment_serializer.rb
@@ -0,0 +1,16 @@
+module Spree
+ module V2
+ module Storefront
+ class ShipmentSerializer < BaseSerializer
+ set_type :shipment
+
+ attributes :number, :final_price, :display_final_price,
+ :state, :shipped_at, :tracking_url
+
+ attribute :free, &:free?
+
+ has_many :shipping_rates, if: proc { |_record, params| params&.dig(:show_rates) }
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/shipping_rate_serializer.rb b/api/app/serializers/spree/v2/storefront/shipping_rate_serializer.rb
new file mode 100644
index 00000000000..dd8d7c16341
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/shipping_rate_serializer.rb
@@ -0,0 +1,14 @@
+module Spree
+ module V2
+ module Storefront
+ class ShippingRateSerializer < BaseSerializer
+ set_type :shipping_rate
+
+ attributes :name, :selected, :final_price, :display_final_price, :cost,
+ :display_cost, :tax_amount, :display_tax_amount, :shipping_method_id
+
+ attribute :free, &:free?
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/state_serializer.rb b/api/app/serializers/spree/v2/storefront/state_serializer.rb
new file mode 100644
index 00000000000..06d5b588ff6
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/state_serializer.rb
@@ -0,0 +1,11 @@
+module Spree
+ module V2
+ module Storefront
+ class StateSerializer < BaseSerializer
+ set_type :state
+
+ attributes :abbr, :name
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/store_credit_event_serializer.rb b/api/app/serializers/spree/v2/storefront/store_credit_event_serializer.rb
new file mode 100644
index 00000000000..e8c34d80089
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/store_credit_event_serializer.rb
@@ -0,0 +1,15 @@
+module Spree
+ module V2
+ module Storefront
+ class StoreCreditEventSerializer < BaseSerializer
+ set_type :store_credit_event
+
+ attributes :action, :amount, :user_total_amount, :created_at
+
+ attribute :order_number do |object|
+ object.order&.number
+ end
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/store_credit_serializer.rb b/api/app/serializers/spree/v2/storefront/store_credit_serializer.rb
new file mode 100644
index 00000000000..18b0cfa1cb8
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/store_credit_serializer.rb
@@ -0,0 +1,17 @@
+module Spree
+ module V2
+ module Storefront
+ class StoreCreditSerializer < BaseSerializer
+ set_type :store_credit
+
+ belongs_to :category
+ has_many :store_credit_events
+ belongs_to :credit_type,
+ id_method_name: :type_id,
+ serializer: :store_credit_type
+
+ attributes :amount, :amount_used, :created_at
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/store_credit_type_serializer.rb b/api/app/serializers/spree/v2/storefront/store_credit_type_serializer.rb
new file mode 100644
index 00000000000..3cb4745a2a6
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/store_credit_type_serializer.rb
@@ -0,0 +1,11 @@
+module Spree
+ module V2
+ module Storefront
+ class StoreCreditTypeSerializer < BaseSerializer
+ set_type :store_credit_type
+
+ attributes :name
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/taxon_image_serializer.rb b/api/app/serializers/spree/v2/storefront/taxon_image_serializer.rb
new file mode 100644
index 00000000000..a18125a20d1
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/taxon_image_serializer.rb
@@ -0,0 +1,11 @@
+module Spree
+ module V2
+ module Storefront
+ class TaxonImageSerializer < BaseSerializer
+ set_type :taxon_image
+
+ attributes :viewable_type, :viewable_id, :styles
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/taxon_serializer.rb b/api/app/serializers/spree/v2/storefront/taxon_serializer.rb
new file mode 100644
index 00000000000..02d38fdce61
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/taxon_serializer.rb
@@ -0,0 +1,28 @@
+module Spree
+ module V2
+ module Storefront
+ class TaxonSerializer < BaseSerializer
+ set_type :taxon
+
+ attributes :name, :pretty_name, :permalink, :seo_title, :meta_title, :meta_description,
+ :meta_keywords, :left, :right, :position, :depth, :updated_at
+
+ attribute :is_root, &:root?
+ attribute :is_child, &:child?
+ attribute :is_leaf, &:leaf?
+
+ belongs_to :parent, record_type: :taxon, serializer: :taxon
+ belongs_to :taxonomy, record_type: :taxonomy
+
+ has_many :children, record_type: :child, serializer: :taxon
+ has_many :products, record_type: :product
+
+ has_one :image,
+ object_method_name: :icon,
+ id_method_name: :icon_id,
+ record_type: :taxon_image,
+ serializer: :taxon_image
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/taxonomy_serializer.rb b/api/app/serializers/spree/v2/storefront/taxonomy_serializer.rb
new file mode 100644
index 00000000000..49e5ff6e8b3
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/taxonomy_serializer.rb
@@ -0,0 +1,11 @@
+module Spree
+ module V2
+ module Storefront
+ class TaxonomySerializer < BaseSerializer
+ set_type :taxonomy
+
+ attributes :name, :position
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/user_serializer.rb b/api/app/serializers/spree/v2/storefront/user_serializer.rb
new file mode 100644
index 00000000000..1736bb653d9
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/user_serializer.rb
@@ -0,0 +1,29 @@
+module Spree
+ module V2
+ module Storefront
+ class UserSerializer < BaseSerializer
+ set_type :user
+
+ attributes :email
+
+ attribute :store_credits, &:total_available_store_credit
+
+ attribute :completed_orders do |object|
+ object.orders.complete.count
+ end
+
+ has_one :default_billing_address,
+ id_method_name: :bill_address_id,
+ object_method_name: :bill_address,
+ record_type: :address,
+ serializer: :address
+
+ has_one :default_shipping_address,
+ id_method_name: :ship_address_id,
+ object_method_name: :ship_address,
+ record_type: :address,
+ serializer: :address
+ end
+ end
+ end
+end
diff --git a/api/app/serializers/spree/v2/storefront/variant_serializer.rb b/api/app/serializers/spree/v2/storefront/variant_serializer.rb
new file mode 100644
index 00000000000..40f575c1140
--- /dev/null
+++ b/api/app/serializers/spree/v2/storefront/variant_serializer.rb
@@ -0,0 +1,20 @@
+module Spree
+ module V2
+ module Storefront
+ class VariantSerializer < BaseSerializer
+ set_type :variant
+
+ attributes :name, :sku, :price, :currency, :display_price, :weight, :height,
+ :width, :depth, :is_master, :options_text, :slug, :description
+
+ attribute :purchasable, &:purchasable?
+ attribute :in_stock, &:in_stock?
+ attribute :backorderable, &:backorderable?
+
+ belongs_to :product
+ has_many :images
+ has_many :option_values
+ end
+ end
+ end
+end
diff --git a/api/app/views/spree/api/errors/gateway_error.v1.rabl b/api/app/views/spree/api/errors/gateway_error.v1.rabl
new file mode 100644
index 00000000000..4df2620dee6
--- /dev/null
+++ b/api/app/views/spree/api/errors/gateway_error.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { I18n.t(:gateway_error, scope: 'spree.api', text: @error) }
diff --git a/api/app/views/spree/api/errors/invalid_api_key.v1.rabl b/api/app/views/spree/api/errors/invalid_api_key.v1.rabl
new file mode 100644
index 00000000000..2c22e71fb38
--- /dev/null
+++ b/api/app/views/spree/api/errors/invalid_api_key.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { I18n.t(:invalid_api_key, key: api_key, scope: 'spree.api') }
diff --git a/api/app/views/spree/api/errors/invalid_resource.v1.rabl b/api/app/views/spree/api/errors/invalid_resource.v1.rabl
new file mode 100644
index 00000000000..5ce64180096
--- /dev/null
+++ b/api/app/views/spree/api/errors/invalid_resource.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:error) { I18n.t(:invalid_resource, scope: 'spree.api') }
+node(:errors) { @resource.errors.to_hash }
diff --git a/api/app/views/spree/api/errors/must_specify_api_key.v1.rabl b/api/app/views/spree/api/errors/must_specify_api_key.v1.rabl
new file mode 100644
index 00000000000..5c34ad9dda9
--- /dev/null
+++ b/api/app/views/spree/api/errors/must_specify_api_key.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { I18n.t(:must_specify_api_key, scope: 'spree.api') }
diff --git a/api/app/views/spree/api/errors/not_found.v1.rabl b/api/app/views/spree/api/errors/not_found.v1.rabl
new file mode 100644
index 00000000000..1fba5b72985
--- /dev/null
+++ b/api/app/views/spree/api/errors/not_found.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { I18n.t(:resource_not_found, scope: 'spree.api') }
diff --git a/api/app/views/spree/api/errors/unauthorized.v1.rabl b/api/app/views/spree/api/errors/unauthorized.v1.rabl
new file mode 100644
index 00000000000..4073205773b
--- /dev/null
+++ b/api/app/views/spree/api/errors/unauthorized.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { I18n.t(:unauthorized, scope: 'spree.api') }
diff --git a/api/app/views/spree/api/v1/addresses/show.v1.rabl b/api/app/views/spree/api/v1/addresses/show.v1.rabl
new file mode 100644
index 00000000000..2d771704bf2
--- /dev/null
+++ b/api/app/views/spree/api/v1/addresses/show.v1.rabl
@@ -0,0 +1,10 @@
+object @address
+cache [I18n.locale, root_object]
+attributes *address_attributes
+
+child(:country) do |_address|
+ attributes *country_attributes
+end
+child(:state) do |_address|
+ attributes *state_attributes
+end
diff --git a/api/app/views/spree/api/v1/adjustments/show.v1.rabl b/api/app/views/spree/api/v1/adjustments/show.v1.rabl
new file mode 100644
index 00000000000..e88c3d801d7
--- /dev/null
+++ b/api/app/views/spree/api/v1/adjustments/show.v1.rabl
@@ -0,0 +1,4 @@
+object @adjustment
+cache [I18n.locale, root_object]
+attributes *adjustment_attributes
+node(:display_amount) { |a| a.display_amount.to_s }
diff --git a/api/app/views/spree/api/v1/countries/index.v1.rabl b/api/app/views/spree/api/v1/countries/index.v1.rabl
new file mode 100644
index 00000000000..d153ae1ed08
--- /dev/null
+++ b/api/app/views/spree/api/v1/countries/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@countries => :countries) do
+ attributes *country_attributes
+end
+node(:count) { @countries.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @countries.total_pages }
diff --git a/api/app/views/spree/api/v1/countries/show.v1.rabl b/api/app/views/spree/api/v1/countries/show.v1.rabl
new file mode 100644
index 00000000000..b485c3c11fd
--- /dev/null
+++ b/api/app/views/spree/api/v1/countries/show.v1.rabl
@@ -0,0 +1,5 @@
+object @country
+attributes *country_attributes
+child states: :states do
+ attributes :id, :name, :abbr, :country_id
+end
diff --git a/api/app/views/spree/api/v1/credit_cards/index.v1.rabl b/api/app/views/spree/api/v1/credit_cards/index.v1.rabl
new file mode 100644
index 00000000000..1eb0e53cf29
--- /dev/null
+++ b/api/app/views/spree/api/v1/credit_cards/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@credit_cards => :credit_cards) do
+ extends 'spree/api/v1/credit_cards/show'
+end
+node(:count) { @credit_cards.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @credit_cards.total_pages }
diff --git a/api/app/views/spree/api/v1/credit_cards/show.v1.rabl b/api/app/views/spree/api/v1/credit_cards/show.v1.rabl
new file mode 100644
index 00000000000..57d734d6abf
--- /dev/null
+++ b/api/app/views/spree/api/v1/credit_cards/show.v1.rabl
@@ -0,0 +1,3 @@
+object @credit_card
+cache [I18n.locale, root_object]
+attributes *creditcard_attributes
diff --git a/api/app/views/spree/api/v1/customer_returns/index.v1.rabl b/api/app/views/spree/api/v1/customer_returns/index.v1.rabl
new file mode 100644
index 00000000000..d9b09853710
--- /dev/null
+++ b/api/app/views/spree/api/v1/customer_returns/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@collection => :customer_returns) do
+ attributes *customer_return_attributes
+end
+node(:count) { @collection.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @collection.total_pages }
diff --git a/api/app/views/spree/api/v1/images/index.v1.rabl b/api/app/views/spree/api/v1/images/index.v1.rabl
new file mode 100644
index 00000000000..60bec605280
--- /dev/null
+++ b/api/app/views/spree/api/v1/images/index.v1.rabl
@@ -0,0 +1,4 @@
+object false
+child(@images => :images) do
+ extends 'spree/api/v1/images/show'
+end
diff --git a/api/app/views/spree/api/v1/images/new.v1.rabl b/api/app/views/spree/api/v1/images/new.v1.rabl
new file mode 100644
index 00000000000..102585c15a0
--- /dev/null
+++ b/api/app/views/spree/api/v1/images/new.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:attributes) { [*image_attributes] }
+node(:required_attributes) { required_fields_for(Spree::Image) }
diff --git a/api/app/views/spree/api/v1/images/show.v1.rabl b/api/app/views/spree/api/v1/images/show.v1.rabl
new file mode 100644
index 00000000000..d8eb5e3e903
--- /dev/null
+++ b/api/app/views/spree/api/v1/images/show.v1.rabl
@@ -0,0 +1,6 @@
+object @image
+attributes *image_attributes
+attributes :viewable_type, :viewable_id
+Spree::Image.styles.each do |k, _v|
+ node("#{k}_url") { |i| main_app.url_for(i.url(k)) }
+end
diff --git a/api/app/views/spree/api/v1/inventory_units/show.rabl b/api/app/views/spree/api/v1/inventory_units/show.rabl
new file mode 100644
index 00000000000..d9e7960b503
--- /dev/null
+++ b/api/app/views/spree/api/v1/inventory_units/show.rabl
@@ -0,0 +1,2 @@
+object @inventory_unit
+attributes *inventory_unit_attributes
diff --git a/api/app/views/spree/api/v1/line_items/new.v1.rabl b/api/app/views/spree/api/v1/line_items/new.v1.rabl
new file mode 100644
index 00000000000..6269ff819ee
--- /dev/null
+++ b/api/app/views/spree/api/v1/line_items/new.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:attributes) { [*line_item_attributes] - [:id] }
+node(:required_attributes) { [:variant_id, :quantity] }
diff --git a/api/app/views/spree/api/v1/line_items/show.v1.rabl b/api/app/views/spree/api/v1/line_items/show.v1.rabl
new file mode 100644
index 00000000000..1de39cb4b09
--- /dev/null
+++ b/api/app/views/spree/api/v1/line_items/show.v1.rabl
@@ -0,0 +1,14 @@
+object @line_item
+cache [I18n.locale, root_object]
+attributes *line_item_attributes
+node(:single_display_amount) { |li| li.single_display_amount.to_s }
+node(:display_amount) { |li| li.display_amount.to_s }
+node(:total, &:total)
+child :variant do
+ extends 'spree/api/v1/variants/small'
+ attributes :product_id
+end
+
+child adjustments: :adjustments do
+ extends 'spree/api/v1/adjustments/show'
+end
diff --git a/api/app/views/spree/api/v1/option_types/index.v1.rabl b/api/app/views/spree/api/v1/option_types/index.v1.rabl
new file mode 100644
index 00000000000..3f1f70659b7
--- /dev/null
+++ b/api/app/views/spree/api/v1/option_types/index.v1.rabl
@@ -0,0 +1,3 @@
+collection @option_types
+
+extends 'spree/api/v1/option_types/show'
diff --git a/api/app/views/spree/api/v1/option_types/new.v1.rabl b/api/app/views/spree/api/v1/option_types/new.v1.rabl
new file mode 100644
index 00000000000..74a74b7dfb8
--- /dev/null
+++ b/api/app/views/spree/api/v1/option_types/new.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:attributes) { [*option_type_attributes] }
+node(:required_attributes) { required_fields_for(Spree::OptionType) }
diff --git a/api/app/views/spree/api/v1/option_types/show.v1.rabl b/api/app/views/spree/api/v1/option_types/show.v1.rabl
new file mode 100644
index 00000000000..8e8d0b33668
--- /dev/null
+++ b/api/app/views/spree/api/v1/option_types/show.v1.rabl
@@ -0,0 +1,5 @@
+object @option_type
+attributes *option_type_attributes
+child option_values: :option_values do
+ attributes *option_value_attributes
+end
diff --git a/api/app/views/spree/api/v1/option_values/index.v1.rabl b/api/app/views/spree/api/v1/option_values/index.v1.rabl
new file mode 100644
index 00000000000..d3592c98cd2
--- /dev/null
+++ b/api/app/views/spree/api/v1/option_values/index.v1.rabl
@@ -0,0 +1,3 @@
+collection @option_values
+
+extends 'spree/api/v1/option_values/show'
diff --git a/api/app/views/spree/api/v1/option_values/new.v1.rabl b/api/app/views/spree/api/v1/option_values/new.v1.rabl
new file mode 100644
index 00000000000..662f9de4485
--- /dev/null
+++ b/api/app/views/spree/api/v1/option_values/new.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:attributes) { [*option_value_attributes] }
+node(:required_attributes) { required_fields_for(Spree::OptionValue) }
diff --git a/api/app/views/spree/api/v1/option_values/show.v1.rabl b/api/app/views/spree/api/v1/option_values/show.v1.rabl
new file mode 100644
index 00000000000..a3f6680d2ef
--- /dev/null
+++ b/api/app/views/spree/api/v1/option_values/show.v1.rabl
@@ -0,0 +1,2 @@
+object @option_value
+attributes *option_value_attributes
diff --git a/public/favicon.ico b/api/app/views/spree/api/v1/orders/address.v1.rabl
similarity index 100%
rename from public/favicon.ico
rename to api/app/views/spree/api/v1/orders/address.v1.rabl
diff --git a/public/plugin_assets/spree/favicon.ico b/api/app/views/spree/api/v1/orders/canceled.v1.rabl
similarity index 100%
rename from public/plugin_assets/spree/favicon.ico
rename to api/app/views/spree/api/v1/orders/canceled.v1.rabl
diff --git a/vendor/plugins/acts_as_tree/test/abstract_unit.rb b/api/app/views/spree/api/v1/orders/cart.v1.rabl
similarity index 100%
rename from vendor/plugins/acts_as_tree/test/abstract_unit.rb
rename to api/app/views/spree/api/v1/orders/cart.v1.rabl
diff --git a/vendor/plugins/acts_as_tree/test/database.yml b/api/app/views/spree/api/v1/orders/complete.v1.rabl
similarity index 100%
rename from vendor/plugins/acts_as_tree/test/database.yml
rename to api/app/views/spree/api/v1/orders/complete.v1.rabl
diff --git a/api/app/views/spree/api/v1/orders/could_not_apply_coupon.v1.rabl b/api/app/views/spree/api/v1/orders/could_not_apply_coupon.v1.rabl
new file mode 100644
index 00000000000..0a457130637
--- /dev/null
+++ b/api/app/views/spree/api/v1/orders/could_not_apply_coupon.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { @coupon_message }
diff --git a/api/app/views/spree/api/v1/orders/could_not_transition.v1.rabl b/api/app/views/spree/api/v1/orders/could_not_transition.v1.rabl
new file mode 100644
index 00000000000..c2bfb5c84a5
--- /dev/null
+++ b/api/app/views/spree/api/v1/orders/could_not_transition.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:error) { I18n.t(:could_not_transition, scope: 'spree.api.order') }
+node(:errors) { @order.errors.to_hash }
diff --git a/api/app/views/spree/api/v1/orders/index.v1.rabl b/api/app/views/spree/api/v1/orders/index.v1.rabl
new file mode 100644
index 00000000000..27acb95f307
--- /dev/null
+++ b/api/app/views/spree/api/v1/orders/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@orders => :orders) do
+ extends 'spree/api/v1/orders/order'
+end
+node(:count) { @orders.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @orders.total_pages }
diff --git a/api/app/views/spree/api/v1/orders/insufficient_quantity.v1.rabl b/api/app/views/spree/api/v1/orders/insufficient_quantity.v1.rabl
new file mode 100644
index 00000000000..2cd37ffdf0e
--- /dev/null
+++ b/api/app/views/spree/api/v1/orders/insufficient_quantity.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { Spree.t(:insufficient_quantity, scope: [:api, :order]) }
diff --git a/api/app/views/spree/api/v1/orders/invalid_shipping_method.v1.rabl b/api/app/views/spree/api/v1/orders/invalid_shipping_method.v1.rabl
new file mode 100644
index 00000000000..9876194d71b
--- /dev/null
+++ b/api/app/views/spree/api/v1/orders/invalid_shipping_method.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:errors) { [I18n.t(:invalid_shipping_method, scope: 'spree.api.order')] }
diff --git a/api/app/views/spree/api/v1/orders/mine.v1.rabl b/api/app/views/spree/api/v1/orders/mine.v1.rabl
new file mode 100644
index 00000000000..ce5284216be
--- /dev/null
+++ b/api/app/views/spree/api/v1/orders/mine.v1.rabl
@@ -0,0 +1,9 @@
+object false
+
+child(@orders => :orders) do
+ extends 'spree/api/v1/orders/show'
+end
+
+node(:count) { @orders.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @orders.total_pages }
diff --git a/api/app/views/spree/api/v1/orders/order.v1.rabl b/api/app/views/spree/api/v1/orders/order.v1.rabl
new file mode 100644
index 00000000000..fda367071c3
--- /dev/null
+++ b/api/app/views/spree/api/v1/orders/order.v1.rabl
@@ -0,0 +1,10 @@
+cache [I18n.locale, root_object]
+attributes *order_attributes
+node(:display_item_total) { |o| o.display_item_total.to_s }
+node(:total_quantity) { |o| o.line_items.sum(:quantity) }
+node(:display_total) { |o| o.display_total.to_s }
+node(:display_ship_total, &:display_ship_total)
+node(:display_tax_total, &:display_tax_total)
+node(:display_adjustment_total, &:display_adjustment_total)
+node(:token, &:token)
+node(:checkout_steps, &:checkout_steps)
diff --git a/api/app/views/spree/api/v1/orders/payment.v1.rabl b/api/app/views/spree/api/v1/orders/payment.v1.rabl
new file mode 100644
index 00000000000..0533ca838a0
--- /dev/null
+++ b/api/app/views/spree/api/v1/orders/payment.v1.rabl
@@ -0,0 +1,3 @@
+child available_payment_methods: :payment_methods do
+ attributes :id, :name, :method_type
+end
diff --git a/api/app/views/spree/api/v1/orders/show.v1.rabl b/api/app/views/spree/api/v1/orders/show.v1.rabl
new file mode 100644
index 00000000000..6ae5a97b3b4
--- /dev/null
+++ b/api/app/views/spree/api/v1/orders/show.v1.rabl
@@ -0,0 +1,51 @@
+object @order
+extends 'spree/api/v1/orders/order'
+
+if lookup_context.find_all("spree/api/v1/orders/#{root_object.state}").present?
+ extends "spree/api/v1/orders/#{root_object.state}"
+end
+
+child billing_address: :bill_address do
+ extends 'spree/api/v1/addresses/show'
+end
+
+child shipping_address: :ship_address do
+ extends 'spree/api/v1/addresses/show'
+end
+
+child line_items: :line_items do
+ extends 'spree/api/v1/line_items/show'
+end
+
+child payments: :payments do
+ attributes *payment_attributes
+
+ child payment_method: :payment_method do
+ attributes :id, :name
+ end
+
+ child source: :source do
+ if @current_user_roles.include?('admin')
+ attributes *payment_source_attributes + [:gateway_customer_profile_id, :gateway_payment_profile_id]
+ else
+ attributes *payment_source_attributes
+ end
+ end
+end
+
+child shipments: :shipments do
+ extends 'spree/api/v1/shipments/small'
+end
+
+child adjustments: :adjustments do
+ extends 'spree/api/v1/adjustments/show'
+end
+
+# Necessary for backend's order interface
+node :permissions do
+ { can_update: current_ability.can?(:update, root_object) }
+end
+
+child valid_credit_cards: :credit_cards do
+ extends 'spree/api/v1/credit_cards/show'
+end
diff --git a/api/app/views/spree/api/v1/payments/credit_over_limit.v1.rabl b/api/app/views/spree/api/v1/payments/credit_over_limit.v1.rabl
new file mode 100644
index 00000000000..e6ca9336fa9
--- /dev/null
+++ b/api/app/views/spree/api/v1/payments/credit_over_limit.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { I18n.t(:credit_over_limit, limit: @payment.credit_allowed, scope: 'spree.api.payment') }
diff --git a/api/app/views/spree/api/v1/payments/index.v1.rabl b/api/app/views/spree/api/v1/payments/index.v1.rabl
new file mode 100644
index 00000000000..b816d9e47fc
--- /dev/null
+++ b/api/app/views/spree/api/v1/payments/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@payments => :payments) do
+ attributes *payment_attributes
+end
+node(:count) { @payments.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @payments.total_pages }
diff --git a/api/app/views/spree/api/v1/payments/new.v1.rabl b/api/app/views/spree/api/v1/payments/new.v1.rabl
new file mode 100644
index 00000000000..05d227cf83e
--- /dev/null
+++ b/api/app/views/spree/api/v1/payments/new.v1.rabl
@@ -0,0 +1,5 @@
+object false
+node(:attributes) { [*payment_attributes] }
+child @payment_methods => :payment_methods do
+ attributes *payment_method_attributes
+end
diff --git a/api/app/views/spree/api/v1/payments/show.v1.rabl b/api/app/views/spree/api/v1/payments/show.v1.rabl
new file mode 100644
index 00000000000..e2f48cb940a
--- /dev/null
+++ b/api/app/views/spree/api/v1/payments/show.v1.rabl
@@ -0,0 +1,2 @@
+object @payment
+attributes *payment_attributes
diff --git a/api/app/views/spree/api/v1/payments/update_forbidden.v1.rabl b/api/app/views/spree/api/v1/payments/update_forbidden.v1.rabl
new file mode 100644
index 00000000000..9ce6b061fc6
--- /dev/null
+++ b/api/app/views/spree/api/v1/payments/update_forbidden.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { I18n.t(:update_forbidden, state: @payment.state, scope: 'spree.api.payment') }
diff --git a/api/app/views/spree/api/v1/product_properties/index.v1.rabl b/api/app/views/spree/api/v1/product_properties/index.v1.rabl
new file mode 100644
index 00000000000..2a04d57282c
--- /dev/null
+++ b/api/app/views/spree/api/v1/product_properties/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@product_properties => :product_properties) do
+ attributes *product_property_attributes
+end
+node(:count) { @product_properties.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @product_properties.total_pages }
diff --git a/api/app/views/spree/api/v1/product_properties/new.v1.rabl b/api/app/views/spree/api/v1/product_properties/new.v1.rabl
new file mode 100644
index 00000000000..37067119816
--- /dev/null
+++ b/api/app/views/spree/api/v1/product_properties/new.v1.rabl
@@ -0,0 +1,2 @@
+node(:attributes) { [*product_property_attributes] }
+node(:required_attributes) { [] }
diff --git a/api/app/views/spree/api/v1/product_properties/show.v1.rabl b/api/app/views/spree/api/v1/product_properties/show.v1.rabl
new file mode 100644
index 00000000000..12e24137b53
--- /dev/null
+++ b/api/app/views/spree/api/v1/product_properties/show.v1.rabl
@@ -0,0 +1,2 @@
+object @product_property
+attributes *product_property_attributes
diff --git a/api/app/views/spree/api/v1/products/index.v1.rabl b/api/app/views/spree/api/v1/products/index.v1.rabl
new file mode 100644
index 00000000000..faca145d230
--- /dev/null
+++ b/api/app/views/spree/api/v1/products/index.v1.rabl
@@ -0,0 +1,9 @@
+object false
+node(:count) { @products.count }
+node(:total_count) { @products.total_count }
+node(:current_page) { params[:page] ? params[:page].to_i : 1 }
+node(:per_page) { params[:per_page].try(:to_i) || Kaminari.config.default_per_page }
+node(:pages) { @products.total_pages }
+child(@products => :products) do
+ extends 'spree/api/v1/products/show'
+end
diff --git a/api/app/views/spree/api/v1/products/new.v1.rabl b/api/app/views/spree/api/v1/products/new.v1.rabl
new file mode 100644
index 00000000000..71861f91837
--- /dev/null
+++ b/api/app/views/spree/api/v1/products/new.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:attributes) { [*product_attributes] }
+node(:required_attributes) { required_fields_for(Spree::Product) }
diff --git a/api/app/views/spree/api/v1/products/product.v1.rabl b/api/app/views/spree/api/v1/products/product.v1.rabl
new file mode 100644
index 00000000000..ba18e3f709f
--- /dev/null
+++ b/api/app/views/spree/api/v1/products/product.v1.rabl
@@ -0,0 +1 @@
+attributes *product_attributes
diff --git a/api/app/views/spree/api/v1/products/show.v1.rabl b/api/app/views/spree/api/v1/products/show.v1.rabl
new file mode 100644
index 00000000000..b6d2472ced3
--- /dev/null
+++ b/api/app/views/spree/api/v1/products/show.v1.rabl
@@ -0,0 +1,32 @@
+object @product
+cache [I18n.locale, @current_user_roles.include?('admin'), current_currency, root_object]
+
+attributes *product_attributes
+
+node(:display_price) { |p| p.display_price.to_s }
+node(:has_variants, &:has_variants?)
+node(:taxon_ids, &:taxon_ids)
+
+child master: :master do
+ extends 'spree/api/v1/variants/small'
+end
+
+child variants: :variants do
+ extends 'spree/api/v1/variants/small'
+end
+
+child option_types: :option_types do
+ attributes *option_type_attributes
+end
+
+child product_properties: :product_properties do
+ attributes *product_property_attributes
+end
+
+child classifications: :classifications do
+ attributes :taxon_id, :position
+
+ child(:taxon) do
+ extends 'spree/api/v1/taxons/show'
+ end
+end
diff --git a/api/app/views/spree/api/v1/promotions/handler.v1.rabl b/api/app/views/spree/api/v1/promotions/handler.v1.rabl
new file mode 100644
index 00000000000..e08a89b08ee
--- /dev/null
+++ b/api/app/views/spree/api/v1/promotions/handler.v1.rabl
@@ -0,0 +1,5 @@
+object false
+node(:success) { @handler.success }
+node(:error) { @handler.error }
+node(:successful) { @handler.successful? }
+node(:status_code) { @handler.status_code }
diff --git a/api/app/views/spree/api/v1/promotions/show.v1.rabl b/api/app/views/spree/api/v1/promotions/show.v1.rabl
new file mode 100644
index 00000000000..90a577d22ab
--- /dev/null
+++ b/api/app/views/spree/api/v1/promotions/show.v1.rabl
@@ -0,0 +1,2 @@
+object @promotion
+attributes *promotion_attributes
diff --git a/api/app/views/spree/api/v1/properties/index.v1.rabl b/api/app/views/spree/api/v1/properties/index.v1.rabl
new file mode 100644
index 00000000000..7a8ca7223c1
--- /dev/null
+++ b/api/app/views/spree/api/v1/properties/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@properties => :properties) do
+ attributes *property_attributes
+end
+node(:count) { @properties.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @properties.total_pages }
diff --git a/api/app/views/spree/api/v1/properties/new.v1.rabl b/api/app/views/spree/api/v1/properties/new.v1.rabl
new file mode 100644
index 00000000000..76f03444e8e
--- /dev/null
+++ b/api/app/views/spree/api/v1/properties/new.v1.rabl
@@ -0,0 +1,2 @@
+node(:attributes) { [*property_attributes] }
+node(:required_attributes) { [] }
diff --git a/api/app/views/spree/api/v1/properties/show.v1.rabl b/api/app/views/spree/api/v1/properties/show.v1.rabl
new file mode 100644
index 00000000000..08cfbe5e9b6
--- /dev/null
+++ b/api/app/views/spree/api/v1/properties/show.v1.rabl
@@ -0,0 +1,2 @@
+object @property
+attributes *property_attributes
diff --git a/api/app/views/spree/api/v1/reimbursements/index.v1.rabl b/api/app/views/spree/api/v1/reimbursements/index.v1.rabl
new file mode 100644
index 00000000000..b3d47cf0503
--- /dev/null
+++ b/api/app/views/spree/api/v1/reimbursements/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@collection => :reimbursements) do
+ attributes *reimbursement_attributes
+end
+node(:count) { @collection.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @collection.total_pages }
diff --git a/api/app/views/spree/api/v1/return_authorizations/index.v1.rabl b/api/app/views/spree/api/v1/return_authorizations/index.v1.rabl
new file mode 100644
index 00000000000..1410d4de9d5
--- /dev/null
+++ b/api/app/views/spree/api/v1/return_authorizations/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@return_authorizations => :return_authorizations) do
+ attributes *return_authorization_attributes
+end
+node(:count) { @return_authorizations.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @return_authorizations.total_pages }
diff --git a/api/app/views/spree/api/v1/return_authorizations/new.v1.rabl b/api/app/views/spree/api/v1/return_authorizations/new.v1.rabl
new file mode 100644
index 00000000000..362fa8ac9de
--- /dev/null
+++ b/api/app/views/spree/api/v1/return_authorizations/new.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:attributes) { [*return_authorization_attributes] }
+node(:required_attributes) { required_fields_for(Spree::ReturnAuthorization) }
diff --git a/api/app/views/spree/api/v1/return_authorizations/show.v1.rabl b/api/app/views/spree/api/v1/return_authorizations/show.v1.rabl
new file mode 100644
index 00000000000..72bfb859110
--- /dev/null
+++ b/api/app/views/spree/api/v1/return_authorizations/show.v1.rabl
@@ -0,0 +1,2 @@
+object @return_authorization
+attributes *return_authorization_attributes
diff --git a/api/app/views/spree/api/v1/shared/stock_location_required.v1.rabl b/api/app/views/spree/api/v1/shared/stock_location_required.v1.rabl
new file mode 100644
index 00000000000..330bee5df9f
--- /dev/null
+++ b/api/app/views/spree/api/v1/shared/stock_location_required.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { I18n.t(:stock_location_required, scope: 'spree.api') }
diff --git a/api/app/views/spree/api/v1/shipments/big.v1.rabl b/api/app/views/spree/api/v1/shipments/big.v1.rabl
new file mode 100644
index 00000000000..7776810a7c4
--- /dev/null
+++ b/api/app/views/spree/api/v1/shipments/big.v1.rabl
@@ -0,0 +1,48 @@
+object @shipment
+cache @shipment
+attributes *shipment_attributes
+
+child selected_shipping_rate: :selected_shipping_rate do
+ extends 'spree/api/v1/shipping_rates/show'
+end
+
+child inventory_units: :inventory_units do
+ object @inventory_unit
+ attributes *inventory_unit_attributes
+
+ child :variant do
+ extends 'spree/api/v1/variants/small'
+ attributes :product_id
+ child(images: :images) { extends 'spree/api/v1/images/show' }
+ end
+
+ child :line_item do
+ attributes *line_item_attributes
+ node(:single_display_amount) { |li| li.single_display_amount.to_s }
+ node(:display_amount) { |li| li.display_amount.to_s }
+ node(:total, &:total)
+ end
+end
+
+child order: :order do
+ extends 'spree/api/v1/orders/order'
+
+ child billing_address: :bill_address do
+ extends 'spree/api/v1/addresses/show'
+ end
+
+ child shipping_address: :ship_address do
+ extends 'spree/api/v1/addresses/show'
+ end
+
+ child adjustments: :adjustments do
+ extends 'spree/api/v1/adjustments/show'
+ end
+
+ child payments: :payments do
+ attributes :id, :amount, :display_amount, :state
+ child payment_method: :payment_method do
+ attributes :id, :name
+ end
+ end
+end
diff --git a/api/app/views/spree/api/v1/shipments/cannot_ready_shipment.v1.rabl b/api/app/views/spree/api/v1/shipments/cannot_ready_shipment.v1.rabl
new file mode 100644
index 00000000000..8481b710c85
--- /dev/null
+++ b/api/app/views/spree/api/v1/shipments/cannot_ready_shipment.v1.rabl
@@ -0,0 +1,2 @@
+object false
+node(:error) { I18n.t(:cannot_ready, scope: 'spree.api.shipment') }
diff --git a/api/app/views/spree/api/v1/shipments/mine.v1.rabl b/api/app/views/spree/api/v1/shipments/mine.v1.rabl
new file mode 100644
index 00000000000..8a4b5e83b7f
--- /dev/null
+++ b/api/app/views/spree/api/v1/shipments/mine.v1.rabl
@@ -0,0 +1,9 @@
+object false
+
+node(:count) { @shipments.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @shipments.total_pages }
+
+child(@shipments => :shipments) do
+ extends 'spree/api/v1/shipments/big'
+end
diff --git a/api/app/views/spree/api/v1/shipments/show.v1.rabl b/api/app/views/spree/api/v1/shipments/show.v1.rabl
new file mode 100644
index 00000000000..f9b069fc88c
--- /dev/null
+++ b/api/app/views/spree/api/v1/shipments/show.v1.rabl
@@ -0,0 +1,32 @@
+object @shipment
+cache [I18n.locale, root_object]
+attributes *shipment_attributes
+node(:order_id) { |shipment| shipment.order.number }
+node(:stock_location_name) { |shipment| shipment.stock_location.name }
+
+child shipping_rates: :shipping_rates do
+ extends 'spree/api/v1/shipping_rates/show'
+end
+
+child selected_shipping_rate: :selected_shipping_rate do
+ extends 'spree/api/v1/shipping_rates/show'
+end
+
+child shipping_methods: :shipping_methods do
+ attributes :id, :name, :tracking_url
+ child zones: :zones do
+ attributes :id, :name, :description
+ end
+
+ child shipping_categories: :shipping_categories do
+ attributes :id, :name
+ end
+end
+
+child manifest: :manifest do
+ child variant: :variant do
+ extends 'spree/api/v1/variants/small'
+ end
+ node(:quantity, &:quantity)
+ node(:states, &:states)
+end
diff --git a/api/app/views/spree/api/v1/shipments/small.v1.rabl b/api/app/views/spree/api/v1/shipments/small.v1.rabl
new file mode 100644
index 00000000000..f826dd6de92
--- /dev/null
+++ b/api/app/views/spree/api/v1/shipments/small.v1.rabl
@@ -0,0 +1,37 @@
+object @shipment
+cache [I18n.locale, 'small_shipment', root_object]
+
+attributes *shipment_attributes
+node(:order_id) { |shipment| shipment.order.number }
+node(:stock_location_name) { |shipment| shipment.stock_location.name }
+
+child shipping_rates: :shipping_rates do
+ extends 'spree/api/v1/shipping_rates/show'
+end
+
+child selected_shipping_rate: :selected_shipping_rate do
+ extends 'spree/api/v1/shipping_rates/show'
+end
+
+child shipping_methods: :shipping_methods do
+ attributes :id, :code, :name
+ child zones: :zones do
+ attributes :id, :name, :description
+ end
+
+ child shipping_categories: :shipping_categories do
+ attributes :id, :name
+ end
+end
+
+child manifest: :manifest do
+ glue(:variant) do
+ attribute id: :variant_id
+ end
+ node(:quantity, &:quantity)
+ node(:states, &:states)
+end
+
+child adjustments: :adjustments do
+ extends 'spree/api/v1/adjustments/show'
+end
diff --git a/api/app/views/spree/api/v1/shipping_rates/show.v1.rabl b/api/app/views/spree/api/v1/shipping_rates/show.v1.rabl
new file mode 100644
index 00000000000..6b06d3102d3
--- /dev/null
+++ b/api/app/views/spree/api/v1/shipping_rates/show.v1.rabl
@@ -0,0 +1,2 @@
+attributes :id, :name, :cost, :selected, :shipping_method_id, :shipping_method_code
+node(:display_cost) { |sr| sr.display_cost.to_s }
diff --git a/api/app/views/spree/api/v1/states/index.v1.rabl b/api/app/views/spree/api/v1/states/index.v1.rabl
new file mode 100644
index 00000000000..5ffad7024d1
--- /dev/null
+++ b/api/app/views/spree/api/v1/states/index.v1.rabl
@@ -0,0 +1,12 @@
+object false
+node(:states_required) { @country.states_required } if @country
+
+child(@states => :states) do
+ attributes *state_attributes
+end
+
+if @states.respond_to?(:total_pages)
+ node(:count) { @states.count }
+ node(:current_page) { params[:page].try(:to_i) || 1 }
+ node(:pages) { @states.total_pages }
+end
diff --git a/api/app/views/spree/api/v1/states/show.v1.rabl b/api/app/views/spree/api/v1/states/show.v1.rabl
new file mode 100644
index 00000000000..3b7533d511b
--- /dev/null
+++ b/api/app/views/spree/api/v1/states/show.v1.rabl
@@ -0,0 +1,2 @@
+object @state
+attributes *state_attributes
diff --git a/api/app/views/spree/api/v1/stock_items/index.v1.rabl b/api/app/views/spree/api/v1/stock_items/index.v1.rabl
new file mode 100644
index 00000000000..350cb1e609d
--- /dev/null
+++ b/api/app/views/spree/api/v1/stock_items/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@stock_items => :stock_items) do
+ extends 'spree/api/v1/stock_items/show'
+end
+node(:count) { @stock_items.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @stock_items.total_pages }
diff --git a/api/app/views/spree/api/v1/stock_items/show.v1.rabl b/api/app/views/spree/api/v1/stock_items/show.v1.rabl
new file mode 100644
index 00000000000..1047974e9c5
--- /dev/null
+++ b/api/app/views/spree/api/v1/stock_items/show.v1.rabl
@@ -0,0 +1,5 @@
+object @stock_item
+attributes *stock_item_attributes
+child(:variant) do
+ extends 'spree/api/v1/variants/small'
+end
diff --git a/api/app/views/spree/api/v1/stock_locations/index.v1.rabl b/api/app/views/spree/api/v1/stock_locations/index.v1.rabl
new file mode 100644
index 00000000000..bbd6cd04610
--- /dev/null
+++ b/api/app/views/spree/api/v1/stock_locations/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@stock_locations => :stock_locations) do
+ extends 'spree/api/v1/stock_locations/show'
+end
+node(:count) { @stock_locations.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @stock_locations.total_pages }
diff --git a/api/app/views/spree/api/v1/stock_locations/show.v1.rabl b/api/app/views/spree/api/v1/stock_locations/show.v1.rabl
new file mode 100644
index 00000000000..f5c74e3b96d
--- /dev/null
+++ b/api/app/views/spree/api/v1/stock_locations/show.v1.rabl
@@ -0,0 +1,8 @@
+object @stock_location
+attributes *stock_location_attributes
+child(:country) do |_address|
+ attributes *country_attributes
+end
+child(:state) do |_address|
+ attributes *state_attributes
+end
diff --git a/api/app/views/spree/api/v1/stock_movements/index.v1.rabl b/api/app/views/spree/api/v1/stock_movements/index.v1.rabl
new file mode 100644
index 00000000000..6e098424c4f
--- /dev/null
+++ b/api/app/views/spree/api/v1/stock_movements/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@stock_movements => :stock_movements) do
+ extends 'spree/api/v1/stock_movements/show'
+end
+node(:count) { @stock_movements.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @stock_movements.total_pages }
diff --git a/api/app/views/spree/api/v1/stock_movements/show.v1.rabl b/api/app/views/spree/api/v1/stock_movements/show.v1.rabl
new file mode 100644
index 00000000000..8c24b4bae26
--- /dev/null
+++ b/api/app/views/spree/api/v1/stock_movements/show.v1.rabl
@@ -0,0 +1,5 @@
+object @stock_movement
+attributes *stock_movement_attributes
+child :stock_item do
+ extends 'spree/api/v1/stock_items/show'
+end
diff --git a/api/app/views/spree/api/v1/stores/index.v1.rabl b/api/app/views/spree/api/v1/stores/index.v1.rabl
new file mode 100644
index 00000000000..ae547356bb7
--- /dev/null
+++ b/api/app/views/spree/api/v1/stores/index.v1.rabl
@@ -0,0 +1,4 @@
+object false
+child(@stores => :stores) do
+ attributes *store_attributes
+end
diff --git a/api/app/views/spree/api/v1/stores/show.v1.rabl b/api/app/views/spree/api/v1/stores/show.v1.rabl
new file mode 100644
index 00000000000..a0e1941b723
--- /dev/null
+++ b/api/app/views/spree/api/v1/stores/show.v1.rabl
@@ -0,0 +1,2 @@
+object @store
+attributes *store_attributes
diff --git a/api/app/views/spree/api/v1/tags/index.v1.rabl b/api/app/views/spree/api/v1/tags/index.v1.rabl
new file mode 100644
index 00000000000..339df7ea387
--- /dev/null
+++ b/api/app/views/spree/api/v1/tags/index.v1.rabl
@@ -0,0 +1,9 @@
+object false
+node(:count) { @tags.count }
+node(:total_count) { @tags.total_count }
+node(:current_page) { params[:page] ? params[:page].to_i : 1 }
+node(:per_page) { params[:per_page] || Kaminari.config.default_per_page }
+node(:pages) { @tags.total_pages }
+child(@tags => :tags) do
+ attributes :name, :id
+end
diff --git a/api/app/views/spree/api/v1/taxonomies/index.v1.rabl b/api/app/views/spree/api/v1/taxonomies/index.v1.rabl
new file mode 100644
index 00000000000..0febb17df5a
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxonomies/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@taxonomies => :taxonomies) do
+ extends 'spree/api/v1/taxonomies/show'
+end
+node(:count) { @taxonomies.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @taxonomies.total_pages }
diff --git a/api/app/views/spree/api/v1/taxonomies/jstree.rabl b/api/app/views/spree/api/v1/taxonomies/jstree.rabl
new file mode 100644
index 00000000000..727debf2dc7
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxonomies/jstree.rabl
@@ -0,0 +1,7 @@
+object false
+node(:data) { @taxonomy.root.name }
+node(:attr) do
+ { id: @taxonomy.root.id,
+ name: @taxonomy.root.name }
+end
+node(:state) { 'closed' }
diff --git a/api/app/views/spree/api/v1/taxonomies/nested.v1.rabl b/api/app/views/spree/api/v1/taxonomies/nested.v1.rabl
new file mode 100644
index 00000000000..1314df59745
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxonomies/nested.v1.rabl
@@ -0,0 +1,11 @@
+attributes *taxonomy_attributes
+
+child root: :root do
+ attributes *taxon_attributes
+
+ child children: :taxons do
+ attributes *taxon_attributes
+
+ extends 'spree/api/v1/taxons/taxons'
+ end
+end
diff --git a/api/app/views/spree/api/v1/taxonomies/new.v1.rabl b/api/app/views/spree/api/v1/taxonomies/new.v1.rabl
new file mode 100644
index 00000000000..a3dd32d4efd
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxonomies/new.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:attributes) { [*taxonomy_attributes] }
+node(:required_attributes) { required_fields_for(Spree::Taxonomy) }
diff --git a/api/app/views/spree/api/v1/taxonomies/show.v1.rabl b/api/app/views/spree/api/v1/taxonomies/show.v1.rabl
new file mode 100644
index 00000000000..a99bddcfdd1
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxonomies/show.v1.rabl
@@ -0,0 +1,15 @@
+object @taxonomy
+
+if params[:set] == 'nested'
+ extends 'spree/api/v1/taxonomies/nested'
+else
+ attributes *taxonomy_attributes
+
+ child root: :root do
+ attributes *taxon_attributes
+
+ child children: :taxons do
+ attributes *taxon_attributes
+ end
+ end
+end
diff --git a/api/app/views/spree/api/v1/taxons/index.v1.rabl b/api/app/views/spree/api/v1/taxons/index.v1.rabl
new file mode 100644
index 00000000000..6256e952b1a
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxons/index.v1.rabl
@@ -0,0 +1,10 @@
+object false
+node(:count) { @taxons.count }
+node(:total_count) { @taxons.total_count }
+node(:current_page) { params[:page] ? params[:page].to_i : 1 }
+node(:per_page) { params[:per_page].try(:to_i) || Kaminari.config.default_per_page }
+node(:pages) { @taxons.total_pages }
+child @taxons => :taxons do
+ attributes *taxon_attributes
+ extends 'spree/api/v1/taxons/taxons' unless params[:without_children]
+end
diff --git a/api/app/views/spree/api/v1/taxons/jstree.rabl b/api/app/views/spree/api/v1/taxons/jstree.rabl
new file mode 100644
index 00000000000..fdfdaae5f51
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxons/jstree.rabl
@@ -0,0 +1,7 @@
+collection @taxon.children, object_root: false
+node(:data, &:name)
+node(:attr) do |taxon|
+ { id: taxon.id,
+ name: taxon.name }
+end
+node(:state) { 'closed' }
diff --git a/api/app/views/spree/api/v1/taxons/new.v1.rabl b/api/app/views/spree/api/v1/taxons/new.v1.rabl
new file mode 100644
index 00000000000..f85dbece9ea
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxons/new.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:attributes) { [*taxon_attributes] }
+node(:required_attributes) { required_fields_for(Spree::Taxon) }
diff --git a/api/app/views/spree/api/v1/taxons/show.v1.rabl b/api/app/views/spree/api/v1/taxons/show.v1.rabl
new file mode 100644
index 00000000000..95f485715f7
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxons/show.v1.rabl
@@ -0,0 +1,6 @@
+object @taxon
+attributes *taxon_attributes
+
+child children: :taxons do
+ attributes *taxon_attributes
+end
diff --git a/api/app/views/spree/api/v1/taxons/taxons.v1.rabl b/api/app/views/spree/api/v1/taxons/taxons.v1.rabl
new file mode 100644
index 00000000000..3470decfc1f
--- /dev/null
+++ b/api/app/views/spree/api/v1/taxons/taxons.v1.rabl
@@ -0,0 +1,5 @@
+attributes *taxon_attributes
+
+node :taxons do |t|
+ t.children.map { |c| partial('spree/api/v1/taxons/taxons', object: c) }
+end
diff --git a/api/app/views/spree/api/v1/users/index.v1.rabl b/api/app/views/spree/api/v1/users/index.v1.rabl
new file mode 100644
index 00000000000..f481400797c
--- /dev/null
+++ b/api/app/views/spree/api/v1/users/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@users => :users) do
+ extends 'spree/api/v1/users/show'
+end
+node(:count) { @users.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @users.total_pages }
diff --git a/api/app/views/spree/api/v1/users/new.v1.rabl b/api/app/views/spree/api/v1/users/new.v1.rabl
new file mode 100644
index 00000000000..f3e36dd7d33
--- /dev/null
+++ b/api/app/views/spree/api/v1/users/new.v1.rabl
@@ -0,0 +1,3 @@
+object false
+node(:attributes) { [*user_attributes] }
+node(:required_attributes) { required_fields_for(Spree.user_class) }
diff --git a/api/app/views/spree/api/v1/users/show.v1.rabl b/api/app/views/spree/api/v1/users/show.v1.rabl
new file mode 100644
index 00000000000..b5f680a93aa
--- /dev/null
+++ b/api/app/views/spree/api/v1/users/show.v1.rabl
@@ -0,0 +1,11 @@
+object @user
+cache [I18n.locale, root_object]
+
+attributes *user_attributes
+child(bill_address: :bill_address) do
+ extends 'spree/api/v1/addresses/show'
+end
+
+child(ship_address: :ship_address) do
+ extends 'spree/api/v1/addresses/show'
+end
diff --git a/api/app/views/spree/api/v1/variants/big.v1.rabl b/api/app/views/spree/api/v1/variants/big.v1.rabl
new file mode 100644
index 00000000000..718422228cc
--- /dev/null
+++ b/api/app/views/spree/api/v1/variants/big.v1.rabl
@@ -0,0 +1,14 @@
+object @variant
+
+cache [I18n.locale, @current_user_roles.include?('admin'), 'big_variant', root_object]
+
+extends 'spree/api/v1/variants/small'
+
+child(stock_items: :stock_items) do
+ attributes :id, :count_on_hand, :stock_location_id, :backorderable
+ attribute available?: :available
+
+ glue(:stock_location) do
+ attribute name: :stock_location_name
+ end
+end
diff --git a/api/app/views/spree/api/v1/variants/index.v1.rabl b/api/app/views/spree/api/v1/variants/index.v1.rabl
new file mode 100644
index 00000000000..3b941ebb29e
--- /dev/null
+++ b/api/app/views/spree/api/v1/variants/index.v1.rabl
@@ -0,0 +1,9 @@
+object false
+node(:count) { @variants.count }
+node(:total_count) { @variants.total_count }
+node(:current_page) { params[:page] ? params[:page].to_i : 1 }
+node(:pages) { @variants.total_pages }
+
+child(@variants => :variants) do
+ extends 'spree/api/v1/variants/big'
+end
diff --git a/api/app/views/spree/api/v1/variants/new.v1.rabl b/api/app/views/spree/api/v1/variants/new.v1.rabl
new file mode 100644
index 00000000000..26cf41db3b2
--- /dev/null
+++ b/api/app/views/spree/api/v1/variants/new.v1.rabl
@@ -0,0 +1,2 @@
+node(:attributes) { [*variant_attributes] }
+node(:required_attributes) { [] }
diff --git a/api/app/views/spree/api/v1/variants/show.v1.rabl b/api/app/views/spree/api/v1/variants/show.v1.rabl
new file mode 100644
index 00000000000..0d4d5942171
--- /dev/null
+++ b/api/app/views/spree/api/v1/variants/show.v1.rabl
@@ -0,0 +1,3 @@
+object @variant
+cache [I18n.locale, @current_user_roles.include?('admin'), 'show', root_object]
+extends 'spree/api/v1/variants/big'
diff --git a/api/app/views/spree/api/v1/variants/small.v1.rabl b/api/app/views/spree/api/v1/variants/small.v1.rabl
new file mode 100644
index 00000000000..ab80fc636f4
--- /dev/null
+++ b/api/app/views/spree/api/v1/variants/small.v1.rabl
@@ -0,0 +1,18 @@
+cache [I18n.locale, @current_user_roles.include?('admin'), 'small_variant', root_object]
+
+attributes *variant_attributes
+
+node(:display_price) { |p| p.display_price.to_s }
+node(:options_text, &:options_text)
+node(:track_inventory, &:should_track_inventory?)
+node(:in_stock, &:in_stock?)
+node(:is_backorderable, &:is_backorderable?)
+node(:is_orderable) { |v| v.is_backorderable? || v.in_stock? }
+node(:total_on_hand, &:total_on_hand)
+node(:is_destroyed, &:destroyed?)
+
+child option_values: :option_values do
+ attributes *option_value_attributes
+end
+
+child(images: :images) { extends 'spree/api/v1/images/show' }
diff --git a/api/app/views/spree/api/v1/zones/index.v1.rabl b/api/app/views/spree/api/v1/zones/index.v1.rabl
new file mode 100644
index 00000000000..633f9181373
--- /dev/null
+++ b/api/app/views/spree/api/v1/zones/index.v1.rabl
@@ -0,0 +1,7 @@
+object false
+child(@zones => :zones) do
+ extends 'spree/api/v1/zones/show'
+end
+node(:count) { @zones.count }
+node(:current_page) { params[:page].try(:to_i) || 1 }
+node(:pages) { @zones.total_pages }
diff --git a/api/app/views/spree/api/v1/zones/show.v1.rabl b/api/app/views/spree/api/v1/zones/show.v1.rabl
new file mode 100644
index 00000000000..eb66574ed4a
--- /dev/null
+++ b/api/app/views/spree/api/v1/zones/show.v1.rabl
@@ -0,0 +1,6 @@
+object @zone
+attributes :id, :name, :description
+
+child zone_members: :zone_members do
+ attributes :id, :name, :zoneable_type, :zoneable_id
+end
diff --git a/api/config/initializers/doorkeeper.rb b/api/config/initializers/doorkeeper.rb
new file mode 100644
index 00000000000..3f6c3347993
--- /dev/null
+++ b/api/config/initializers/doorkeeper.rb
@@ -0,0 +1,20 @@
+Doorkeeper.configure do
+ orm :active_record
+ use_refresh_token
+ api_only
+
+ resource_owner_authenticator { current_spree_user }
+
+ resource_owner_from_credentials do
+ user = Spree.user_class.find_for_database_authentication(email: params[:username])
+ user if user&.valid_for_authentication? { user.valid_password?(params[:password]) }
+ end
+
+ admin_authenticator do |routes|
+ current_spree_user&.has_spree_role?('admin') || redirect_to(routes.root_url)
+ end
+
+ grant_flows %w(password)
+
+ access_token_methods :from_bearer_authorization, :from_access_token_param
+end
diff --git a/api/config/initializers/metal_load_paths.rb b/api/config/initializers/metal_load_paths.rb
new file mode 100644
index 00000000000..a21345f6f1e
--- /dev/null
+++ b/api/config/initializers/metal_load_paths.rb
@@ -0,0 +1 @@
+Spree::Api::BaseController.append_view_path(ApplicationController.view_paths)
diff --git a/api/config/initializers/user_class_extensions.rb b/api/config/initializers/user_class_extensions.rb
new file mode 100644
index 00000000000..15f34b33274
--- /dev/null
+++ b/api/config/initializers/user_class_extensions.rb
@@ -0,0 +1,7 @@
+# Ensure that Spree.user_class includes the UserApiMethods concern
+
+Spree::Core::Engine.config.to_prepare do
+ if Spree.user_class && !Spree.user_class.included_modules.include?(Spree::UserApiMethods)
+ Spree.user_class.include Spree::UserApiMethods
+ end
+end
diff --git a/api/config/locales/en.yml b/api/config/locales/en.yml
new file mode 100644
index 00000000000..710f826c271
--- /dev/null
+++ b/api/config/locales/en.yml
@@ -0,0 +1,35 @@
+en:
+ spree:
+ api:
+ must_specify_api_key: "You must specify an API key."
+ invalid_api_key: "Invalid API key (%{key}) specified."
+ unauthorized: "You are not authorized to perform that action."
+ invalid_resource: "Invalid resource. Please fix errors and try again."
+ resource_not_found: "The resource you were looking for could not be found."
+ gateway_error: "There was a problem with the payment gateway: %{text}"
+ access: "API Access"
+ key: "Key"
+ clear_key: "Clear key"
+ regenerate_key: "Regenerate Key"
+ no_key: "No key"
+ generate_key: "Generate API key"
+ key_generated: "Key generated"
+ key_cleared: "Key cleared"
+ order:
+ could_not_transition: "The order could not be transitioned. Please fix the errors and try again."
+ invalid_shipping_method: "Invalid shipping method specified."
+ insufficient_quantity: An item in your cart has become unavailable.
+ payment:
+ credit_over_limit: "This payment can only be credited up to %{limit}. Please specify an amount less than or equal to this number."
+ update_forbidden: "This payment cannot be updated because it is %{state}."
+ shipment:
+ cannot_ready: "Cannot ready shipment."
+ stock_location_required: "A stock_location_id parameter must be provided in order to retrieve stock movements."
+ invalid_taxonomy_id: "Invalid taxonomy id."
+ shipment_transfer_errors_occured: "Following errors occured while attempting this action:"
+ negative_quantity: "quantity is negative"
+ wrong_shipment_target: "target shipment is the same as original shipment"
+
+ v2:
+ cart:
+ wrong_quantity: "Quantity has to be greater than 0"
diff --git a/api/config/routes.rb b/api/config/routes.rb
new file mode 100644
index 00000000000..51396abe62f
--- /dev/null
+++ b/api/config/routes.rb
@@ -0,0 +1,190 @@
+spree_path = Rails.application.routes.url_helpers.try(:spree_path, trailing_slash: true) || '/'
+
+Rails.application.routes.draw do
+ use_doorkeeper scope: "#{spree_path}/spree_oauth"
+end
+
+Spree::Core::Engine.add_routes do
+ namespace :api, defaults: { format: 'json' } do
+ namespace :v1 do
+ resources :promotions, only: [:show]
+
+ resources :customer_returns, only: [:index]
+ resources :reimbursements, only: [:index]
+
+ resources :products do
+ resources :images
+ resources :variants
+ resources :product_properties
+ end
+
+ concern :order_routes do
+ member do
+ put :approve
+ put :cancel
+ put :empty
+ put :apply_coupon_code
+ end
+
+ resources :line_items
+ resources :payments do
+ member do
+ put :authorize
+ put :capture
+ put :purchase
+ put :void
+ put :credit
+ end
+ end
+
+ resources :addresses, only: [:show, :update]
+
+ resources :return_authorizations do
+ member do
+ put :add
+ put :cancel
+ put :receive
+ end
+ end
+ end
+
+ resources :checkouts, only: [:update], concerns: :order_routes do
+ member do
+ put :next
+ put :advance
+ end
+ end
+
+ resources :variants do
+ resources :images
+ end
+
+ resources :option_types do
+ resources :option_values
+ end
+ resources :option_values
+
+ resources :option_values, only: :index
+
+ get '/orders/mine', to: 'orders#mine', as: 'my_orders'
+ get '/orders/current', to: 'orders#current', as: 'current_order'
+
+ resources :orders, concerns: :order_routes do
+ put :remove_coupon_code, on: :member
+ end
+
+ resources :zones
+ resources :countries, only: [:index, :show] do
+ resources :states, only: [:index, :show]
+ end
+
+ resources :shipments, only: [:create, :update] do
+ collection do
+ post 'transfer_to_location'
+ post 'transfer_to_shipment'
+ get :mine
+ end
+
+ member do
+ put :ready
+ put :ship
+ put :add
+ put :remove
+ end
+ end
+ resources :states, only: [:index, :show]
+
+ resources :taxonomies do
+ member do
+ get :jstree
+ end
+ resources :taxons do
+ member do
+ get :jstree
+ end
+ end
+ end
+
+ resources :taxons, only: [:index]
+
+ resources :inventory_units, only: [:show, :update]
+
+ resources :users do
+ resources :credit_cards, only: [:index]
+ end
+
+ resources :properties
+ resources :stock_locations do
+ resources :stock_movements
+ resources :stock_items
+ end
+
+ resources :stock_items, only: [:index, :update, :destroy]
+ resources :stores
+
+ resources :tags, only: :index
+
+ put '/classifications', to: 'classifications#update', as: :classifications
+ get '/taxons/products', to: 'taxons#products', as: :taxon_products
+ end
+
+ namespace :v2 do
+ namespace :storefront do
+ resource :cart, controller: :cart, only: %i[show create] do
+ post :add_item
+ patch :empty
+ delete 'remove_line_item/:line_item_id', to: 'cart#remove_line_item', as: :cart_remove_line_item
+ patch :set_quantity
+ patch :apply_coupon_code
+ delete 'remove_coupon_code/:coupon_code', to: 'cart#remove_coupon_code', as: :cart_remove_coupon_code
+ end
+
+ resource :checkout, controller: :checkout, only: %i[update] do
+ patch :next
+ patch :advance
+ patch :complete
+ post :add_store_credit
+ post :remove_store_credit
+ get :payment_methods
+ get :shipping_rates
+ end
+
+ resource :account, controller: :account, only: %i[show]
+
+ namespace :account do
+ resources :credit_cards, controller: :credit_cards, only: %i[index show]
+ resources :orders, controller: :orders, only: %i[index show]
+ end
+
+ resources :countries, only: %i[index]
+ get '/countries/:iso', to: 'countries#show', as: :country
+ resources :products, only: %i[index show]
+ resources :taxons, only: %i[index show]
+ end
+ end
+
+ get '/404', to: 'errors#render_404'
+
+ match 'v:api/*path', to: redirect { |params, request|
+ format = ".#{params[:format]}" unless params[:format].blank?
+ query = "?#{request.query_string}" unless request.query_string.blank?
+
+ if request.path == "#{spree_path}api/v1/#{params[:path]}#{format}#{query}"
+ "#{spree_path}api/404"
+ else
+ "#{spree_path}api/v1/#{params[:path]}#{format}#{query}"
+ end
+ }, via: [:get, :post, :put, :patch, :delete]
+
+ match '*path', to: redirect { |params, request|
+ format = ".#{params[:format]}" unless params[:format].blank?
+ query = "?#{request.query_string}" unless request.query_string.blank?
+
+ if request.path == "#{spree_path}api/v1/#{params[:path]}#{format}#{query}"
+ "#{spree_path}api/404"
+ else
+ "#{spree_path}api/v1/#{params[:path]}#{format}#{query}"
+ end
+ }, via: [:get, :post, :put, :patch, :delete]
+ end
+end
diff --git a/api/db/migrate/20100107141738_add_api_key_to_spree_users.rb b/api/db/migrate/20100107141738_add_api_key_to_spree_users.rb
new file mode 100644
index 00000000000..e73d67b2765
--- /dev/null
+++ b/api/db/migrate/20100107141738_add_api_key_to_spree_users.rb
@@ -0,0 +1,7 @@
+class AddApiKeyToSpreeUsers < ActiveRecord::Migration[4.2]
+ def change
+ unless defined?(User)
+ add_column :spree_users, :api_key, :string, limit: 40
+ end
+ end
+end
diff --git a/api/db/migrate/20120411123334_resize_api_key_field.rb b/api/db/migrate/20120411123334_resize_api_key_field.rb
new file mode 100644
index 00000000000..23d520214fc
--- /dev/null
+++ b/api/db/migrate/20120411123334_resize_api_key_field.rb
@@ -0,0 +1,7 @@
+class ResizeApiKeyField < ActiveRecord::Migration[4.2]
+ def change
+ unless defined?(User)
+ change_column :spree_users, :api_key, :string, limit: 48
+ end
+ end
+end
diff --git a/api/db/migrate/20120530054546_rename_api_key_to_spree_api_key.rb b/api/db/migrate/20120530054546_rename_api_key_to_spree_api_key.rb
new file mode 100644
index 00000000000..230a6209658
--- /dev/null
+++ b/api/db/migrate/20120530054546_rename_api_key_to_spree_api_key.rb
@@ -0,0 +1,7 @@
+class RenameApiKeyToSpreeApiKey < ActiveRecord::Migration[4.2]
+ def change
+ unless defined?(User)
+ rename_column :spree_users, :api_key, :spree_api_key
+ end
+ end
+end
diff --git a/api/db/migrate/20131017162334_add_index_to_user_spree_api_key.rb b/api/db/migrate/20131017162334_add_index_to_user_spree_api_key.rb
new file mode 100644
index 00000000000..a1472bd5d54
--- /dev/null
+++ b/api/db/migrate/20131017162334_add_index_to_user_spree_api_key.rb
@@ -0,0 +1,7 @@
+class AddIndexToUserSpreeApiKey < ActiveRecord::Migration[4.2]
+ def change
+ unless defined?(User)
+ add_index :spree_users, :spree_api_key
+ end
+ end
+end
diff --git a/api/db/migrate/20180320110726_create_doorkeeper_tables.rb b/api/db/migrate/20180320110726_create_doorkeeper_tables.rb
new file mode 100644
index 00000000000..aacc661ee67
--- /dev/null
+++ b/api/db/migrate/20180320110726_create_doorkeeper_tables.rb
@@ -0,0 +1,69 @@
+class CreateDoorkeeperTables < ActiveRecord::Migration[5.1]
+ def change
+ create_table :spree_oauth_applications do |t|
+ t.string :name, null: false
+ t.string :uid, null: false
+ t.string :secret, null: false
+ t.text :redirect_uri, null: false
+ t.string :scopes, null: false, default: ''
+ t.boolean :confidential, null: false, default: true
+ t.timestamps null: false
+ end
+
+ add_index :spree_oauth_applications, :uid, unique: true
+
+ create_table :spree_oauth_access_grants do |t|
+ t.integer :resource_owner_id, null: false
+ t.references :application, null: false
+ t.string :token, null: false
+ t.integer :expires_in, null: false
+ t.text :redirect_uri, null: false
+ t.datetime :created_at, null: false
+ t.datetime :revoked_at
+ t.string :scopes
+ end
+
+ add_index :spree_oauth_access_grants, :token, unique: true
+ add_foreign_key(
+ :spree_oauth_access_grants,
+ :spree_oauth_applications,
+ column: :application_id
+ )
+
+ create_table :spree_oauth_access_tokens do |t|
+ t.integer :resource_owner_id
+ t.references :application
+
+ # If you use a custom token generator you may need to change this column
+ # from string to text, so that it accepts tokens larger than 255
+ # characters. More info on custom token generators in:
+ # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator
+ #
+ # t.text :token, null: false
+ t.string :token, null: false
+
+ t.string :refresh_token
+ t.integer :expires_in
+ t.datetime :revoked_at
+ t.datetime :created_at, null: false
+ t.string :scopes
+
+ # If there is a previous_refresh_token column,
+ # refresh tokens will be revoked after a related access token is used.
+ # If there is no previous_refresh_token column,
+ # previous tokens are revoked as soon as a new access token is created.
+ # Comment out this line if you'd rather have refresh tokens
+ # instantly revoked.
+ t.string :previous_refresh_token, null: false, default: ""
+ end
+
+ add_index :spree_oauth_access_tokens, :token, unique: true
+ add_index :spree_oauth_access_tokens, :resource_owner_id
+ add_index :spree_oauth_access_tokens, :refresh_token, unique: true
+ add_foreign_key(
+ :spree_oauth_access_tokens,
+ :spree_oauth_applications,
+ column: :application_id
+ )
+ end
+end
diff --git a/api/docs/oauth/index.yml b/api/docs/oauth/index.yml
new file mode 100644
index 00000000000..d426a7704a0
--- /dev/null
+++ b/api/docs/oauth/index.yml
@@ -0,0 +1,77 @@
+openapi: 3.0.0
+servers:
+ - url: 'http://localhost:3000/spree_oauth'
+info:
+ version: 1.0.0
+ title: Spree OAuth 2.0 Authentication
+ description: >-
+ Spree uses the [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) gem
+ for handling OAuth. This integration is built into `spree_api` gem.
+paths:
+ '/token':
+ post:
+ description: >-
+ Creates or refreshes a Bearer token required to authorize calls API calls
+ tags:
+ - Token
+ operationId: Create or Refresh Token
+ responses:
+ '200':
+ description: Token was succesfully created or refreshed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Token'
+ requestBody:
+ required: true
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ oneOf:
+ - $ref: '#/components/schemas/CreateTokenBody'
+ - $ref: '#/components/schemas/RefreshTokenBody'
+components:
+ schemas:
+ Token:
+ properties:
+ access_token:
+ type: string
+ example: 2480c16561d1391ea81ca5336b651e9a29f4524f6dee8c7f3f02a600159189c3
+ token_type:
+ type: string
+ example: Bearer
+ default: Bearer
+ expires_in:
+ type: integer
+ example: 7200
+ description: 'Time (in seconds) after which the access token will expire'
+ refresh_token:
+ type: string
+ example: f5d78642252eeb3f3001f67b196ac21a27afc030462a54060b0ebbdae2b8dc9c
+ created_at:
+ type: integer
+ example: 1539863418
+ CreateTokenBody:
+ properties:
+ grant_type:
+ type: string
+ default: password
+ description: >-
+ Use `password` to create a token and `refresh_token` to refresh it
+ username:
+ type: string
+ description: User email address
+ example: 'spree@example.com'
+ password:
+ type: string
+ description: User password
+ example: 'spree123'
+ RefreshTokenBody:
+ properties:
+ grant_type:
+ type: string
+ default: refresh_token
+ refresh_token:
+ type: string
+ description: Rerefresh token obtained from the old created token
+ example: 27af95fd57a424e5d01aaf5eab1324a8d5c0ca57daf384fae39f811a5144330143301'
diff --git a/api/docs/v2/storefront/index.yaml b/api/docs/v2/storefront/index.yaml
new file mode 100644
index 00000000000..f1a27be6724
--- /dev/null
+++ b/api/docs/v2/storefront/index.yaml
@@ -0,0 +1,2547 @@
+openapi: 3.0.0
+servers:
+ - url: 'http://localhost:3000/api/v2/storefront'
+info:
+ version: 2.0.0
+ title: Storefront API
+ description: >-
+
+ Storefront API v2 is a modern REST API based on the
+ JSON API spec
+ which provides you with necessary endpoints to build amazing user
+ intefaces either in JavaScript frameworks or native mobile libraries.
+
+
+ Please read our introduction to the API v2 [insert link here]
+
+ You can obtain payment_method_id by querying
+ GET /checkout/payment_methods endpoint.
+
+
5. Complete checkout
+
PATCH /checkout/complete
+
+
+ This will complete the Checout and will marke the Order as completed.
+ You cannot execute any operations on this Order anymore via Storefront API.
+ Further operations on the Order are possible via Platform API.
+
+ tags:
+ - Checkout
+ operationId: 'Update Checkout'
+ responses:
+ '200':
+ description: Checkout was updated
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Cart'
+ '422':
+ description: Checkout couldn't be updated
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+ security:
+ - orderToken: []
+ - bearerAuth: []
+ parameters:
+ - $ref: '#/components/parameters/CartIncludeParam'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ order:
+ type: object
+ properties:
+ email:
+ type: string
+ example: 'john@snow.org'
+ bill_address_attributes:
+ $ref: '#/components/schemas/AddressPayload'
+ ship_address_attributes:
+ $ref: '#/components/schemas/AddressPayload'
+ payments_attributes:
+ type: array
+ items:
+ type: object
+ properties:
+ payment_method_id:
+ type: number
+ example: 1
+ description: 'ID of selected payment method'
+ shipments_attributes:
+ type: object
+ payment_source:
+ type: object
+
+ '/checkout/next':
+ patch:
+ description: Goes to the next Checkout step
+ tags:
+ - Checkout
+ operationId: 'Checkout Next'
+ responses:
+ '200':
+ description: Checkout transitioned to the next step
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Cart'
+ '422':
+ description: Checkout couldn't transition to the next step
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+ security:
+ - orderToken: []
+ - bearerAuth: []
+ parameters:
+ - $ref: '#/components/parameters/CartIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+
+ '/checkout/advance':
+ patch:
+ description: Advances Checkout to the furthest Checkout step validation allows, until the Complete step
+ tags:
+ - Checkout
+ operationId: 'Advance Checkout'
+ responses:
+ '200':
+ description: Checkout was advanced to the furthest step
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Cart'
+ '422':
+ description: Checkout couldn't transition to the next step
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+ security:
+ - orderToken: []
+ - bearerAuth: []
+ parameters:
+ - $ref: '#/components/parameters/CartIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+
+ '/checkout/complete':
+ patch:
+ description: Completes the Checkout
+ tags:
+ - Checkout
+ operationId: 'Complete Checkout'
+ responses:
+ '200':
+ description: Checkout was completed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Cart'
+ '422':
+ description: Checkout couldn't be completed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+ security:
+ - orderToken: []
+ - bearerAuth: []
+ parameters:
+ - $ref: '#/components/parameters/CartIncludeParam'
+
+ '/checkout/add_store_credit':
+ post:
+ description: Adds Store Credit payments if a user has any
+ tags:
+ - Checkout
+ operationId: 'Add Store Credit'
+ responses:
+ '200':
+ description: Store Credit payment created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Cart'
+ '422':
+ description: Store Credit couldn't be created
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+ security:
+ - orderToken: []
+ - bearerAuth: []
+ parameters:
+ - in: query
+ name: amount
+ description: >-
+ Amount of Store Credit to use.
+ As much as possible Store Credit will be applied if no amount is passed.
+ schema:
+ type: string
+ example: 100.0
+ - $ref: '#/components/parameters/CartIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+ '/checkout/remove_store_credit':
+ post:
+ description: Remove Store Credit payments if any applied
+ tags:
+ - Checkout
+ operationId: 'Remove Store Credit'
+ responses:
+ '200':
+ description: Store Credit payment removed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Cart'
+ '422':
+ description: Store Credit payments weren't removed
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Error'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+ security:
+ - orderToken: []
+ - bearerAuth: []
+ parameters:
+ - $ref: '#/components/parameters/CartIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+
+ '/checkout/payment_methods':
+ get:
+ description: >-
+ Returns a list of available Payment Methods
+ tags:
+ - Checkout
+ operationId: 'Payment Methods'
+ responses:
+ '200':
+ description: Returns a list of available Payment Methods
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PaymentMethodsList'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+ security:
+ - orderToken: []
+ - bearerAuth: []
+
+ '/checkout/shipping_rates':
+ get:
+ description: >-
+ Returns a list of available Shipping Rates for Checkout.
+ Shipping Rates are grouped against Shipments.
+ Each checkout cna have multiple Shipments eg. some products are available
+ in stock and will be send out instantly and some needs to be backordered.
+ tags:
+ - Checkout
+ operationId: 'Shipping Rates'
+ responses:
+ '200':
+ description: Returns a list of available Shipping Rates for Checkout
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ShippingRatesList'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+ security:
+ - orderToken: []
+ - bearerAuth: []
+
+ '/products':
+ get:
+ description: >-
+ Returns a list of Products
+ tags:
+ - Products
+ operationId: 'Products List'
+ parameters:
+ - $ref: '#/components/parameters/FilterByIds'
+ - in: query
+ name: filter[price]
+ schema:
+ type: string
+ example: 10,100
+ description: Filter Prodcuts based on price (minimum, maximum range)
+ - in: query
+ name: filter[taxons]
+ schema:
+ type: string
+ example: 1,2,3,4,5,6,7,8,9,10,11
+ description: Filter Prodcuts based on taxons (IDs of categories, brands, etc)
+ - in: query
+ name: filter[name]
+ schema:
+ type: string
+ example: rails
+ description: Find Prodcuts with matching name (supports wild-card, partial-word match search)
+ - in: query
+ name: 'filter[options][tshirt-color]'
+ schema:
+ type: string
+ example: Red
+ description: >-
+ Find Prodcuts with Variants that have the specified option (eg. color, size)
+ and value (eg. red, XS)
+ - in: query
+ name: sort
+ schema:
+ type: string
+ example: >-
+ -updated_at,price
+ description: >-
+ Sort products based on:
+
+
price (ascending/descenging)
+
updated_at (ascending/descenging)
+
+ Use - sign to set descenging sort, eg. -updated_at
+ - $ref: '#/components/parameters/PageParam'
+ - $ref: '#/components/parameters/PerPageParam'
+ - $ref: '#/components/parameters/ProductIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+ responses:
+ '200':
+ description: Returns a list of Products
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductsList'
+ '/products/{id}':
+ get:
+ description: >-
+ To view the details for a single product, make a request using that
+ product's permalink: GET /api/v2/products/a-product
+
You may also query by the product's id attribute: GET
+ /api/v2/products/1
Note that the API will attempt a
+ permalink lookup before an ID lookup.
+ tags:
+ - Products
+ operationId: 'Show Product'
+ parameters:
+ - $ref: '#/components/parameters/IdOrPermalink'
+ - $ref: '#/components/parameters/ProductIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+ responses:
+ '200':
+ description: Returns the requested Product
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Product'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+
+ '/taxons':
+ get:
+ description: >-
+ Returns a list of Taxons. You can filter out taxons by...
+ tags:
+ - Taxons
+ operationId: 'Taxons List'
+ parameters:
+ - $ref: '#/components/parameters/FilterByIds'
+ - $ref: '#/components/parameters/FilterByName'
+ - in: query
+ name: filter[parent_id]
+ schema:
+ type: string
+ example: '1'
+ description: Fetch children nodes of specified Taxon
+ - in: query
+ name: filter[taxonomy_id]
+ schema:
+ type: string
+ example: '1'
+ description: Fetch Taxons in a specified Taxonomy
+ - in: query
+ name: filter[roots]
+ schema:
+ type: boolean
+ example: false
+ description: Fetch only root Taxons (Taxonomies)
+ - $ref: '#/components/parameters/PageParam'
+ - $ref: '#/components/parameters/PerPageParam'
+ - $ref: '#/components/parameters/TaxonIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+ responses:
+ '200':
+ description: Returns a list of Taxons
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TaxonsList'
+
+ '/taxons/{id}':
+ get:
+ description: >-
+ To view the details for a single Taxon, make a request using that
+ Taxon's permalink: GET /api/v2/taxons/t-shirts
+
You may also query by the Taxons's ID attribute: GET
+ /api/v2/taxons/1
Note that the API will attempt a
+ permalink lookup before an ID lookup.
+ tags:
+ - Taxons
+ operationId: 'Show Taxon'
+ parameters:
+ - $ref: '#/components/parameters/IdOrPermalink'
+ - $ref: '#/components/parameters/TaxonIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+ responses:
+ '200':
+ description: Returns the reqested Taxon
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Taxon'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+
+ '/countries':
+ get:
+ description: >-
+ Returns a list of all Countries
+ tags:
+ - Countries
+ operationId: 'Countries List'
+ responses:
+ '200':
+ description: Returns a list of all Countries
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CountriesList'
+
+ '/countries/{iso}':
+ get:
+ description: >-
+ To view the details for a single Country, make a request using that
+ Country's iso code: GET /api/v2/storefront/countries/gb
+
You may also query by the Country's iso3 code: GET
+ /api/v2/storefront/coutries/gbr
Note that the API will attempt a
+ iso lookup before an iso3 lookup.
+ tags:
+ - Countries
+ operationId: 'Show Country'
+ parameters:
+ - $ref: '#/components/parameters/IsoOrIso3'
+ - $ref: '#/components/parameters/CountryIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+ responses:
+ '200':
+ description: Returns the requested Country
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Country'
+ '404':
+ $ref: '#/components/responses/404NotFound'
+
+ '/countries/default':
+ get:
+ description: >-
+ Returns the default Country for the application.
+ By default this will be the US.
+ tags:
+ - Countries
+ operationId: 'Default Country'
+ parameters:
+ - $ref: '#/components/parameters/CountryIncludeParam'
+ - $ref: '#/components/parameters/SparseFieldsParam'
+ responses:
+ '200':
+ description: Returns the default Country
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Country'
+
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ description: >-
+ User token to authorize Cart and Checkout requests. You can obtain it
+ from `http://your_store_url.com/spree_oauth` endpoint.
+ It is required to associate Cart with the User.
+ See OAuth documentation for more details.
+ orderToken:
+ type: apiKey
+ in: header
+ description: >-
+
+ Order token to authorize Cart and Checkout requests.
+ Useful for guest checouts when you don't have the user token
+ (bearerAuth)
+
+
+ You can obtain it from the `/cart` endpoint -
+ it's part of the response (value of the `token` field).
+
diff --git a/app/views/admin/orders/_address.rhtml b/app/views/admin/orders/_address.rhtml
deleted file mode 100644
index 924b6ac7820..00000000000
--- a/app/views/admin/orders/_address.rhtml
+++ /dev/null
@@ -1,37 +0,0 @@
-<%
-# HACK - in_place_editor field works only with instance variables and not local variables
-# See http://dev.rubyonrails.org/ticket/3094 for details
-
-@address = address
-%>
-
-
- <% end %>
- <% if @order.order_operations.empty? %>
-
-
None Available.
-
- <% end %>
-
-
-
-
Transaction
-
Amount
-
Card Number
-
Type
-
Response Code
-
Date/Time
-
- <% if @order.credit_card %>
- <% @order.credit_card.txns.each do |t| %>
-
-
<%=Txn::TxnType.from_value t.txn_type.to_i%>
-
<%=t.amount%>
-
<%=t.credit_card.display_number%>
-
<%=t.credit_card.cc_type%>
-
<%=t.response_code%>
-
<%=t.created_at.to_s(:db)%>
-
- <% end %>
- <% end %>
-
-<%= link_to "Back", :controller => :orders %>
\ No newline at end of file
diff --git a/app/views/admin/overview/index.rhtml b/app/views/admin/overview/index.rhtml
deleted file mode 100644
index c6a8845e18c..00000000000
--- a/app/views/admin/overview/index.rhtml
+++ /dev/null
@@ -1,3 +0,0 @@
-
Overview
-
-Welcome to the Overview Page. This is a placeholder for some real content. Eventually we envision a bunch of "widgets" summarizing recent activity and other elements of interest.
\ No newline at end of file
diff --git a/app/views/admin/products/_form.rhtml b/app/views/admin/products/_form.rhtml
deleted file mode 100644
index 8810a11e018..00000000000
--- a/app/views/admin/products/_form.rhtml
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-<% end %>
\ No newline at end of file
diff --git a/app/views/admin/products/_new_variant.rhtml b/app/views/admin/products/_new_variant.rhtml
deleted file mode 100644
index 29fd29af548..00000000000
--- a/app/views/admin/products/_new_variant.rhtml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-<% end %>
\ No newline at end of file
diff --git a/app/views/checkout/_address_ro.rhtml b/app/views/checkout/_address_ro.rhtml
deleted file mode 100644
index a0a9a8e4e93..00000000000
--- a/app/views/checkout/_address_ro.rhtml
+++ /dev/null
@@ -1,18 +0,0 @@
-
For Visa, MasterCard, and Discover cards, the card code is the last 3 digit number located on the back of your card on or above your signature line. For an American Express card, it is the 4 digits on the FRONT above the end of your card number.
-
To help reduce fraud in the card-not-present environment, credit card companies have introduced a card code program. Visa calls this code Card Verification Value (CVV); MasterCard calls it Card Validation Code (CVC); Discover calls it Card ID (CID). The card code is a three- or four- digit security code that is printed on the back of cards. The number typically appears at the end of the signature panel.
-
- <%= submit_tag "Process" %>
- <% if logged_in? and current_user.has_role?("admin") %>
- <%= link_to "Comp Order", {:action => "comp"}, {:confirm => "Customer will not be charged. Are you sure you want to comp this order?"} %>
- <% end %>
-<% end %>
diff --git a/app/views/checkout/incomplete.rhtml b/app/views/checkout/incomplete.rhtml
deleted file mode 100644
index f0f413a8631..00000000000
--- a/app/views/checkout/incomplete.rhtml
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/backend/app/views/kaminari/_last_page.html.erb b/backend/app/views/kaminari/_last_page.html.erb
new file mode 100644
index 00000000000..d8fc4195fd0
--- /dev/null
+++ b/backend/app/views/kaminari/_last_page.html.erb
@@ -0,0 +1,11 @@
+<%# Link to the "Last" page
+ - available local variables
+ url: url to the last page
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+-%>
+
diff --git a/backend/app/views/kaminari/_next_page.html.erb b/backend/app/views/kaminari/_next_page.html.erb
new file mode 100644
index 00000000000..cf0e2301155
--- /dev/null
+++ b/backend/app/views/kaminari/_next_page.html.erb
@@ -0,0 +1,15 @@
+<%# Link to the "Next" page
+ - available local variables
+ url: url to the next page
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+-%>
+
diff --git a/backend/app/views/kaminari/_page.html.erb b/backend/app/views/kaminari/_page.html.erb
new file mode 100644
index 00000000000..842469ba93f
--- /dev/null
+++ b/backend/app/views/kaminari/_page.html.erb
@@ -0,0 +1,12 @@
+<%# Link showing page number
+ - available local variables
+ page: a page object for "this" page
+ url: url to this page
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+-%>
+
diff --git a/backend/app/views/kaminari/_paginator.html.erb b/backend/app/views/kaminari/_paginator.html.erb
new file mode 100644
index 00000000000..6645d6e990a
--- /dev/null
+++ b/backend/app/views/kaminari/_paginator.html.erb
@@ -0,0 +1,23 @@
+<%# The container tag
+ - available local variables
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+ paginator: the paginator that renders the pagination tags inside
+-%>
+<%= paginator.render do %>
+
+<% end %>
diff --git a/backend/app/views/kaminari/_prev_page.html.erb b/backend/app/views/kaminari/_prev_page.html.erb
new file mode 100644
index 00000000000..1e8c0f33eb9
--- /dev/null
+++ b/backend/app/views/kaminari/_prev_page.html.erb
@@ -0,0 +1,15 @@
+<%# Link to the "Previous" page
+ - available local variables
+ url: url to the previous page
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+-%>
+
+<% end %>
diff --git a/backend/app/views/spree/admin/payments/_capture_events.html.erb b/backend/app/views/spree/admin/payments/_capture_events.html.erb
new file mode 100644
index 00000000000..d63a064febb
--- /dev/null
+++ b/backend/app/views/spree/admin/payments/_capture_events.html.erb
@@ -0,0 +1,19 @@
+<% if @payment.capture_events.exists? %>
+
<%= Spree.t(:capture_events) %>
+
+
+
+
<%= "#{Spree.t('date')}/#{Spree.t('time')}" %>
+
<%= Spree.t(:amount) %>
+
+
+
+ <% @payment.capture_events.each do |capture_event| %>
+
+
<%= pretty_time(capture_event.created_at) %>
+
<%= capture_event.display_amount %>
+
+ <% end %>
+
+
+<% end %>
\ No newline at end of file
diff --git a/backend/app/views/spree/admin/payments/_form.html.erb b/backend/app/views/spree/admin/payments/_form.html.erb
new file mode 100644
index 00000000000..a39d3c691ca
--- /dev/null
+++ b/backend/app/views/spree/admin/payments/_form.html.erb
@@ -0,0 +1,33 @@
+
+ Please remember to configure all of the emails that Spree has provided to your needs.
+ Spree comes shipped with Ink
+ prepackaged, but you can use your own version. Ink is not placed in the asset pipeline.
+
+
+
+ Also take note that Gmail does not support <style> tags.
+ Therefore, you will need a gem that will be able to remove your <style>
+ tags and place them inline. Gmail only supports inline styles. We use
+ Premailer for Rails by default.
+
+
+
+
+
+
diff --git a/core/app/views/spree/test_mailer/test_email.text.erb b/core/app/views/spree/test_mailer/test_email.text.erb
new file mode 100644
index 00000000000..3491d6fbf3f
--- /dev/null
+++ b/core/app/views/spree/test_mailer/test_email.text.erb
@@ -0,0 +1,4 @@
+<%= Spree.t('test_mailer.test_email.greeting') %>
+================
+
+<%= Spree.t('test_mailer.test_email.message') %>
diff --git a/core/config/initializers/active_storage.rb b/core/config/initializers/active_storage.rb
new file mode 100644
index 00000000000..ca4d071fd92
--- /dev/null
+++ b/core/config/initializers/active_storage.rb
@@ -0,0 +1,5 @@
+class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
+ def perform(blob)
+ blob.purge unless blob.attachments.present?
+ end
+end
diff --git a/core/config/initializers/acts_as_taggable_on.rb b/core/config/initializers/acts_as_taggable_on.rb
new file mode 100644
index 00000000000..85a1a486b66
--- /dev/null
+++ b/core/config/initializers/acts_as_taggable_on.rb
@@ -0,0 +1,9 @@
+require 'acts-as-taggable-on'
+
+ActsAsTaggableOn::Tag.class_eval do
+ self.table_name_prefix = 'spree_'
+end
+
+ActsAsTaggableOn::Tagging.class_eval do
+ self.table_name_prefix = 'spree_'
+end
diff --git a/core/config/initializers/assets.rb b/core/config/initializers/assets.rb
new file mode 100644
index 00000000000..5c508b19548
--- /dev/null
+++ b/core/config/initializers/assets.rb
@@ -0,0 +1 @@
+Rails.application.config.assets.precompile += %w(logo/spree_50.png noimage/*.png)
diff --git a/core/config/initializers/friendly_id.rb b/core/config/initializers/friendly_id.rb
new file mode 100644
index 00000000000..6596efc8f98
--- /dev/null
+++ b/core/config/initializers/friendly_id.rb
@@ -0,0 +1,7 @@
+# To learn more, check out the guide:
+# http://norman.github.io/friendly_id/file.Guide.html
+FriendlyId.defaults do |config|
+ config.use :reserved
+ config.reserved_words = %w(new edit index session login logout users admin
+ stylesheets assets javascripts images)
+end
diff --git a/core/config/initializers/premailer_assets.rb b/core/config/initializers/premailer_assets.rb
new file mode 100644
index 00000000000..634de23377e
--- /dev/null
+++ b/core/config/initializers/premailer_assets.rb
@@ -0,0 +1 @@
+Rails.application.config.assets.precompile += %w(ink.css)
diff --git a/core/config/initializers/premailer_rails.rb b/core/config/initializers/premailer_rails.rb
new file mode 100644
index 00000000000..00210e8a938
--- /dev/null
+++ b/core/config/initializers/premailer_rails.rb
@@ -0,0 +1,3 @@
+if Gem.loaded_specs['premailer-rails'].version >= Gem::Version.create('1.10.0')
+ Premailer::Rails.config[:strategies] = [:filesystem, :network, :asset_pipeline]
+end
diff --git a/core/config/initializers/use_paperclip.rb b/core/config/initializers/use_paperclip.rb
new file mode 100644
index 00000000000..435c7502009
--- /dev/null
+++ b/core/config/initializers/use_paperclip.rb
@@ -0,0 +1,3 @@
+Rails.application.configure do
+ config.use_paperclip = ActiveModel::Type::Boolean.new.cast ENV['SPREE_USE_PAPERCLIP']
+end
diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml
new file mode 100644
index 00000000000..1cde3e334bb
--- /dev/null
+++ b/core/config/locales/en.yml
@@ -0,0 +1,1546 @@
+en:
+ activemodel:
+ errors:
+ models:
+ spree/fulfilment_changer:
+ attributes:
+ desired_shipment:
+ can_not_transfer_within_same_shipment: can not be same as current shipment
+ not_enough_stock_at_desired_location: not enough stock in desired stock location
+ current_shipment:
+ has_already_been_shipped: has already been shipped
+ can_not_have_backordered_inventory_units: has backordered inventory units
+ activerecord:
+ attributes:
+ spree/address:
+ address1: Address
+ address2: Address (contd.)
+ city: City
+ country: Country
+ firstname: First Name
+ lastname: Last Name
+ phone: Phone
+ state: State
+ zipcode: Zip Code
+ spree/calculator/tiered_flat_rate:
+ preferred_base_amount: Base Amount
+ preferred_tiers: Tiers
+ spree/calculator/tiered_percent:
+ preferred_base_percent: Base Percent
+ preferred_tiers: Tiers
+ spree/country:
+ iso: ISO
+ iso3: ISO3
+ iso_name: ISO Name
+ name: Name
+ numcode: ISO Code
+ spree/credit_card:
+ base: ''
+ cc_type: Type
+ month: Month
+ number: Number
+ verification_value: Verification Value
+ year: Year
+ name: Name
+ spree/inventory_unit:
+ state: State
+ spree/line_item:
+ price: Price
+ quantity: Quantity
+ spree/option_type:
+ name: Name
+ presentation: Presentation
+ spree/order:
+ checkout_complete: Checkout Complete
+ completed_at: Completed At
+ coupon_code: Coupon Code
+ created_at: Order Date
+ email: Customer E-Mail
+ ip_address: IP Address
+ item_total: Item Total
+ number: Number
+ payment_state: Payment State
+ shipment_state: Shipment State
+ special_instructions: Special Instructions
+ state: State
+ total: Total
+ considered_risky: Risky
+ spree/order/bill_address:
+ address1: Billing address street
+ city: Billing address city
+ firstname: Billing address first name
+ lastname: Billing address last name
+ phone: Billing address phone
+ state: Billing address state
+ zipcode: Billing address zipcode
+ spree/order/ship_address:
+ address1: Shipping address street
+ city: Shipping address city
+ firstname: Shipping address first name
+ lastname: Shipping address last name
+ phone: Shipping address phone
+ state: Shipping address state
+ zipcode: Shipping address zipcode
+ spree/payment:
+ amount: Amount
+ number: Number
+ spree/payment_method:
+ name: Name
+ spree/product:
+ available_on: Available On
+ discontinue_on: Discontinue On
+ cost_currency: Cost Currency
+ cost_price: Cost Price
+ description: Description
+ master_price: Master Price
+ name: Name
+ on_hand: On Hand
+ shipping_category: Shipping Category
+ tax_category: Tax Category
+ spree/promotion:
+ advertise: Advertise
+ code: Code
+ description: Description
+ event_name: Event Name
+ expires_at: Expires At
+ generate_code: Generate coupon code
+ name: Name
+ path: Path
+ starts_at: Starts At
+ usage_limit: Usage Limit
+ promotion_category: Promotion Category
+ spree/promotion_category:
+ name: Name
+ code: Code
+ spree/property:
+ name: Name
+ presentation: Presentation
+ spree/prototype:
+ name: Name
+ spree/return_authorization:
+ amount: Amount
+ spree/role:
+ name: Name
+ spree/shipment:
+ number: Number
+ spree/state:
+ abbr: Abbreviation
+ name: Name
+ spree/state_change:
+ state_changes: State changes
+ type: Type
+ state_from: State from
+ state_to: State to
+ user: User
+ timestamp: Timestamp
+ updated: Updated
+ spree/store:
+ url: Site URL
+ meta_description: Meta Description
+ meta_keywords: Meta Keywords
+ seo_title: Seo Title
+ name: Site Name
+ mail_from_address: Mail From Address
+ spree/store_credit:
+ amount_used: "Amount used"
+ spree/store_credit_category:
+ name: Name
+ spree/tax_category:
+ description: Description
+ name: Name
+ spree/tax_rate:
+ amount: Rate
+ included_in_price: Included in Price
+ show_rate_in_label: Show rate in label
+ spree/taxon:
+ name: Name
+ permalink: Permalink
+ position: Position
+ spree/taxonomy:
+ name: Name
+ spree/user:
+ email: Email
+ password: Password
+ password_confirmation: Password Confirmation
+ spree/variant:
+ cost_currency: Cost Currency
+ cost_price: Cost Price
+ depth: Depth
+ height: Height
+ price: Price
+ sku: SKU
+ weight: Weight
+ width: Width
+ spree/zone:
+ description: Description
+ name: Name
+ models:
+ spree/address:
+ one: Address
+ other: Addresses
+ spree/country:
+ one: Country
+ other: Countries
+ spree/credit_card:
+ one: Credit Card
+ other: Credit Cards
+ spree/customer_return:
+ one: Customer Return
+ other: Customer Returns
+ spree/inventory_unit:
+ one: Inventory Unit
+ other: Inventory Units
+ spree/line_item:
+ one: Line Item
+ other: Line Items
+ spree/option_type:
+ one: Option Type
+ other: Option Types
+ spree/option_value:
+ one: Option Value
+ other: Option Values
+ spree/order:
+ one: Order
+ other: Orders
+ spree/payment:
+ one: Payment
+ other: Payments
+ spree/payment_method:
+ one: Payment Method
+ other: Payment Methods
+ spree/product:
+ one: Product
+ other: Products
+ spree/promotion:
+ one: Promotion
+ other: Promotions
+ spree/promotion_category:
+ one: Promotion Category
+ other: Promotion Categories
+ spree/property:
+ one: Property
+ other: Properties
+ spree/prototype:
+ one: Prototype
+ other: Prototypes
+ spree/refund_reason:
+ one: Refund Reason
+ other: Refund Reasons
+ spree/reimbursement:
+ one: Reimbursement
+ other: Reimbursements
+ spree/reimbursement_type:
+ one: Reimbursement Type
+ other: Reimbursement Types
+ spree/return_authorization:
+ one: Return Authorization
+ other: Return Authorizations
+ spree/return_authorization_reason:
+ one: Return Authorization Reason
+ other: Return Authorization Reasons
+ spree/role:
+ one: Roles
+ other: Roles
+ spree/shipment:
+ one: Shipment
+ other: Shipments
+ spree/shipping_category:
+ one: Shipping Category
+ other: Shipping Categories
+ spree/shipping_method:
+ one: Shipping Method
+ other: Shipping Methods
+ spree/state:
+ one: State
+ other: States
+ spree/state_change:
+ one: State Change
+ other: State Changes
+ spree/stock_movement:
+ one: Stock Movement
+ other: Stock Movements
+ spree/stock_location:
+ one: Stock Location
+ other: Stock Locations
+ spree/stock_transfer:
+ one: Stock Transfer
+ other: Stock Transfers
+ spree/store_credit:
+ one: Store Credit
+ other: Store Credits
+ spree/store_credit_category:
+ one: Store Credit Category
+ other: Store Credit Categories
+ spree/tax_category:
+ one: Tax Category
+ other: Tax Categories
+ spree/tax_rate:
+ one: Tax Rate
+ other: Tax Rates
+ spree/taxon:
+ one: Taxon
+ other: Taxons
+ spree/taxonomy:
+ one: Taxonomy
+ other: Taxonomies
+ spree/tracker:
+ one: Tracker
+ other: Trackers
+ spree/user:
+ one: User
+ other: Users
+ spree/variant:
+ one: Variant
+ other: Variants
+ spree/zone:
+ one: Zone
+ other: Zones
+ errors:
+ models:
+ spree/calculator/tiered_flat_rate:
+ attributes:
+ base:
+ keys_should_be_positive_number: "Tier keys should all be numbers larger than 0"
+ preferred_tiers:
+ should_be_hash: "should be a hash"
+ spree/calculator/tiered_percent:
+ attributes:
+ base:
+ keys_should_be_positive_number: "Tier keys should all be numbers larger than 0"
+ values_should_be_percent: "Tier values should all be percentages between 0% and 100%"
+ preferred_tiers:
+ should_be_hash: "should be a hash"
+ spree/classification:
+ attributes:
+ taxon_id:
+ already_linked: "is already linked to this product"
+ spree/credit_card:
+ attributes:
+ base:
+ card_expired: "Card has expired"
+ expiry_invalid: "Card expiration is invalid"
+ spree/line_item:
+ attributes:
+ currency:
+ must_match_order_currency: "Must match order currency"
+ spree/promotion:
+ attributes:
+ expires_at:
+ invalid_date_range: must be later than start date
+ spree/product:
+ attributes:
+ discontinue_on:
+ invalid_date_range: must be later than available date
+ base:
+ cannot_destroy_if_attached_to_line_items: Cannot delete products once they are attached to line items.
+ spree/refund:
+ attributes:
+ amount:
+ greater_than_allowed: is greater than the allowed amount.
+ spree/reimbursement:
+ attributes:
+ base:
+ return_items_order_id_does_not_match: One or more of the return items specified do not belong to the same order as the reimbursement.
+ spree/return_item:
+ attributes:
+ reimbursement:
+ cannot_be_associated_unless_accepted: cannot be associated to a return item that is not accepted.
+ inventory_unit:
+ other_completed_return_item_exists: "%{inventory_unit_id} has already been taken by return item %{return_item_id}"
+ spree/shipping_method:
+ attributes:
+ base:
+ required_shipping_category: "You must select at least one shipping category"
+ spree/store:
+ attributes:
+ base:
+ cannot_destroy_default_store: Cannot destroy the default Store.
+ spree/store_credit:
+ attributes:
+ amount_used:
+ cannot_be_greater_than_amount: Cannot be greater than amount.
+ greater_than_zero_restrict_delete: is greater than zero. Can not delete store credit.
+ amount_authorized:
+ exceeds_total_credits: Exceeds total credits.
+ spree/store_credit_category:
+ attributes:
+ base:
+ cannot_destroy_if_used_in_store_credit: Cannot delete store credit categories once they are used in store credit.
+ spree/variant:
+ attributes:
+ base:
+ cannot_destroy_if_attached_to_line_items: Cannot delete variants once they are attached to line items.
+ no_master_variant_found_to_infer_price: No master variant found to infer price
+ must_supply_price_for_variant_or_master: Must supply price for variant or master price for product.
+ spree/image:
+ attributes:
+ attachment:
+ not_allowed_content_type: has not allowed content type
+ attachment_must_be_present: must be present
+ spree/taxon_image:
+ attributes:
+ attachment:
+ not_allowed_content_type: has not allowed content type
+ devise:
+ confirmations:
+ confirmed: Your account was successfully confirmed. You are now signed in.
+ send_instructions: You will receive an email with instructions about how to confirm your account in a few minutes.
+ failure:
+ inactive: Your account was not activated yet.
+ invalid: Invalid email or password.
+ invalid_token: Invalid authentication token.
+ locked: Your account is locked.
+ timeout: Your session expired, please sign in again to continue.
+ unauthenticated: You need to sign in or sign up before continuing.
+ unconfirmed: You have to confirm your account before continuing.
+ mailer:
+ confirmation_instructions:
+ subject: Confirmation instructions
+ reset_password_instructions:
+ subject: Reset password instructions
+ unlock_instructions:
+ subject: Unlock Instructions
+ oauth_callbacks:
+ failure: Could not authorize you from %{kind} because "%{reason}".
+ success: Successfully authorized from %{kind} account.
+ unlocks:
+ send_instructions: You will receive an email with instructions about how to unlock your account in a few minutes.
+ unlocked: Your account was successfully unlocked. You are now signed in.
+ user_passwords:
+ user:
+ cannot_be_blank: Your password cannot be blank.
+ send_instructions: You will receive an email with instructions about how to reset your password in a few minutes.
+ updated: Your password was changed successfully. You are now signed in.
+ user_registrations:
+ destroyed: Bye! Your account was successfully cancelled. We hope to see you again soon.
+ inactive_signed_up: You have signed up successfully. However, we could not sign you in because your account is %{reason}.
+ signed_up: Welcome! You have signed up successfully.
+ updated: You updated your account successfully.
+ user_sessions:
+ signed_in: Signed in successfully.
+ signed_out: Signed out successfully.
+
+ errors:
+ messages:
+ already_confirmed: was already confirmed
+ not_found: not found
+ not_locked: was not locked
+ not_saved:
+ one: ! '1 error prohibited this %{resource} from being saved:'
+ other: ! '%{count} errors prohibited this %{resource} from being saved:'
+
+ number:
+ percentage:
+ format:
+ precision: 1
+
+ spree:
+ abbreviation: Abbreviation
+ accept: Accept
+ acceptance_status: Acceptance status
+ acceptance_errors: Acceptance errors
+ accepted: Accepted
+ account: Account
+ account_updated: Account updated
+ action: Action
+ actions:
+ cancel: Cancel
+ continue: Continue
+ create: Create
+ destroy: Destroy
+ edit: Edit
+ list: List
+ listing: Listing
+ new: New
+ refund: Refund
+ save: Save
+ update: Update
+ activate: Activate
+ active: Active
+ add: Add
+ add_action_of_type: Add action of type
+ add_country: Add Country
+ add_coupon_code: Add Coupon Code
+ add_new_header: Add New Header
+ add_new_style: Add New Style
+ add_one: Add One
+ add_option_value: Add Option Value
+ add_product: Add Product
+ add_product_properties: Add Product Properties
+ add_rule_of_type: Add rule of type
+ add_state: Add State
+ add_stock: Add Stock
+ add_to_cart: Add To Cart
+ add_variant: Add Variant
+ add_store_credit: Add Store Credit
+ additional_item: Additional Item
+ address: Address
+ address1: Address
+ address2: Address (contd.)
+ addresses: Addresses
+ adjustable: Adjustable
+ adjustment: Adjustment
+ adjustment_amount: Amount
+ adjustment_labels:
+ tax_rates:
+ including_tax: '%{name}%{amount} (Included in Price)'
+ excluding_tax: '%{name}%{amount}'
+ adjustment_successfully_closed: Adjustment has been successfully closed!
+ adjustment_successfully_opened: Adjustment has been successfully opened!
+ adjustment_total: Adjustment Total
+ adjustments: Adjustments
+ admin:
+ tab:
+ configuration: Configuration
+ option_types: Option Types
+ orders: Orders
+ overview: Overview
+ products: Products
+ promotions: Promotions
+ promotion_categories: Promotion Categories
+ properties: Properties
+ prototypes: Prototypes
+ reports: Reports
+ taxonomies: Taxonomies
+ taxons: Taxons
+ users: Users
+ return_authorizations: Return Authorizations
+ customer_returns: Customer Returns
+ order:
+ events:
+ approve: approve
+ cancel: cancel
+ resume: resume
+ resend: Resend
+ user:
+ account: Account
+ addresses: Addresses
+ items: Items
+ items_purchased: Items Purchased
+ order_history: Order History
+ order_num: "Order #"
+ orders: Orders
+ user_information: User Information
+ stores: Stores
+ store_credits: Store Credits
+ no_store_credit: User has no Store Credit available.
+ available_store_credit: User has %{amount} in Store Credit available.
+ administration: Administration
+ advertise: Advertise
+ agree_to_privacy_policy: Agree to Privacy Policy
+ agree_to_terms_of_service: Agree to Terms of Service
+ all: All
+ all_adjustments_closed: All adjustments successfully closed!
+ all_adjustments_opened: All adjustments successfully opened!
+ all_departments: All departments
+ all_items_have_been_returned: All items have been returned
+ alt_text: Alternative Text
+ alternative_phone: Alternative Phone
+ amount: Amount
+ and: and
+ approve: approve
+ approver: Approver
+ approved_at: Approved at
+ are_you_sure: Are you sure?
+ are_you_sure_delete: Are you sure you want to delete this record?
+ associated_adjustment_closed: The associated adjustment is closed, and will not be recalculated. Do you want to open it?
+ at_symbol: '@'
+ authorization_failure: Authorization Failure
+ authorized: Authorized
+ auto_capture: Auto Capture
+ available_on: Available On
+ available: Available
+ average_order_value: Average Order Value
+ avs_response: AVS Response
+ back: Back
+ back_end: Backend
+ backordered: Backordered
+ back_to_resource_list: 'Back To %{resource} List'
+ back_to_payment: Back To Payment
+ back_to_rma_reason_list: Back To RMA Reason List
+ back_to_store: Back to Store
+ back_to_users_list: Back To Users List
+ backorderable: Backorderable
+ backorderable_default: Backorderable default
+ backorders_allowed: backorders allowed
+ balance_due: Balance Due
+ base_amount: Base Amount
+ base_percent: Base Percent
+ bill_address: Bill Address
+ billing: Billing
+ billing_address: Billing Address
+ both: Both
+ calculated_reimbursements: Calculated Reimbursements
+ calculator: Calculator
+ calculator_settings_warning: If you are changing the calculator type, you must save first before you can edit the calculator settings
+ cancel: cancel
+ canceled: Canceled
+ canceler: Canceler
+ canceled_at: Canceled at
+ cannot_empty_completed_order: Order has already been processed so it cannot be emptied
+ cannot_create_payment_without_payment_methods: You cannot create a payment for an order without any payment methods defined.
+ cannot_create_customer_returns: Cannot create customer returns as this order has no shipped units.
+ cannot_create_returns: Cannot create returns as this order has no shipped units.
+ cannot_perform_operation: Cannot perform requested operation
+ cannot_return_more_than_bought_quantity: Cannot return more than bought quantity.
+ cannot_set_shipping_method_without_address: Cannot set shipping method until customer details are provided.
+ capture: Capture
+ capture_events: Capture events
+ card_code: Card Code
+ card_number: Card Number
+ card_type: Brand
+ card_type_is: Card type is
+ cart: Cart
+ cart_subtotal:
+ one: 'Subtotal (1 item)'
+ other: 'Subtotal (%{count} items)'
+ categories: Categories
+ category: Category
+ channel: Channel
+ charged: Charged
+ checkout: Checkout
+ choose_a_customer: Choose a customer
+ choose_a_taxon_to_sort_products_for: "Choose a taxon to sort products for"
+ choose_currency: Choose Currency
+ choose_dashboard_locale: Choose Dashboard Locale
+ choose_location: Choose location
+ city: City
+ clear_cache: Clear Cache
+ clear_cache_ok: Cache was flushed
+ clear_cache_warning: Clearing cache will temporarily reduce the performance of your store.
+ click_and_drag_on_the_products_to_sort_them: Click and drag on the products to sort them.
+ clone: Clone
+ close: Close
+ close_all_adjustments: Close All Adjustments
+ code: Code
+ company: Company
+ complete: complete
+ configuration: Configuration
+ configurations: Configurations
+ confirm: Confirm
+ confirm_delete: Confirm Deletion
+ confirm_password: Password Confirmation
+ continue: Continue
+ continue_shopping: Continue shopping
+ cost_currency: Cost Currency
+ cost_price: Cost Price
+ could_not_create_customer_return: Could not create customer return
+ could_not_create_stock_movement: There was a problem saving this stock movement. Please try again.
+ count_on_hand: Count On Hand
+ countries: Countries
+ country: Country
+ country_based: Country Based
+ country_name: Name
+ country_names:
+ CA: Canada
+ FRA: France
+ ITA: Italy
+ US: United States of America
+ country_rule:
+ label: Choose must be shipped to this country
+ coupon: Coupon
+ coupon_code: Coupon code
+ coupon_code_apply: Apply
+ coupon_code_already_applied: The coupon code has already been applied to this order
+ coupon_code_applied: The coupon code was successfully applied to your order.
+ coupon_code_better_exists: The previously applied coupon code results in a better deal
+ coupon_code_expired: The coupon code is expired
+ coupon_code_max_usage: Coupon code usage limit exceeded
+ coupon_code_not_eligible: This coupon code is not eligible for this order
+ coupon_code_not_found: The coupon code you entered doesn't exist. Please try again.
+ coupon_code_unknown_error: This coupon code could not be applied to the cart at this time.
+ create: Create
+ create_a_new_account: Create a new account
+ create_new_order: Create new order
+ create_reimbursement: Create reimbursement
+ created_at: Created At
+ created_by: Created By
+ credit: Credit
+ credit_card: Credit Card
+ credit_cards: Credit Cards
+ credit_owed: Credit Owed
+ credited: Credited
+ credits: Credits
+ currency: Currency
+ currency_settings: Currency Settings
+ current: Current
+ current_promotion_usage: ! 'Current Usage: %{count}'
+ customer: Customer
+ customer_details: Customer Details
+ customer_details_updated: Customer Details Updated
+ customer_return: Customer Return
+ customer_returns: Customer Returns
+ customer_search: Customer Search
+ cut: Cut
+ cvv_response: CVV Response
+ date: Date
+ date_completed: Date Completed
+ date_picker:
+ first_day: 0
+ format: ! '%Y/%m/%d'
+ js_format: yy/mm/dd
+ date_range: Date Range
+ default: Default
+ default_country_cannot_be_deleted: Default country cannot be deleted
+ default_refund_amount: Default Refund Amount
+ default_tax: Default Tax
+ default_tax_zone: Default Tax Zone
+ delete: Delete
+ deleted: Deleted
+ delete_from_taxon: Delete From Taxon
+ discontinued_variants_present: Some line items in this order have products that are no longer available.
+ delivery: Delivery
+ depth: Depth
+ details: Details
+ description: Description
+ destination: Destination
+ destroy: Destroy
+ discount_amount: Discount Amount
+ discontinue_on: Discontinue On
+ discontinued: Discontinued
+ dismiss_banner: No. Thanks! I'm not interested, do not display this message again
+ display: Display
+ doesnt_track_inventory: It doesn't track inventory
+ edit: Edit
+ editing_resource: 'Editing %{resource}'
+ editing_rma_reason: Editing RMA Reason
+ editing_user: Editing User
+ eligibility_errors:
+ messages:
+ has_excluded_product: Your cart contains a product that prevents this coupon code from being applied.
+ item_total_less_than: This coupon code can't be applied to orders less than %{amount}.
+ item_total_less_than_or_equal: This coupon code can't be applied to orders less than or equal to %{amount}.
+ item_total_more_than: This coupon code can't be applied to orders higher than %{amount}.
+ item_total_more_than_or_equal: This coupon code can't be applied to orders higher than or equal to %{amount}.
+ limit_once_per_user: This coupon code can only be used once per user.
+ missing_product: This coupon code can't be applied because you don't have all of the necessary products in your cart.
+ missing_taxon: You need to add a product from all applicable categories before applying this coupon code.
+ no_applicable_products: You need to add an applicable product before applying this coupon code.
+ no_matching_taxons: You need to add a product from an applicable category before applying this coupon code.
+ no_user_or_email_specified: You need to login or provide your email before applying this coupon code.
+ no_user_specified: You need to login before applying this coupon code.
+ not_first_order: This coupon code can only be applied to your first order.
+ email: Email
+ empty: Empty
+ empty_cart: Empty Cart
+ enable_mail_delivery: Enable Mail Delivery
+ end: End
+ ending_in: Ending in
+ error: error
+ errors:
+ messages:
+ blank: can't be blank
+ could_not_create_taxon: Could not create taxon
+ no_shipping_methods_available: No shipping methods available for selected location, please change your address and try again.
+ services:
+ get_shipping_rates:
+ no_shipping_address: To generate Shipping Rates Order needs to have a Shipping Address
+ no_line_items: To generate Shipping Rates you need to add some Line Items to Order
+ errors_prohibited_this_record_from_being_saved:
+ one: 1 error prohibited this record from being saved
+ other: ! '%{count} errors prohibited this record from being saved'
+ error_user_destroy_with_orders: User associated with orders cannot be destroyed
+ event: Event
+ events:
+ spree:
+ cart:
+ add: Add to cart
+ checkout:
+ coupon_code_added: Coupon code added
+ content:
+ visited: Visit static content page
+ order:
+ contents_changed: Order contents changed
+ page_view: Static page viewed
+ user:
+ signup: User signup
+ exceptions:
+ count_on_hand_setter: Cannot set count_on_hand manually, as it is set automatically by the recalculate_count_on_hand callback. Please use `update_column(:count_on_hand, value)` instead.
+ exchange_for: Exchange for
+ expedited_exchanges_warning: "Any specified exchanges will ship to the customer immediately upon saving. The customer will be charged the full amount of the item if they do not return the original item within %{days_window} days."
+ excl: excl.
+ expiration: Expiration
+ extension: Extension
+ extensions_directory: Extensions Directory
+ existing_shipments: Existing shipments
+ failed_payment_attempts: Failed Payment Attempts
+ filename: Filename
+ fill_in_customer_info: Please fill in customer info
+ filter: Filter
+ filter_results: Filter Results
+ finalize: Finalize
+ find_a_taxon: Find a Taxon
+ finalized: Finalized
+ first_item: First Item
+ first_name: First Name
+ first_name_begins_with: First Name Begins With
+ flat_percent: Flat Percent
+ flat_rate_per_order: Flat Rate
+ flexible_rate: Flexible Rate
+ forgot_password: Forgot Password?
+ free_shipping: Free Shipping
+ free_shipping_amount: "-"
+ front_end: Front End
+ gateway: Gateway
+ gateway_error: Gateway Error
+ general: General
+ general_settings: General Settings
+ generate_code: Generate coupon code
+ guest_checkout: Guest Checkout
+ guest_user_account: Checkout as a Guest
+ has_no_shipped_units: has no shipped units
+ height: Height
+ home: Home
+ help_center: Help Center
+ i18n:
+ available_locales: Available Locales
+ fields: Fields
+ language: Language
+ localization_settings: Localization Settings
+ only_incomplete: Only incomplete
+ only_complete: Only complete
+ select_locale: Select locale
+ show_only: Show only
+ supported_locales: Supported Locales
+ this_file_language: English (US)
+ translations: Translations
+ icon: Icon
+ image: Image
+ images: Images
+ implement_eligible_for_return: "Must implement #eligible_for_return? for your EligibilityValidator."
+ implement_requires_manual_intervention: "Must implement #requires_manual_intervention? for your EligibilityValidator."
+ inactive: Inactive
+ incl: incl.
+ included_in_price: Included in Price
+ included_price_validation: cannot be selected unless you have set a Default Tax Zone
+ incomplete: Incomplete
+ info_product_has_multiple_skus: "This product has %{count} variants:"
+ info_number_of_skus_not_shown:
+ one: "and one other"
+ other: "and %{count} others"
+ instructions_to_reset_password: Please enter your email on the form below
+ insufficient_stock: Insufficient stock available, only %{on_hand} remaining
+ insufficient_stock_item_quantity: Insufficient stock quantity available
+ insufficient_stock_lines_present: Some line items in this order have insufficient quantity.
+ intercept_email_address: Intercept Email Address
+ intercept_email_instructions: Override email recipient and replace with this address.
+ internal_name: Internal Name
+ invalid_credit_card: Invalid credit card.
+ invalid_exchange_variant: Invalid exchange variant.
+ invalid_payment_provider: Invalid payment provider.
+ invalid_promotion_action: Invalid promotion action.
+ invalid_promotion_rule: Invalid promotion rule.
+ inventory: Inventory
+ inventory_adjustment: Inventory Adjustment
+ inventory_error_flash_for_insufficient_quantity: An item in your cart has become unavailable.
+ inventory_state: Inventory State
+ is_not_available_to_shipment_address: is not available to shipment address
+ iso_name: Iso Name
+ issued_on: Issued On
+ item: Item
+ item_description: Item Description
+ item_total: Item Total
+ item_total_rule:
+ operators:
+ gt: greater than
+ gte: greater than or equal to
+ lt: less than
+ lte: less than or equal to
+ items_cannot_be_shipped: We are unable to calculate shipping rates for the selected items.
+ items_in_rmas: Items in Return Authorizations
+ items_to_be_reimbursed: Items to be reimbursed
+ items_reimbursed: Items reimbursed
+ join_slack: Join Slack
+ last_name: Last Name
+ last_name_begins_with: Last Name Begins With
+ learn_more: Learn More
+ lifetime_stats: Lifetime Stats
+ line_item_adjustments: "Line item adjustments"
+ list: List
+ loading: Loading
+ loading_tree: Loading tree. Please wait…
+ locale_changed: Locale Changed
+ location: Location
+ lock: Lock
+ log_entries: "Log Entries"
+ logs: "Logs"
+ logged_in_as: Logged in as
+ logged_in_succesfully: Logged in successfully
+ logged_out: You have been logged out.
+ login: Login
+ login_as_existing: Login as Existing Customer
+ login_failed: Login authentication failed.
+ login_name: Login
+ logout: Logout
+ look_for_similar_items: Look for similar items
+ make_refund: Make refund
+ make_sure_the_above_reimbursement_amount_is_correct: Make sure the above reimbursement amount is correct
+ manage_promotion_categories: Manage Promotion Categories
+ manual_intervention_required: Manual intervention required
+ manage_variants: Manage Variants
+ master_price: Master Price
+ master_sku: Master SKU
+ match_choices:
+ all: All
+ none: None
+ max_items: Max Items
+ member_since: Member Since
+ memo: Memo
+ meta_description: Meta Description
+ meta_keywords: Meta Keywords
+ meta_title: Meta Title
+ metadata: Metadata
+ mutable: Mutable
+ minimal_amount: Minimal Amount
+ missing_return_authorization: ! 'Missing Return Authorization for %{item_name}.'
+ month: Month
+ more: More
+ move_stock_between_locations: Move Stock Between Locations
+ my_account: My Account
+ my_orders: My Orders
+ name: Name
+ name_on_card: Name on card
+ name_or_sku: Name or SKU (enter at least first 3 characters of product name)
+ new: New
+ new_adjustment: New Adjustment
+ new_customer: New Customer
+ new_customer_return: New Customer Return
+ new_country: New Country
+ new_image: New Image
+ new_option_type: New Option Type
+ new_order: New Order
+ new_order_completed: New Order Completed
+ new_payment: New Payment
+ new_payment_method: New Payment Method
+ new_product: New Product
+ new_promotion: New Promotion
+ new_promotion_category: New Promotion Category
+ new_property: New Property
+ new_prototype: New Prototype
+ new_refund: New Refund
+ new_refund_reason: New Refund Reason
+ new_reimbursement_type: New Reimbursement Type
+ new_rma_reason: New RMA Reason
+ new_return_authorization: New Return Authorization
+ new_role: New Role
+ new_shipping_category: New Shipping Category
+ new_shipping_method: New Shipping Method
+ new_shipment_at_location: New shipment at location
+ new_state: New State
+ new_stock_location: New Stock Location
+ new_stock_movement: New Stock Movement
+ new_stock_transfer: New Stock Transfer
+ new_store_credit: New Store Credit
+ new_store_credit_category: New Store Credit Category
+ new_tax_category: New Tax Category
+ new_tax_rate: New Tax Rate
+ new_taxon: New Taxon
+ new_taxonomy: New Taxonomy
+ new_tracker: New Tracker
+ new_user: New User
+ new_variant: New Variant
+ new_zone: New Zone
+ next: Next
+ no_actions_added: No actions added
+ no_available_date_set: No available date set
+ no_payment_found: No payment found
+ no_pending_payments: No pending payments
+ no_products_found: No products found
+ no_results: No results
+ no_rules_added: No rules added
+ no_resource_found: 'No %{resource} found'
+ no_returns_found: No returns found
+ no_shipping_method_selected: No shipping method selected.
+ no_state_changes: No state changes yet.
+ no_tracking_present: No tracking details provided.
+ user_not_found: User not found
+ none: None
+ none_selected: None Selected
+ normal_amount: Normal Amount
+ not: not
+ not_available: N/A
+ not_enough_stock: There is not enough inventory at the source location to complete this transfer.
+ not_found: ! '%{resource} is not found'
+ note: Note
+ notice_messages:
+ product_cloned: Product has been cloned
+ product_deleted: Product has been deleted
+ product_not_cloned: "Product could not be cloned. Reason: %{error}"
+ product_not_deleted: "Product could not be deleted. Reason: %{error}"
+ variant_deleted: Variant has been deleted
+ variant_not_deleted: "Variant could not be deleted. Reason: %{error}"
+ num_orders: "# Orders"
+ number: Number
+ on_hand: On Hand
+ open: Open
+ open_all_adjustments: Open All Adjustments
+ option_type: Option Type
+ option_type_placeholder: Choose an option type
+ option_types: Option Types
+ option_value: Option Value
+ option_values: Option Values
+ optional: Optional
+ options: Options
+ or: or
+ or_over_price: ! '%{price} or over'
+ order: Order
+ order_adjustments: Order adjustments
+ order_already_updated: The order has already been updated.
+ order_approved: Order approved
+ order_canceled: Order canceled
+ order_details: Order Details
+ order_email_resent: Order Email Resent
+ order_information: Order Information
+ order_line_items: Order Line Items
+ order_mailer:
+ subtotal: ! 'Subtotal:'
+ total: ! 'Order Total:'
+ cancel_email:
+ dear_customer: Dear Customer,
+ instructions: Your order has been CANCELED. Please retain this cancellation information for your records.
+ order_summary_canceled: Order %{number} Summary [CANCELED]
+ subject: Cancellation of Order
+ confirm_email:
+ dear_customer: Dear Customer,
+ instructions: Please review and retain the following order information for your records.
+ order_summary: Order %{number} Summary
+ subject: Order Confirmation
+ thanks: Thank you for your business.
+ order_not_found: We couldn't find your order. Please try that action again.
+ order_number: Order %{number}
+ order_processed_successfully: Your order has been processed successfully
+ order_resumed: Order resumed
+ order_state:
+ address: address
+ awaiting_return: awaiting return
+ canceled: canceled
+ cart: cart
+ considered_risky: considered risky
+ complete: complete
+ confirm: confirm
+ delivery: delivery
+ payment: payment
+ resumed: resumed
+ returned: returned
+ order_summary: Order Summary
+ order_sure_want_to: Are you sure you want to %{event} this order?
+ order_total: Order Total
+ order_updated: Order Updated
+ orders: Orders
+ out_of_stock: Out of Stock
+ backordered_info: Selected item is backordered so expect delays
+ backordered_confirm_info: Selected item is backordered so expect delays. Are you sure you want to order it?
+ overview: Overview
+ package_from: package from
+ pagination:
+ next_page: next page »
+ previous_page: ! '« previous page'
+ truncate: ! '…'
+ password: Password
+ paste: Paste
+ path: Path
+ pay: pay
+ payment: Payment
+ payment_could_not_be_created: Payment could not be created.
+ payment_information: Payment Information
+ payment_method: Payment Method
+ payment_methods: Payment Methods
+ payment_method_not_supported: That payment method is unsupported. Please choose another one.
+ payment_processing_failed: Payment could not be processed, please check the details you entered
+ payment_processor_choose_banner_text: If you need help choosing a payment processor, please visit
+ payment_processor_choose_link: our payments page
+ payment_state: Payment State
+ payment_identifier: Payment Identifier
+ payment_states:
+ balance_due: balance due
+ checkout: checkout
+ completed: completed
+ credit_owed: credit owed
+ failed: failed
+ paid: paid
+ pending: pending
+ processing: processing
+ void: void
+ payment_updated: Payment Updated
+ payments: Payments
+ percent: Percent
+ percent_per_item: Percent Per Item
+ permalink: Permalink
+ pending: Pending
+ pending_sale: Pending Sale
+ phone: Phone
+ place_order: Place Order
+ please_define_payment_methods: Please define some payment methods first.
+ please_enter_reasonable_quantity: Please enter a reasonable quantity.
+ populate_get_error: Something went wrong. Please try adding the item again.
+ powered_by: Powered by
+ pre_tax_refund_amount: Pre-Tax Refund Amount
+ pre_tax_amount: Pre-Tax Amount
+ pre_tax_total: Pre-Tax Total
+ preferred_reimbursement_type: Preferred Reimbursement Type
+ presentation: Presentation
+ preview_product: Preview Product
+ previous: Previous
+ previous_state_missing: "n/a"
+ price: Price
+ price_range: Price Range
+ price_sack: Price Sack
+ process: Process
+ product: Product
+ product_details: Product Details
+ product_has_no_description: This product has no description
+ product_not_available_in_this_currency: This product is not available in the selected currency.
+ product_properties: Product Properties
+ product_rule:
+ choose_products: Choose products
+ label: Order must contain x amount of these products
+ match_all: all
+ match_any: at least one
+ match_none: none
+ product_source:
+ group: From product group
+ manual: Manually choose
+ products: Products
+ promotion: Promotion
+ promotionable: Promotable
+ promotion_cloned: Promotion has been cloned
+ promotion_not_cloned: "Promotion has not been cloned. Reason: %{error}"
+ promotion_action: Promotion Action
+ promotion_action_types:
+ create_adjustment:
+ description: Creates a promotion credit adjustment on the order
+ name: Create whole-order adjustment
+ create_item_adjustments:
+ description: Creates a promotion credit adjustment on a line item
+ name: Create per-line-item adjustment
+ create_line_items:
+ description: Populates the cart with the specified quantity of variant
+ name: Create line items
+ free_shipping:
+ description: Makes all shipments for the order free
+ name: Free shipping
+ promotion_actions: Actions
+ promotion_category: Promotion Category
+ promotion_form:
+ match_policies:
+ all: Match all of these rules
+ any: Match any of these rules
+ promotion_label: 'Promotion (%{name})'
+ promotion_rule: Promotion Rule
+ promotion_rule_types:
+ country:
+ description: Order is shipped to proper (or default) country
+ name: Country
+ first_order:
+ description: Must be the customer's first order
+ name: First order
+ item_total:
+ description: Order total meets these criteria
+ name: Item total
+ one_use_per_user:
+ description: Only One Use Per User
+ name: One Use Per User
+ option_value:
+ description: Order includes specified product(s) with matching option value(s)
+ name: Option Value(s)
+ product:
+ description: Order includes specified product(s)
+ name: Product(s)
+ user:
+ description: Available only to the specified users
+ name: User
+ user_logged_in:
+ description: Available only to logged in users
+ name: User Logged In
+ taxon:
+ description: Order includes products with specified taxon(s)
+ name: Taxon(s)
+ promotions: Promotions
+ promotion_uses: Promotion uses
+ propagate_all_variants: Propagate all variants
+ properties: Properties
+ property: Property
+ prototype: Prototype
+ prototypes: Prototypes
+ provider: Provider
+ provider_settings_warning: If you are changing the provider type, you must save first before you can edit the provider settings
+ purchased_quantity: Purchased Quantity
+ qty: Qty
+ quantity: Quantity
+ quantity_returned: Quantity Returned
+ quantity_shipped: Quantity Shipped
+ quick_search: Quick search..
+ rate: Rate
+ reason: Reason
+ receive: receive
+ receive_stock: Receive Stock
+ received: Received
+ reception_status: Reception Status
+ reference: Reference
+ reference_contains: Reference Contains
+ refund: Refund
+ refund_reasons: Refund Reasons
+ refunded_amount: Refunded Amount
+ refunds: Refunds
+ refund_amount_must_be_greater_than_zero: Refund amount must be greater than zero
+ register: Register
+ registration: Registration
+ reimburse: Reimburse
+ reimbursed: Reimbursed
+ reimbursement: Reimbursement
+ reimbursement_perform_failed: "Reimbursement could not be performed. Error: %{error}"
+ reimbursement_status: Reimbursement status
+ reimbursement_type: Reimbursement type
+ reimbursement_type_override: Reimbursement Type Override
+ reimbursement_types: Reimbursement Types
+ reimbursements: Reimbursements
+ reject: Reject
+ rejected: Rejected
+ remember_me: Remember me
+ remove: Remove
+ rename: Rename
+ report: Report
+ reports: Reports
+ resellable: Resellable
+ resend: Resend
+ reset_password: Reset my password
+ response_code: Response Code
+ resume: resume
+ resumed: Resumed
+ return: return
+ returns: Returns
+ return_authorization: Return Authorization
+ return_authorization_reasons: Return Authorization Reasons
+ return_authorization_states:
+ authorized: Authorized
+ canceled: Canceled
+ return_authorization_updated: Return authorization updated
+ return_authorizations: Return Authorizations
+ return_item_inventory_unit_ineligible: Return item's inventory unit must be shipped
+ return_item_inventory_unit_reimbursed: Return item's inventory unit is already reimbursed
+ return_item_order_not_completed: Return item's order must be completed
+ return_item_rma_ineligible: Return item requires an RMA
+ return_item_time_period_ineligible: Return item is outside the eligible time period
+ return_items: Return Items
+ return_items_cannot_be_associated_with_multiple_orders: Return items cannot be associated with multiple orders.
+ reimbursement_mailer:
+ reimbursement_email:
+ days_to_send: ! 'You have %{days} days to send back any items awaiting exchange.'
+ dear_customer: Dear Customer,
+ exchange_summary: Exchange Summary
+ for: for
+ instructions: Your reimbursement has been processed.
+ refund_summary: Refund Summary
+ subject: Reimbursement Notification
+ total_refunded: ! 'Total refunded: %{total}'
+ thanks: Thank you for your business.
+ return_number: Return Number
+ return_quantity: Return Quantity
+ returned: Returned
+ review: Review
+ risk: Risk
+ risk_analysis: Risk Analysis
+ risky: Risky
+ rma_credit: RMA Credit
+ rma_number: RMA Number
+ rma_value: RMA Value
+ role_id: Role ID
+ roles: Roles
+ rules: Rules
+ safe: Safe
+ sales_total: Sales Total
+ sales_total_description: Sales Total For All Orders
+ sales_totals: Sales Totals
+ save_and_continue: Save and Continue
+ save_my_address: Save my address
+ saving: Saving
+ say_no: 'No'
+ say_yes: 'Yes'
+ scope: Scope
+ search: Search
+ search_results: Search results for '%{keywords}'
+ searching: Searching
+ secure_connection_type: Secure Connection Type
+ security_settings: Security Settings
+ select: Select
+ select_from_prototype: Select From Prototype
+ select_a_return_authorization_reason: Select a reason for the return authorization
+ select_a_stock_location: Select a stock location
+ select_a_store_credit_reason: Select a reason for the store credit
+ select_stock: Select stock
+ selected_quantity_not_available: ! 'selected of %{item} is not available.'
+ send_copy_of_all_mails_to: Send Copy of All Mails To
+ send_mails_as: Send Mails As
+ server: Server
+ server_error: The server returned an error
+ settings: Settings
+ ship: Ship
+ ship_address: Ship Address
+ ship_total: Ship Total
+ shipment: Shipment
+ shipment_adjustments: "Shipment adjustments"
+ shipment_details: "From %{stock_location} via %{shipping_method}"
+ shipment_mailer:
+ shipped_email:
+ dear_customer: Dear Customer,
+ instructions: Your order %{number} has been shipped
+ shipment_summary: Shipment Summary for order
+ shipping_method: "Shipping method: %{shipping_method}"
+ subject: Shipment Notification
+ thanks: Thank you for your business.
+ track_information: ! 'Tracking Information: %{tracking}'
+ track_link: "Tracking Link: %{url}"
+ shipment_state: Shipment State
+ shipment_states:
+ backorder: backorder
+ canceled: canceled
+ partial: partial
+ pending: pending
+ ready: ready
+ shipped: shipped
+ shipment_transfer_success: 'Variants successfully transferred'
+ shipment_transfer_error: 'There was an error transferring variants'
+ shipments: Shipments
+ shipped: Shipped
+ shipping: Shipping
+ shipping_address: Shipping Address
+ shipping_categories: Shipping Categories
+ shipping_category: Shipping Category
+ shipping_flat_rate_per_item: Flat rate per package item
+ shipping_flat_rate_per_order: Flat rate
+ shipping_flexible_rate: Flexible Rate per package item
+ shipping_instructions: Shipping Instructions
+ shipping_method: Shipping Method
+ shipping_methods: Shipping Methods
+ shipping_price_sack: Price sack
+ shipping_rates:
+ display_price:
+ including_tax: "%{price} (incl. %{tax_amount} %{tax_rate_name})"
+ excluding_tax: "%{price} (+ %{tax_amount} %{tax_rate_name})"
+ shipping_total: Shipping total
+ shop_by_taxonomy: Shop by %{taxonomy}
+ shopping_cart: Shopping Cart
+ show: Show
+ show_active: Show Active
+ show_deleted: Show Deleted
+ show_discontinued: Show Discontinued
+ show_only_complete_orders: Only show complete orders
+ show_only_considered_risky: Only show risky orders
+ show_rate_in_label: Show rate in label
+ sku: SKU
+ skus: SKUs
+ slug: Slug
+ source: Source
+ special_instructions: Special Instructions
+ split: Split
+ spree_gateway_error_flash_for_checkout: There was a problem with your payment information. Please check your information and try again.
+ ssl:
+ change_protocol: "Please switch to using HTTP (rather than HTTPS) and retry this request."
+ start: Start
+ state: State
+ state_based: State Based
+ states: States
+ state_machine_states:
+ accepted: Accepted
+ address: Address
+ authorized: Authorized
+ awaiting: Awaiting
+ awaiting_return: Awaiting return
+ backordered: Back ordered
+ cart: Cart
+ canceled: Canceled
+ checkout: Checkout
+ confirm: Confirm
+ complete: Complete
+ completed: Completed
+ closed: Closed
+ delivery: Delivery
+ errored: Errored
+ failed: Failed
+ given_to_customer: Given to customer
+ invalid: Invalid
+ manual_intervention_required: Manual intervention required
+ open: Open
+ order: Order
+ on_hand: On hand
+ payment: Payment
+ pending: Pending
+ processing: Processing
+ ready: Ready
+ reimbursed: Reimbursed
+ resumed: Resumed
+ returned: Returned
+ shipped: Shipped
+ void: Void
+ states_required: States Required
+ status: Status
+ stock: Stock
+ stock_location: Stock Location
+ stock_location_info: Stock location info
+ stock_locations: Stock Locations
+ stock_locations_need_a_default_country: You must create a default country before creating a stock location.
+ stock_management: Stock Management
+ stock_management_requires_a_stock_location: Please create a stock location in order to manage stock.
+ stock_movements: Stock Movements
+ stock_movements_for_stock_location: Stock Movements for %{stock_location_name}
+ stock_successfully_transferred: Stock was successfully transferred between locations.
+ stock_transfer_name: Stock Transfer
+ stock_transfer:
+ errors:
+ must_have_variant: You must add atleast one variant.
+ stock_transfers: Stock Transfers
+ stop: Stop
+ store: Store
+ stores: Stores
+ add_store: New Store
+ store_credit_name: Store Credit
+ store_credit:
+ credit: "Credit"
+ authorized: "Authorized"
+ captured: "Used"
+ allocated: "Added"
+ apply: Apply Store Credit
+ remove: Remove Store Credit
+ applicable_amount: "%{amount} in store credit will be applied to this order."
+ available_amount: "You have %{amount} in Store Credit available!"
+ remaining_amount: "You have %{amount} remaining in your account's Store Credit."
+ additional_payment_needed: Select another payment method for the remaining %{amount}.
+ errors:
+ cannot_change_used_store_credit: You cannot change a store credit that has already been used
+ unable_to_create: Unable to create store credit
+ unable_to_delete: Unable to delete store credit
+ unable_to_fund: Unable to pay for order using store credits
+ unable_to_update: Unable to update store credit
+ store_credits: Store Credits
+ store_credit_categories: Store Credit Categories
+ store_credit_payment_method:
+ unable_to_void: "Unable to void code: %{auth_code}"
+ unable_to_credit: "Unable to credit code: %{auth_code}"
+ successful_action: "Successful store credit %{action}"
+ unable_to_find: "Could not find store credit"
+ insufficient_funds: "Store credit amount remaining is not sufficient"
+ currency_mismatch: "Store credit currency does not match order currency"
+ insufficient_authorized_amount: "Unable to capture more than authorized amount"
+ unable_to_find_for_action: "Could not find store credit for auth code: %{auth_code} for action: %{action}"
+ user_has_no_store_credits: "User does not have any available store credit"
+ select_one_store_credit: "Select store credit to go towards remaining balance"
+ store_default: Default store
+ store_set_default_button: Set as default
+ store_not_set_as_default: Couldn't set store %{store} as a default store
+ store_set_as_default: Store %{store} is now a default store
+ street_address: Street Address
+ street_address_2: Street Address (cont'd)
+ subtotal: Subtotal
+ subtract: Subtract
+ success: Success
+ successfully_created: ! '%{resource} has been successfully created!'
+ successfully_refunded: ! '%{resource} has been successfully refunded!'
+ successfully_removed: ! '%{resource} has been successfully removed!'
+ successfully_updated: ! '%{resource} has been successfully updated!'
+ summary: Summary
+ tax: Tax
+ tax_included: "Tax (incl.)"
+ tax_categories: Tax Categories
+ tax_category: Tax Category
+ tax_code: Tax Code
+ tax_rate_amount_explanation: Tax rates are a decimal amount to aid in calculations, (i.e. if the tax rate is 5% then enter 0.05)
+ tax_rates: Tax Rates
+ taxon: Taxon
+ taxon_edit: Edit Taxon
+ taxon_placeholder: Add a Taxon
+ tags: Tags
+ tags_placeholder: Add Tags
+ taxon_rule:
+ choose_taxons: Choose taxons
+ label: Order must contain x amount of these taxons
+ match_all: all
+ match_any: at least one
+ taxonomies: Taxonomies
+ taxonomy: Taxonomy
+ taxonomy_brands_name: Brands
+ taxonomy_categories_name: Categories
+ taxonomy_edit: Edit taxonomy
+ taxonomy_tree_error: The requested change has not been accepted and the tree has been returned to its previous state, please try again.
+ taxonomy_tree_instruction: ! '* Right click a child in the tree to access the menu for adding, deleting or sorting a child.'
+ taxons: Taxons
+ test: Test
+ test_mailer:
+ test_email:
+ greeting: Congratulations!
+ message: If you have received this email, then your email settings are correct.
+ subject: Test Mail
+ test_mode: Test Mode
+ thank_you_for_your_order: Thank you for your business. Please print out a copy of this confirmation page for your records.
+ there_are_no_items_for_this_order: There are no items for this order. Please add an item to the order to continue.
+ there_were_problems_with_the_following_fields: There were problems with the following fields
+ this_order_has_already_received_a_refund: This order has already received a refund
+ thumbnail: Thumbnail
+ tiers: Tiers
+ tiered_flat_rate: Tiered Flat Rate
+ tiered_percent: Tiered Percent
+ time: Time
+ to_add_variants_you_must_first_define: To add variants, you must first define
+ total: Total
+ total_per_item: Total per item
+ total_pre_tax_refund: Total Pre-Tax Refund
+ total_price: Total price
+ total_sales: Total Sales
+ track_inventory: Track Inventory
+ tracking: Tracking
+ tracking_number: Tracking Number
+ tracking_url: Tracking URL
+ tracking_url_placeholder: e.g. http://quickship.com/package?num=:tracking
+ transaction_id: Transaction ID
+ transfer_from_location: Transfer From
+ transfer_stock: Transfer Stock
+ transfer_to_location: Transfer To
+ tree: Tree
+ type: Type
+ type_to_search: Type to search
+ unable_to_connect_to_gateway: Unable to connect to gateway.
+ unable_to_create_reimbursements: Unable to create reimbursements because there are items pending manual intervention.
+ under_price: Under %{price}
+ unlock: Unlock
+ unrecognized_card_type: Unrecognized card type
+ unshippable_items: Unshippable Items
+ update: Update
+ updating: Updating
+ url: URL
+ usage_limit: Usage Limit
+ use_app_default: Use App Default
+ use_billing_address: Use Billing Address
+ use_existing_cc: Use an existing card on file
+ use_new_cc: Use a new card
+ use_new_cc_or_payment_method: Use a new card / payment method
+ use_s3: Use Amazon S3 For Images
+ used: Used
+ user: User
+ user_rule:
+ choose_users: Choose users
+ users: Users
+ validation:
+ unpaid_amount_not_zero: "Amount was not fully reimbursed. Still due: %{amount}"
+ cannot_be_less_than_shipped_units: cannot be less than the number of shipped units.
+ cannot_destroy_line_item_as_inventory_units_have_shipped: Cannot destroy line item as some inventory units have shipped.
+ exceeds_available_stock: exceeds available stock. Please ensure line items have a valid quantity.
+ is_too_large: is too large -- stock on hand cannot cover requested quantity!
+ must_be_int: must be an integer
+ must_be_non_negative: must be a non-negative value
+ value: Value
+ variant: Variant
+ variant_placeholder: Choose a variant
+ variants: Variants
+ version: Version
+ void: Void
+ weight: Weight
+ what_is_a_cvv: What is a (CVV) Credit Card Code?
+ what_is_this: What's This?
+ width: Width
+ year: Year
+ you_have_no_orders_yet: You have no orders yet
+ your_cart_is_empty: Your cart is empty
+ your_order_is_empty_add_product: Your order is empty, please search for and add a product above
+ zip: Zip
+ zipcode: Zip Code
+ zone: Zone
+ zones: Zones
diff --git a/core/config/routes.rb b/core/config/routes.rb
new file mode 100644
index 00000000000..98125a5fef4
--- /dev/null
+++ b/core/config/routes.rb
@@ -0,0 +1,5 @@
+Spree::Core::Engine.add_routes do
+ get '/forbidden', to: 'home#forbidden', as: :forbidden
+end
+
+Spree::Core::Engine.draw_routes
diff --git a/core/db/default/spree/countries.rb b/core/db/default/spree/countries.rb
new file mode 100644
index 00000000000..d4829eb099a
--- /dev/null
+++ b/core/db/default/spree/countries.rb
@@ -0,0 +1,18 @@
+require 'carmen'
+
+Carmen::Country.all.each do |country|
+ Spree::Country.where(
+ name: country.name,
+ iso3: country.alpha_3_code,
+ iso: country.alpha_2_code,
+ iso_name: country.name.upcase,
+ numcode: country.numeric_code,
+ states_required: country.subregions?
+ ).first_or_create
+end
+
+Spree::Config[:default_country_id] = Spree::Country.find_by(iso: 'US').id
+
+# find countries that do not use postal codes (by iso) and set 'zipcode_required' to false for them.
+
+Spree::Country.where(iso: Spree::Address::NO_ZIPCODE_ISO_CODES).update_all(zipcode_required: false)
diff --git a/core/db/default/spree/default_reimbursement_type.rb b/core/db/default/spree/default_reimbursement_type.rb
new file mode 100644
index 00000000000..62c1e99212d
--- /dev/null
+++ b/core/db/default/spree/default_reimbursement_type.rb
@@ -0,0 +1 @@
+Spree::RefundReason.find_or_create_by!(name: 'Return processing', mutable: false)
diff --git a/core/db/default/spree/roles.rb b/core/db/default/spree/roles.rb
new file mode 100644
index 00000000000..afdffc4255b
--- /dev/null
+++ b/core/db/default/spree/roles.rb
@@ -0,0 +1,2 @@
+Spree::Role.where(name: 'admin').first_or_create
+Spree::Role.where(name: 'user').first_or_create
diff --git a/core/db/default/spree/states.rb b/core/db/default/spree/states.rb
new file mode 100644
index 00000000000..c11b65ca0c1
--- /dev/null
+++ b/core/db/default/spree/states.rb
@@ -0,0 +1,12 @@
+Spree::Country.where(states_required: true).each do |country|
+ carmen_country = Carmen::Country.named(country.name)
+ next unless carmen_country
+
+ carmen_country.subregions.each do |subregion|
+ country.states.where(
+ name: subregion.name,
+ abbr: subregion.code,
+ country_id: country.id
+ ).first_or_create
+ end
+end
diff --git a/core/db/default/spree/stores.rb b/core/db/default/spree/stores.rb
new file mode 100644
index 00000000000..f6f6cf53507
--- /dev/null
+++ b/core/db/default/spree/stores.rb
@@ -0,0 +1,9 @@
+# Possibly already created by a migration.
+unless Spree::Store.where(code: 'spree').exists?
+ Spree::Store.new do |s|
+ s.code = 'spree'
+ s.name = 'Spree Demo Site'
+ s.url = 'example.com'
+ s.mail_from_address = 'spree@example.com'
+ end.save!
+end
diff --git a/core/db/default/spree/zones.rb b/core/db/default/spree/zones.rb
new file mode 100644
index 00000000000..e12ef3586d0
--- /dev/null
+++ b/core/db/default/spree/zones.rb
@@ -0,0 +1,10 @@
+eu_vat = Spree::Zone.where(name: 'EU_VAT', description: 'Countries that make up the EU VAT zone.', kind: 'country').first_or_create!
+north_america = Spree::Zone.where(name: 'North America', description: 'USA + Canada', kind: 'country').first_or_create!
+
+%w(PL FI PT RO DE FR SK HU SI IE AT ES IT BE SE LV BG GB LT CY LU MT DK NL EE HR CZ GR).each do |name|
+ eu_vat.zone_members.where(zoneable: Spree::Country.find_by!(iso: name)).first_or_create!
+end
+
+%w(US CA).each do |name|
+ north_america.zone_members.where(zoneable: Spree::Country.find_by!(iso: name)).first_or_create!
+end
diff --git a/core/db/migrate/20120831092320_spree_one_two.rb b/core/db/migrate/20120831092320_spree_one_two.rb
new file mode 100644
index 00000000000..181a8b33f3c
--- /dev/null
+++ b/core/db/migrate/20120831092320_spree_one_two.rb
@@ -0,0 +1,481 @@
+class SpreeOneTwo < ActiveRecord::Migration[4.2]
+ def up
+ # This migration is just a compressed version of all the previous
+ # migrations for spree_core. Do not run it if one of the core tables
+ # already exists. Assume the best.
+ return if data_source_exists?(:spree_addresses)
+
+
+ create_table :spree_activators do |t|
+ t.string :description
+ t.datetime :expires_at
+ t.datetime :starts_at
+ t.string :name
+ t.string :event_name
+ t.string :type
+ t.integer :usage_limit
+ t.string :match_policy, default: 'all'
+ t.string :code
+ t.boolean :advertise, default: false
+ t.string :path
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_addresses do |t|
+ t.string :firstname
+ t.string :lastname
+ t.string :address1
+ t.string :address2
+ t.string :city
+ t.string :zipcode
+ t.string :phone
+ t.string :state_name
+ t.string :alternative_phone
+ t.string :company
+ t.references :state
+ t.references :country
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_addresses, [:firstname], name: 'index_addresses_on_firstname'
+ add_index :spree_addresses, [:lastname], name: 'index_addresses_on_lastname'
+
+ create_table :spree_adjustments do |t|
+ t.references :source, polymorphic: true
+ t.references :adjustable, polymorphic: true
+ t.references :originator, polymorphic: true
+ t.decimal :amount, precision: 8, scale: 2
+ t.string :label
+ t.boolean :mandatory
+ t.boolean :locked
+ t.boolean :eligible, default: true
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_adjustments, [:adjustable_id], name: 'index_adjustments_on_order_id'
+
+ create_table :spree_assets do |t|
+ t.references :viewable, polymorphic: true
+ t.integer :attachment_width
+ t.integer :attachment_height
+ t.integer :attachment_file_size
+ t.integer :position
+ t.string :attachment_content_type
+ t.string :attachment_file_name
+ t.string :type, limit: 75
+ t.datetime :attachment_updated_at
+ t.text :alt
+ end
+
+ add_index :spree_assets, [:viewable_id], name: 'index_assets_on_viewable_id'
+ add_index :spree_assets, [:viewable_type, :type], name: 'index_assets_on_viewable_type_and_type'
+
+ create_table :spree_calculators do |t|
+ t.string :type
+ t.references :calculable, polymorphic: true
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_configurations do |t|
+ t.string :name
+ t.string :type, limit: 50
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_configurations, [:name, :type], name: 'index_spree_configurations_on_name_and_type'
+
+ create_table :spree_countries do |t|
+ t.string :iso_name
+ t.string :iso
+ t.string :iso3
+ t.string :name
+ t.integer :numcode
+ end
+
+ create_table :spree_credit_cards do |t|
+ t.string :month
+ t.string :year
+ t.string :cc_type
+ t.string :last_digits
+ t.string :first_name
+ t.string :last_name
+ t.string :start_month
+ t.string :start_year
+ t.string :issue_number
+ t.references :address
+ t.string :gateway_customer_profile_id
+ t.string :gateway_payment_profile_id
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_gateways do |t|
+ t.string :type
+ t.string :name
+ t.text :description
+ t.boolean :active, default: true
+ t.string :environment, default: 'development'
+ t.string :server, default: 'test'
+ t.boolean :test_mode, default: true
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_inventory_units do |t|
+ t.integer :lock_version, default: 0
+ t.string :state
+ t.references :variant
+ t.references :order
+ t.references :shipment
+ t.references :return_authorization
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_inventory_units, [:order_id], name: 'index_inventory_units_on_order_id'
+ add_index :spree_inventory_units, [:shipment_id], name: 'index_inventory_units_on_shipment_id'
+ add_index :spree_inventory_units, [:variant_id], name: 'index_inventory_units_on_variant_id'
+
+ create_table :spree_line_items do |t|
+ t.references :variant
+ t.references :order
+ t.integer :quantity, null: false
+ t.decimal :price, precision: 8, scale: 2, null: false
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_line_items, [:order_id], name: 'index_spree_line_items_on_order_id'
+ add_index :spree_line_items, [:variant_id], name: 'index_spree_line_items_on_variant_id'
+
+ create_table :spree_log_entries do |t|
+ t.references :source, polymorphic: true
+ t.text :details
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_mail_methods do |t|
+ t.string :environment
+ t.boolean :active, default: true
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_option_types do |t|
+ t.string :name, limit: 100
+ t.string :presentation, limit: 100
+ t.integer :position, default: 0, null: false
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_option_types_prototypes, id: false do |t|
+ t.references :prototype
+ t.references :option_type
+ end
+
+ create_table :spree_option_values do |t|
+ t.integer :position
+ t.string :name
+ t.string :presentation
+ t.references :option_type
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_option_values_variants, id: false do |t|
+ t.references :variant
+ t.references :option_value
+ end
+
+ add_index :spree_option_values_variants, [:variant_id, :option_value_id], name: 'index_option_values_variants_on_variant_id_and_option_value_id'
+ add_index :spree_option_values_variants, [:variant_id], name: 'index_spree_option_values_variants_on_variant_id'
+
+ create_table :spree_orders do |t|
+ t.string :number, limit: 15
+ t.decimal :item_total, precision: 8, scale: 2, default: 0.0, null: false
+ t.decimal :total, precision: 8, scale: 2, default: 0.0, null: false
+ t.string :state
+ t.decimal :adjustment_total, precision: 8, scale: 2, default: 0.0, null: false
+ t.references :user
+ t.datetime :completed_at
+ t.references :bill_address
+ t.references :ship_address
+ t.decimal :payment_total, precision: 8, scale: 2, default: 0.0
+ t.references :shipping_method
+ t.string :shipment_state
+ t.string :payment_state
+ t.string :email
+ t.text :special_instructions
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_orders, [:number], name: 'index_spree_orders_on_number'
+
+ create_table :spree_payment_methods do |t|
+ t.string :type
+ t.string :name
+ t.text :description
+ t.boolean :active, default: true
+ t.string :environment, default: 'development'
+ t.datetime :deleted_at
+ t.string :display_on
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_payments do |t|
+ t.decimal :amount, precision: 8, scale: 2, default: 0.0, null: false
+ t.references :order
+ t.references :source, polymorphic: true
+ t.references :payment_method
+ t.string :state
+ t.string :response_code
+ t.string :avs_response
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_preferences do |t|
+ t.string :name, limit: 100
+ t.references :owner, polymorphic: true
+ t.text :value
+ t.string :key
+ t.string :value_type
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_preferences, [:key], name: 'index_spree_preferences_on_key', unique: true
+
+ create_table :spree_product_option_types do |t|
+ t.integer :position
+ t.references :product
+ t.references :option_type
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_product_properties do |t|
+ t.string :value
+ t.references :product
+ t.references :property
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_product_properties, [:product_id], name: 'index_product_properties_on_product_id'
+
+ create_table :spree_products do |t|
+ t.string :name, default: '', null: false
+ t.text :description
+ t.datetime :available_on
+ t.datetime :deleted_at
+ t.string :permalink
+ t.string :meta_description
+ t.string :meta_keywords
+ t.references :tax_category
+ t.references :shipping_category
+ t.integer :count_on_hand, default: 0, null: false
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_products, [:available_on], name: 'index_spree_products_on_available_on'
+ add_index :spree_products, [:deleted_at], name: 'index_spree_products_on_deleted_at'
+ add_index :spree_products, [:name], name: 'index_spree_products_on_name'
+ add_index :spree_products, [:permalink], name: 'index_spree_products_on_permalink'
+
+ create_table :spree_products_taxons, id: false do |t|
+ t.references :product
+ t.references :taxon
+ end
+
+ add_index :spree_products_taxons, [:product_id], name: 'index_spree_products_taxons_on_product_id'
+ add_index :spree_products_taxons, [:taxon_id], name: 'index_spree_products_taxons_on_taxon_id'
+
+ create_table :spree_properties do |t|
+ t.string :name
+ t.string :presentation, null: false
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_properties_prototypes, id: false do |t|
+ t.references :prototype
+ t.references :property
+ end
+
+ create_table :spree_prototypes do |t|
+ t.string :name
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_return_authorizations do |t|
+ t.string :number
+ t.string :state
+ t.decimal :amount, precision: 8, scale: 2, default: 0.0, null: false
+ t.references :order
+ t.text :reason
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_roles do |t|
+ t.string :name
+ end
+
+ create_table :spree_roles_users, id: false do |t|
+ t.references :role
+ t.references :user
+ end
+
+ add_index :spree_roles_users, [:role_id], name: 'index_spree_roles_users_on_role_id'
+ add_index :spree_roles_users, [:user_id], name: 'index_spree_roles_users_on_user_id'
+
+ create_table :spree_shipments do |t|
+ t.string :tracking
+ t.string :number
+ t.decimal :cost, precision: 8, scale: 2
+ t.datetime :shipped_at
+ t.references :order
+ t.references :shipping_method
+ t.references :address
+ t.string :state
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_shipments, [:number], name: 'index_shipments_on_number'
+
+ create_table :spree_shipping_categories do |t|
+ t.string :name
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_shipping_methods do |t|
+ t.string :name
+ t.references :zone
+ t.string :display_on
+ t.references :shipping_category
+ t.boolean :match_none
+ t.boolean :match_all
+ t.boolean :match_one
+ t.datetime :deleted_at
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_state_changes do |t|
+ t.string :name
+ t.string :previous_state
+ t.references :stateful
+ t.references :user
+ t.string :stateful_type
+ t.string :next_state
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_states do |t|
+ t.string :name
+ t.string :abbr
+ t.references :country
+ end
+
+ create_table :spree_tax_categories do |t|
+ t.string :name
+ t.string :description
+ t.boolean :is_default, default: false
+ t.datetime :deleted_at
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_tax_rates do |t|
+ t.decimal :amount, precision: 8, scale: 5
+ t.references :zone
+ t.references :tax_category
+ t.boolean :included_in_price, default: false
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_taxonomies do |t|
+ t.string :name, null: false
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_taxons do |t|
+ t.references :parent
+ t.integer :position, default: 0
+ t.string :name, null: false
+ t.string :permalink
+ t.references :taxonomy
+ t.integer :lft
+ t.integer :rgt
+ t.string :icon_file_name
+ t.string :icon_content_type
+ t.integer :icon_file_size
+ t.datetime :icon_updated_at
+ t.text :description
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_taxons, [:parent_id], name: 'index_taxons_on_parent_id'
+ add_index :spree_taxons, [:permalink], name: 'index_taxons_on_permalink'
+ add_index :spree_taxons, [:taxonomy_id], name: 'index_taxons_on_taxonomy_id'
+
+ create_table :spree_tokenized_permissions, force: true do |t|
+ t.references :permissable, polymorphic: true
+ t.string :token
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_tokenized_permissions, [:permissable_id, :permissable_type], name: 'index_tokenized_name_and_type'
+
+ create_table :spree_trackers do |t|
+ t.string :environment
+ t.string :analytics_id
+ t.boolean :active, default: true
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_users do |t|
+ t.string :encrypted_password, limit: 128
+ t.string :password_salt, limit: 128
+ t.string :email
+ t.string :remember_token
+ t.string :persistence_token
+ t.string :reset_password_token
+ t.string :perishable_token
+ t.integer :sign_in_count, default: 0, null: false
+ t.integer :failed_attempts, default: 0, null: false
+ t.datetime :last_request_at
+ t.datetime :current_sign_in_at
+ t.datetime :last_sign_in_at
+ t.string :current_sign_in_ip
+ t.string :last_sign_in_ip
+ t.string :login
+ t.references :ship_address
+ t.references :bill_address
+ t.string :authentication_token
+ t.string :unlock_token
+ t.datetime :locked_at
+ t.datetime :remember_created_at
+ t.datetime :reset_password_sent_at
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_variants do |t|
+ t.string :sku, default: '', null: false
+ t.decimal :price, precision: 8, scale: 2, null: false
+ t.decimal :weight, precision: 8, scale: 2
+ t.decimal :height, precision: 8, scale: 2
+ t.decimal :width, precision: 8, scale: 2
+ t.decimal :depth, precision: 8, scale: 2
+ t.datetime :deleted_at
+ t.boolean :is_master, default: false
+ t.references :product
+ t.integer :count_on_hand, default: 0, null: false
+ t.decimal :cost_price, precision: 8, scale: 2
+ t.integer :position
+ end
+
+ add_index :spree_variants, [:product_id], name: 'index_spree_variants_on_product_id'
+
+ create_table :spree_zone_members do |t|
+ t.references :zoneable, polymorphic: true
+ t.references :zone
+ t.timestamps null: false, precision: 6
+ end
+
+ create_table :spree_zones do |t|
+ t.string :name
+ t.string :description
+ t.boolean :default_tax, default: false
+ t.integer :zone_members_count, default: 0
+ t.timestamps null: false, precision: 6
+ end
+ end
+end
diff --git a/core/db/migrate/20120831092359_spree_promo_one_two.rb b/core/db/migrate/20120831092359_spree_promo_one_two.rb
new file mode 100644
index 00000000000..53d55603b99
--- /dev/null
+++ b/core/db/migrate/20120831092359_spree_promo_one_two.rb
@@ -0,0 +1,45 @@
+class SpreePromoOneTwo < ActiveRecord::Migration[4.2]
+ def up
+ # This migration is just a compressed migration for all previous versions of spree_promo
+ return if data_source_exists?(:spree_products_promotion_rules)
+
+ create_table :spree_products_promotion_rules, id: false, force: true do |t|
+ t.references :product
+ t.references :promotion_rule
+ end
+
+ add_index :spree_products_promotion_rules, [:product_id], name: 'index_products_promotion_rules_on_product_id'
+ add_index :spree_products_promotion_rules, [:promotion_rule_id], name: 'index_products_promotion_rules_on_promotion_rule_id'
+
+ create_table :spree_promotion_action_line_items, force: true do |t|
+ t.references :promotion_action
+ t.references :variant
+ t.integer :quantity, default: 1
+ end
+
+ create_table :spree_promotion_actions, force: true do |t|
+ t.references :activator
+ t.integer :position
+ t.string :type
+ end
+
+ create_table :spree_promotion_rules, force: true do |t|
+ t.references :activator
+ t.references :user
+ t.references :product_group
+ t.string :type
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_promotion_rules, [:product_group_id], name: 'index_promotion_rules_on_product_group_id'
+ add_index :spree_promotion_rules, [:user_id], name: 'index_promotion_rules_on_user_id'
+
+ create_table :spree_promotion_rules_users, id: false, force: true do |t|
+ t.references :user
+ t.references :promotion_rule
+ end
+
+ add_index :spree_promotion_rules_users, [:promotion_rule_id], name: 'index_promotion_rules_users_on_promotion_rule_id'
+ add_index :spree_promotion_rules_users, [:user_id], name: 'index_promotion_rules_users_on_user_id'
+ end
+end
diff --git a/core/db/migrate/20120905145253_add_tax_rate_label.rb b/core/db/migrate/20120905145253_add_tax_rate_label.rb
new file mode 100644
index 00000000000..b264947efa4
--- /dev/null
+++ b/core/db/migrate/20120905145253_add_tax_rate_label.rb
@@ -0,0 +1,5 @@
+class AddTaxRateLabel < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_tax_rates, :name, :string
+ end
+end
diff --git a/core/db/migrate/20120905151823_add_toggle_tax_rate_display.rb b/core/db/migrate/20120905151823_add_toggle_tax_rate_display.rb
new file mode 100644
index 00000000000..0a227804f8a
--- /dev/null
+++ b/core/db/migrate/20120905151823_add_toggle_tax_rate_display.rb
@@ -0,0 +1,5 @@
+class AddToggleTaxRateDisplay < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_tax_rates, :show_rate_in_label, :boolean, default: true
+ end
+end
diff --git a/core/db/migrate/20120929093553_remove_unused_preference_columns.rb b/core/db/migrate/20120929093553_remove_unused_preference_columns.rb
new file mode 100644
index 00000000000..d4ee5d75911
--- /dev/null
+++ b/core/db/migrate/20120929093553_remove_unused_preference_columns.rb
@@ -0,0 +1,8 @@
+class RemoveUnusedPreferenceColumns < ActiveRecord::Migration[4.2]
+ def change
+ # Columns have already been removed if the application was upgraded from an older version, but must be removed from new apps.
+ remove_column :spree_preferences, :name if ApplicationRecord.connection.column_exists?(:spree_preferences, :name)
+ remove_column :spree_preferences, :owner_id if ApplicationRecord.connection.column_exists?(:spree_preferences, :owner_id)
+ remove_column :spree_preferences, :owner_type if ApplicationRecord.connection.column_exists?(:spree_preferences, :owner_type)
+ end
+end
diff --git a/core/db/migrate/20121009142519_add_lock_version_to_variant.rb b/core/db/migrate/20121009142519_add_lock_version_to_variant.rb
new file mode 100644
index 00000000000..d2ec250180d
--- /dev/null
+++ b/core/db/migrate/20121009142519_add_lock_version_to_variant.rb
@@ -0,0 +1,5 @@
+class AddLockVersionToVariant < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_variants, :lock_version, :integer, default: 0
+ end
+end
diff --git a/core/db/migrate/20121010142909_add_states_required_to_countries.rb b/core/db/migrate/20121010142909_add_states_required_to_countries.rb
new file mode 100644
index 00000000000..23444612881
--- /dev/null
+++ b/core/db/migrate/20121010142909_add_states_required_to_countries.rb
@@ -0,0 +1,5 @@
+class AddStatesRequiredToCountries < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_countries, :states_required, :boolean, default: true
+ end
+end
diff --git a/core/db/migrate/20121012071449_add_on_demand_to_product_and_variant.rb b/core/db/migrate/20121012071449_add_on_demand_to_product_and_variant.rb
new file mode 100644
index 00000000000..16511c07922
--- /dev/null
+++ b/core/db/migrate/20121012071449_add_on_demand_to_product_and_variant.rb
@@ -0,0 +1,6 @@
+class AddOnDemandToProductAndVariant < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_products, :on_demand, :boolean, default: false
+ add_column :spree_variants, :on_demand, :boolean, default: false
+ end
+end
diff --git a/core/db/migrate/20121017010007_remove_not_null_constraint_from_products_on_hand.rb b/core/db/migrate/20121017010007_remove_not_null_constraint_from_products_on_hand.rb
new file mode 100644
index 00000000000..3792e20c1bd
--- /dev/null
+++ b/core/db/migrate/20121017010007_remove_not_null_constraint_from_products_on_hand.rb
@@ -0,0 +1,11 @@
+class RemoveNotNullConstraintFromProductsOnHand < ActiveRecord::Migration[4.2]
+ def up
+ change_column :spree_products, :count_on_hand, :integer, null: true
+ change_column :spree_variants, :count_on_hand, :integer, null: true
+ end
+
+ def down
+ change_column :spree_products, :count_on_hand, :integer, null: false
+ change_column :spree_variants, :count_on_hand, :integer, null: false
+ end
+end
diff --git a/core/db/migrate/20121031162139_split_prices_from_variants.rb b/core/db/migrate/20121031162139_split_prices_from_variants.rb
new file mode 100644
index 00000000000..b4c10407ae1
--- /dev/null
+++ b/core/db/migrate/20121031162139_split_prices_from_variants.rb
@@ -0,0 +1,31 @@
+class SplitPricesFromVariants < ActiveRecord::Migration[4.2]
+ def up
+ create_table :spree_prices do |t|
+ t.integer :variant_id, null: false
+ t.decimal :amount, precision: 8, scale: 2, null: false
+ t.string :currency
+ end
+
+ Spree::Variant.all.each do |variant|
+ Spree::Price.create!(
+ variant_id: variant.id,
+ amount: variant[:price],
+ currency: Spree::Config[:currency]
+ )
+ end
+
+ remove_column :spree_variants, :price
+ end
+
+ def down
+ prices = ApplicationRecord.connection.execute("select variant_id, amount from spree_prices")
+ add_column :spree_variants, :price, :decimal, after: :sku, scale: 2, precision: 8
+
+ prices.each do |price|
+ ApplicationRecord.connection.execute("update spree_variants set price = #{price['amount']} where id = #{price['variant_id']}")
+ end
+
+ change_column :spree_variants, :price, :decimal, after: :sku, scale: 2, precision: 8, null: false
+ drop_table :spree_prices
+ end
+end
diff --git a/core/db/migrate/20121107003422_remove_not_null_from_spree_prices_amount.rb b/core/db/migrate/20121107003422_remove_not_null_from_spree_prices_amount.rb
new file mode 100644
index 00000000000..6770570fbcd
--- /dev/null
+++ b/core/db/migrate/20121107003422_remove_not_null_from_spree_prices_amount.rb
@@ -0,0 +1,9 @@
+class RemoveNotNullFromSpreePricesAmount < ActiveRecord::Migration[4.2]
+ def up
+ change_column :spree_prices, :amount, :decimal, precision: 8, scale: 2, null: true
+ end
+
+ def down
+ change_column :spree_prices, :amount, :decimal, precision: 8, scale: 2, null: false
+ end
+end
diff --git a/core/db/migrate/20121107184631_add_currency_to_line_items.rb b/core/db/migrate/20121107184631_add_currency_to_line_items.rb
new file mode 100644
index 00000000000..070861da18e
--- /dev/null
+++ b/core/db/migrate/20121107184631_add_currency_to_line_items.rb
@@ -0,0 +1,5 @@
+class AddCurrencyToLineItems < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_line_items, :currency, :string
+ end
+end
diff --git a/core/db/migrate/20121107194006_add_currency_to_orders.rb b/core/db/migrate/20121107194006_add_currency_to_orders.rb
new file mode 100644
index 00000000000..858542f18a7
--- /dev/null
+++ b/core/db/migrate/20121107194006_add_currency_to_orders.rb
@@ -0,0 +1,5 @@
+class AddCurrencyToOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :currency, :string
+ end
+end
diff --git a/core/db/migrate/20121109173623_add_cost_currency_to_variants.rb b/core/db/migrate/20121109173623_add_cost_currency_to_variants.rb
new file mode 100644
index 00000000000..479f8c308e3
--- /dev/null
+++ b/core/db/migrate/20121109173623_add_cost_currency_to_variants.rb
@@ -0,0 +1,5 @@
+class AddCostCurrencyToVariants < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_variants, :cost_currency, :string, after: :cost_price
+ end
+end
diff --git a/core/db/migrate/20121111231553_remove_display_on_from_payment_methods.rb b/core/db/migrate/20121111231553_remove_display_on_from_payment_methods.rb
new file mode 100644
index 00000000000..795da35415e
--- /dev/null
+++ b/core/db/migrate/20121111231553_remove_display_on_from_payment_methods.rb
@@ -0,0 +1,5 @@
+class RemoveDisplayOnFromPaymentMethods < ActiveRecord::Migration[4.2]
+ def up
+ remove_column :spree_payment_methods, :display_on
+ end
+end
diff --git a/core/db/migrate/20121124203911_add_position_to_taxonomies.rb b/core/db/migrate/20121124203911_add_position_to_taxonomies.rb
new file mode 100644
index 00000000000..4be2df595d6
--- /dev/null
+++ b/core/db/migrate/20121124203911_add_position_to_taxonomies.rb
@@ -0,0 +1,5 @@
+class AddPositionToTaxonomies < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_taxonomies, :position, :integer, default: 0
+ end
+end
diff --git a/core/db/migrate/20121126040517_add_last_ip_to_spree_orders.rb b/core/db/migrate/20121126040517_add_last_ip_to_spree_orders.rb
new file mode 100644
index 00000000000..1c8996eb3c3
--- /dev/null
+++ b/core/db/migrate/20121126040517_add_last_ip_to_spree_orders.rb
@@ -0,0 +1,5 @@
+class AddLastIpToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :last_ip_address, :string
+ end
+end
diff --git a/core/db/migrate/20121213162028_add_state_to_spree_adjustments.rb b/core/db/migrate/20121213162028_add_state_to_spree_adjustments.rb
new file mode 100644
index 00000000000..06c25d96e90
--- /dev/null
+++ b/core/db/migrate/20121213162028_add_state_to_spree_adjustments.rb
@@ -0,0 +1,6 @@
+class AddStateToSpreeAdjustments < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_adjustments, :state, :string
+ remove_column :spree_adjustments, :locked
+ end
+end
diff --git a/core/db/migrate/20130114053446_add_display_on_to_spree_payment_methods.rb b/core/db/migrate/20130114053446_add_display_on_to_spree_payment_methods.rb
new file mode 100644
index 00000000000..5b59d1d8ea1
--- /dev/null
+++ b/core/db/migrate/20130114053446_add_display_on_to_spree_payment_methods.rb
@@ -0,0 +1,9 @@
+class AddDisplayOnToSpreePaymentMethods < ActiveRecord::Migration[4.2]
+ def self.up
+ add_column :spree_payment_methods, :display_on, :string
+ end
+
+ def self.down
+ remove_column :spree_payment_methods, :display_on
+ end
+end
diff --git a/core/db/migrate/20130120201805_add_position_to_product_properties.spree.rb b/core/db/migrate/20130120201805_add_position_to_product_properties.spree.rb
new file mode 100644
index 00000000000..72a76fabe88
--- /dev/null
+++ b/core/db/migrate/20130120201805_add_position_to_product_properties.spree.rb
@@ -0,0 +1,6 @@
+class AddPositionToProductProperties < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_product_properties, :position, :integer, default: 0
+ end
+end
+
diff --git a/core/db/migrate/20130203232234_add_identifier_to_spree_payments.rb b/core/db/migrate/20130203232234_add_identifier_to_spree_payments.rb
new file mode 100644
index 00000000000..8eb9d45df12
--- /dev/null
+++ b/core/db/migrate/20130203232234_add_identifier_to_spree_payments.rb
@@ -0,0 +1,5 @@
+class AddIdentifierToSpreePayments < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_payments, :identifier, :string
+ end
+end
diff --git a/core/db/migrate/20130207155350_add_order_id_index_to_payments.rb b/core/db/migrate/20130207155350_add_order_id_index_to_payments.rb
new file mode 100644
index 00000000000..90c7fe7f365
--- /dev/null
+++ b/core/db/migrate/20130207155350_add_order_id_index_to_payments.rb
@@ -0,0 +1,9 @@
+class AddOrderIdIndexToPayments < ActiveRecord::Migration[4.2]
+ def self.up
+ add_index :spree_payments, :order_id
+ end
+
+ def self.down
+ remove_index :spree_payments, :order_id
+ end
+end
diff --git a/core/db/migrate/20130208032954_add_primary_to_spree_products_taxons.rb b/core/db/migrate/20130208032954_add_primary_to_spree_products_taxons.rb
new file mode 100644
index 00000000000..832973e5d7e
--- /dev/null
+++ b/core/db/migrate/20130208032954_add_primary_to_spree_products_taxons.rb
@@ -0,0 +1,5 @@
+class AddPrimaryToSpreeProductsTaxons < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_products_taxons, :id, :primary_key
+ end
+end
diff --git a/core/db/migrate/20130211190146_create_spree_stock_items.rb b/core/db/migrate/20130211190146_create_spree_stock_items.rb
new file mode 100644
index 00000000000..835b5324e54
--- /dev/null
+++ b/core/db/migrate/20130211190146_create_spree_stock_items.rb
@@ -0,0 +1,14 @@
+class CreateSpreeStockItems < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_stock_items do |t|
+ t.belongs_to :stock_location
+ t.belongs_to :variant
+ t.integer :count_on_hand, null: false, default: 0
+ t.integer :lock_version
+
+ t.timestamps null: false, precision: 6
+ end
+ add_index :spree_stock_items, :stock_location_id
+ add_index :spree_stock_items, [:stock_location_id, :variant_id], name: 'stock_item_by_loc_and_var_id'
+ end
+end
diff --git a/core/db/migrate/20130211191120_create_spree_stock_locations.rb b/core/db/migrate/20130211191120_create_spree_stock_locations.rb
new file mode 100644
index 00000000000..725ed24f66c
--- /dev/null
+++ b/core/db/migrate/20130211191120_create_spree_stock_locations.rb
@@ -0,0 +1,11 @@
+class CreateSpreeStockLocations < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_stock_locations do |t|
+ t.string :name
+ t.belongs_to :address
+
+ t.timestamps null: false, precision: 6
+ end
+ add_index :spree_stock_locations, :address_id
+ end
+end
diff --git a/core/db/migrate/20130213191427_create_default_stock.rb b/core/db/migrate/20130213191427_create_default_stock.rb
new file mode 100644
index 00000000000..ce3ac95bb14
--- /dev/null
+++ b/core/db/migrate/20130213191427_create_default_stock.rb
@@ -0,0 +1,33 @@
+class CreateDefaultStock < ActiveRecord::Migration[4.2]
+ def up
+ unless column_exists? :spree_stock_locations, :default
+ add_column :spree_stock_locations, :default, :boolean, null: false, default: false
+ end
+
+ Spree::StockLocation.skip_callback(:create, :after, :create_stock_items)
+ Spree::StockLocation.skip_callback(:save, :after, :ensure_one_default)
+ location = Spree::StockLocation.new(name: 'default')
+ location.save(validate: false)
+
+ Spree::Variant.find_each do |variant|
+ stock_item = Spree::StockItem.unscoped.build(stock_location: location, variant: variant)
+ stock_item.send(:count_on_hand=, variant.count_on_hand)
+ # Avoid running default_scope defined by acts_as_paranoid, related to #3805,
+ # validations would run a query with a delete_at column that might not be present yet
+ stock_item.save! validate: false
+ end
+
+ remove_column :spree_variants, :count_on_hand
+ end
+
+ def down
+ add_column :spree_variants, :count_on_hand, :integer
+
+ Spree::StockItem.find_each do |stock_item|
+ stock_item.variant.update_column :count_on_hand, stock_item.count_on_hand
+ end
+
+ Spree::StockLocation.delete_all
+ Spree::StockItem.delete_all
+ end
+end
diff --git a/core/db/migrate/20130222032153_add_order_id_index_to_shipments.rb b/core/db/migrate/20130222032153_add_order_id_index_to_shipments.rb
new file mode 100644
index 00000000000..5b21223cfd4
--- /dev/null
+++ b/core/db/migrate/20130222032153_add_order_id_index_to_shipments.rb
@@ -0,0 +1,5 @@
+class AddOrderIdIndexToShipments < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_shipments, :order_id
+ end
+end
diff --git a/core/db/migrate/20130226032817_change_meta_description_on_spree_products_to_text.rb b/core/db/migrate/20130226032817_change_meta_description_on_spree_products_to_text.rb
new file mode 100644
index 00000000000..ee1c5d213ac
--- /dev/null
+++ b/core/db/migrate/20130226032817_change_meta_description_on_spree_products_to_text.rb
@@ -0,0 +1,5 @@
+class ChangeMetaDescriptionOnSpreeProductsToText < ActiveRecord::Migration[4.2]
+ def change
+ change_column :spree_products, :meta_description, :text, limit: nil
+ end
+end
diff --git a/core/db/migrate/20130226191231_add_stock_location_id_to_spree_shipments.rb b/core/db/migrate/20130226191231_add_stock_location_id_to_spree_shipments.rb
new file mode 100644
index 00000000000..e576eb334a7
--- /dev/null
+++ b/core/db/migrate/20130226191231_add_stock_location_id_to_spree_shipments.rb
@@ -0,0 +1,5 @@
+class AddStockLocationIdToSpreeShipments < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_shipments, :stock_location_id, :integer
+ end
+end
diff --git a/core/db/migrate/20130227143905_add_pending_to_inventory_unit.rb b/core/db/migrate/20130227143905_add_pending_to_inventory_unit.rb
new file mode 100644
index 00000000000..d65d80bc876
--- /dev/null
+++ b/core/db/migrate/20130227143905_add_pending_to_inventory_unit.rb
@@ -0,0 +1,6 @@
+class AddPendingToInventoryUnit < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_inventory_units, :pending, :boolean, default: true
+ Spree::InventoryUnit.update_all(pending: false)
+ end
+end
diff --git a/core/db/migrate/20130228164411_remove_on_demand_from_product_and_variant.rb b/core/db/migrate/20130228164411_remove_on_demand_from_product_and_variant.rb
new file mode 100644
index 00000000000..da92fe176f3
--- /dev/null
+++ b/core/db/migrate/20130228164411_remove_on_demand_from_product_and_variant.rb
@@ -0,0 +1,6 @@
+class RemoveOnDemandFromProductAndVariant < ActiveRecord::Migration[4.2]
+ def change
+ remove_column :spree_products, :on_demand
+ remove_column :spree_variants, :on_demand
+ end
+end
diff --git a/core/db/migrate/20130228210442_create_shipping_method_zone.rb b/core/db/migrate/20130228210442_create_shipping_method_zone.rb
new file mode 100644
index 00000000000..ee328e81de3
--- /dev/null
+++ b/core/db/migrate/20130228210442_create_shipping_method_zone.rb
@@ -0,0 +1,21 @@
+class CreateShippingMethodZone < ActiveRecord::Migration[4.2]
+ class ShippingMethodZone < ApplicationRecord
+ self.table_name = 'shipping_methods_zones'
+ end
+ def up
+ create_table :shipping_methods_zones, id: false do |t|
+ t.integer :shipping_method_id
+ t.integer :zone_id
+ end
+ Spree::ShippingMethod.all.each do |sm|
+ ShippingMethodZone.create!(zone_id: sm.zone_id, shipping_method_id: sm.id)
+ end
+
+ remove_column :spree_shipping_methods, :zone_id
+ end
+
+ def down
+ drop_table :shipping_methods_zones
+ add_column :spree_shipping_methods, :zone_id, :integer
+ end
+end
diff --git a/core/db/migrate/20130301162745_remove_shipping_category_id_from_shipping_method.rb b/core/db/migrate/20130301162745_remove_shipping_category_id_from_shipping_method.rb
new file mode 100644
index 00000000000..abe12a22f8f
--- /dev/null
+++ b/core/db/migrate/20130301162745_remove_shipping_category_id_from_shipping_method.rb
@@ -0,0 +1,5 @@
+class RemoveShippingCategoryIdFromShippingMethod < ActiveRecord::Migration[4.2]
+ def change
+ remove_column :spree_shipping_methods, :shipping_category_id
+ end
+end
diff --git a/core/db/migrate/20130301162924_create_shipping_method_categories.rb b/core/db/migrate/20130301162924_create_shipping_method_categories.rb
new file mode 100644
index 00000000000..f97dbadfbff
--- /dev/null
+++ b/core/db/migrate/20130301162924_create_shipping_method_categories.rb
@@ -0,0 +1,13 @@
+class CreateShippingMethodCategories < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_shipping_method_categories do |t|
+ t.integer :shipping_method_id, null: false
+ t.integer :shipping_category_id, null: false
+
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_shipping_method_categories, :shipping_method_id
+ add_index :spree_shipping_method_categories, :shipping_category_id
+ end
+end
diff --git a/core/db/migrate/20130301205200_add_tracking_url_to_spree_shipping_methods.rb b/core/db/migrate/20130301205200_add_tracking_url_to_spree_shipping_methods.rb
new file mode 100644
index 00000000000..d91c63c7b55
--- /dev/null
+++ b/core/db/migrate/20130301205200_add_tracking_url_to_spree_shipping_methods.rb
@@ -0,0 +1,5 @@
+class AddTrackingUrlToSpreeShippingMethods < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_shipping_methods, :tracking_url, :string
+ end
+end
diff --git a/core/db/migrate/20130304162240_create_spree_shipping_rates.rb b/core/db/migrate/20130304162240_create_spree_shipping_rates.rb
new file mode 100644
index 00000000000..a6f4fc490bf
--- /dev/null
+++ b/core/db/migrate/20130304162240_create_spree_shipping_rates.rb
@@ -0,0 +1,24 @@
+class CreateSpreeShippingRates < ActiveRecord::Migration[4.2]
+ def up
+ create_table :spree_shipping_rates do |t|
+ t.belongs_to :shipment
+ t.belongs_to :shipping_method
+ t.boolean :selected, default: false
+ t.decimal :cost, precision: 8, scale: 2
+ t.timestamps null: false, precision: 6
+ end
+ add_index(:spree_shipping_rates, [:shipment_id, :shipping_method_id],
+ name: 'spree_shipping_rates_join_index',
+ unique: true)
+
+ # Spree::Shipment.all.each do |shipment|
+ # shipping_method = Spree::ShippingMethod.find(shipment.shipment_method_id)
+ # shipment.add_shipping_method(shipping_method, true)
+ # end
+ end
+
+ def down
+ # add_column :spree_shipments, :shipping_method_id, :integer
+ drop_table :spree_shipping_rates
+ end
+end
diff --git a/core/db/migrate/20130304192936_remove_category_match_attributes_from_shipping_method.rb b/core/db/migrate/20130304192936_remove_category_match_attributes_from_shipping_method.rb
new file mode 100644
index 00000000000..09217e00c44
--- /dev/null
+++ b/core/db/migrate/20130304192936_remove_category_match_attributes_from_shipping_method.rb
@@ -0,0 +1,7 @@
+class RemoveCategoryMatchAttributesFromShippingMethod < ActiveRecord::Migration[4.2]
+ def change
+ remove_column :spree_shipping_methods, :match_none
+ remove_column :spree_shipping_methods, :match_one
+ remove_column :spree_shipping_methods, :match_all
+ end
+end
diff --git a/core/db/migrate/20130305143310_create_stock_movements.rb b/core/db/migrate/20130305143310_create_stock_movements.rb
new file mode 100644
index 00000000000..b25d0d33dfc
--- /dev/null
+++ b/core/db/migrate/20130305143310_create_stock_movements.rb
@@ -0,0 +1,12 @@
+class CreateStockMovements < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_stock_movements do |t|
+ t.belongs_to :stock_item
+ t.integer :quantity
+ t.string :action
+
+ t.timestamps null: false, precision: 6
+ end
+ add_index :spree_stock_movements, :stock_item_id
+ end
+end
diff --git a/core/db/migrate/20130306181701_add_address_fields_to_stock_location.rb b/core/db/migrate/20130306181701_add_address_fields_to_stock_location.rb
new file mode 100644
index 00000000000..68dbb09e5b2
--- /dev/null
+++ b/core/db/migrate/20130306181701_add_address_fields_to_stock_location.rb
@@ -0,0 +1,22 @@
+class AddAddressFieldsToStockLocation < ActiveRecord::Migration[4.2]
+ def change
+ remove_column :spree_stock_locations, :address_id
+
+ add_column :spree_stock_locations, :address1, :string
+ add_column :spree_stock_locations, :address2, :string
+ add_column :spree_stock_locations, :city, :string
+ add_column :spree_stock_locations, :state_id, :integer
+ add_column :spree_stock_locations, :state_name, :string
+ add_column :spree_stock_locations, :country_id, :integer
+ add_column :spree_stock_locations, :zipcode, :string
+ add_column :spree_stock_locations, :phone, :string
+
+
+ usa = Spree::Country.where(iso: 'US').first
+ # In case USA isn't found.
+ # See #3115
+ country = usa || Spree::Country.first
+ Spree::Country.reset_column_information
+ Spree::StockLocation.update_all(country_id: country)
+ end
+end
diff --git a/core/db/migrate/20130306191917_add_active_field_to_stock_locations.rb b/core/db/migrate/20130306191917_add_active_field_to_stock_locations.rb
new file mode 100644
index 00000000000..0f365038430
--- /dev/null
+++ b/core/db/migrate/20130306191917_add_active_field_to_stock_locations.rb
@@ -0,0 +1,5 @@
+class AddActiveFieldToStockLocations < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_stock_locations, :active, :boolean, default: true
+ end
+end
diff --git a/core/db/migrate/20130306195650_add_backorderable_to_stock_item.rb b/core/db/migrate/20130306195650_add_backorderable_to_stock_item.rb
new file mode 100644
index 00000000000..71a96aa281f
--- /dev/null
+++ b/core/db/migrate/20130306195650_add_backorderable_to_stock_item.rb
@@ -0,0 +1,5 @@
+class AddBackorderableToStockItem < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_stock_items, :backorderable, :boolean, default: true
+ end
+end
diff --git a/core/db/migrate/20130307161754_add_default_quantity_to_stock_movement.rb b/core/db/migrate/20130307161754_add_default_quantity_to_stock_movement.rb
new file mode 100644
index 00000000000..5a423e6d54d
--- /dev/null
+++ b/core/db/migrate/20130307161754_add_default_quantity_to_stock_movement.rb
@@ -0,0 +1,5 @@
+class AddDefaultQuantityToStockMovement < ActiveRecord::Migration[4.2]
+ def change
+ change_column :spree_stock_movements, :quantity, :integer, default: 0
+ end
+end
diff --git a/core/db/migrate/20130318151756_add_source_and_destination_to_stock_movements.rb b/core/db/migrate/20130318151756_add_source_and_destination_to_stock_movements.rb
new file mode 100644
index 00000000000..f36569a10ef
--- /dev/null
+++ b/core/db/migrate/20130318151756_add_source_and_destination_to_stock_movements.rb
@@ -0,0 +1,8 @@
+class AddSourceAndDestinationToStockMovements < ActiveRecord::Migration[4.2]
+ def change
+ change_table :spree_stock_movements do |t|
+ t.references :source, polymorphic: true
+ t.references :destination, polymorphic: true
+ end
+ end
+end
diff --git a/core/db/migrate/20130319062004_change_orders_total_precision.rb b/core/db/migrate/20130319062004_change_orders_total_precision.rb
new file mode 100644
index 00000000000..4a60136af89
--- /dev/null
+++ b/core/db/migrate/20130319062004_change_orders_total_precision.rb
@@ -0,0 +1,8 @@
+class ChangeOrdersTotalPrecision < ActiveRecord::Migration[4.2]
+ def change
+ change_column :spree_orders, :item_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ change_column :spree_orders, :total, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ change_column :spree_orders, :adjustment_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ change_column :spree_orders, :payment_total, :decimal, precision: 10, scale: 2, default: 0.0
+ end
+end
diff --git a/core/db/migrate/20130319063911_change_spree_payments_amount_precision.rb b/core/db/migrate/20130319063911_change_spree_payments_amount_precision.rb
new file mode 100644
index 00000000000..17c91e53d90
--- /dev/null
+++ b/core/db/migrate/20130319063911_change_spree_payments_amount_precision.rb
@@ -0,0 +1,7 @@
+class ChangeSpreePaymentsAmountPrecision < ActiveRecord::Migration[4.2]
+ def change
+
+ change_column :spree_payments, :amount, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+
+ end
+end
diff --git a/core/db/migrate/20130319064308_change_spree_return_authorization_amount_precision.rb b/core/db/migrate/20130319064308_change_spree_return_authorization_amount_precision.rb
new file mode 100644
index 00000000000..100043a2650
--- /dev/null
+++ b/core/db/migrate/20130319064308_change_spree_return_authorization_amount_precision.rb
@@ -0,0 +1,7 @@
+class ChangeSpreeReturnAuthorizationAmountPrecision < ActiveRecord::Migration[4.2]
+ def change
+
+ change_column :spree_return_authorizations, :amount, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+
+ end
+end
diff --git a/core/db/migrate/20130319082943_change_adjustments_amount_precision.rb b/core/db/migrate/20130319082943_change_adjustments_amount_precision.rb
new file mode 100644
index 00000000000..2fd17232066
--- /dev/null
+++ b/core/db/migrate/20130319082943_change_adjustments_amount_precision.rb
@@ -0,0 +1,7 @@
+class ChangeAdjustmentsAmountPrecision < ActiveRecord::Migration[4.2]
+ def change
+
+ change_column :spree_adjustments, :amount, :decimal, precision: 10, scale: 2
+
+ end
+end
diff --git a/core/db/migrate/20130319183250_add_originator_to_stock_movement.rb b/core/db/migrate/20130319183250_add_originator_to_stock_movement.rb
new file mode 100644
index 00000000000..83f8973d3ba
--- /dev/null
+++ b/core/db/migrate/20130319183250_add_originator_to_stock_movement.rb
@@ -0,0 +1,7 @@
+class AddOriginatorToStockMovement < ActiveRecord::Migration[4.2]
+ def change
+ change_table :spree_stock_movements do |t|
+ t.references :originator, polymorphic: true
+ end
+ end
+end
diff --git a/core/db/migrate/20130319190507_drop_source_and_destination_from_stock_movement.rb b/core/db/migrate/20130319190507_drop_source_and_destination_from_stock_movement.rb
new file mode 100644
index 00000000000..a2c181cb4d3
--- /dev/null
+++ b/core/db/migrate/20130319190507_drop_source_and_destination_from_stock_movement.rb
@@ -0,0 +1,15 @@
+class DropSourceAndDestinationFromStockMovement < ActiveRecord::Migration[4.2]
+ def up
+ change_table :spree_stock_movements do |t|
+ t.remove_references :source, polymorphic: true
+ t.remove_references :destination, polymorphic: true
+ end
+ end
+
+ def down
+ change_table :spree_stock_movements do |t|
+ t.references :source, polymorphic: true
+ t.references :destination, polymorphic: true
+ end
+ end
+end
diff --git a/core/db/migrate/20130325163316_migrate_inventory_unit_sold_to_on_hand.rb b/core/db/migrate/20130325163316_migrate_inventory_unit_sold_to_on_hand.rb
new file mode 100644
index 00000000000..a4f26084411
--- /dev/null
+++ b/core/db/migrate/20130325163316_migrate_inventory_unit_sold_to_on_hand.rb
@@ -0,0 +1,9 @@
+class MigrateInventoryUnitSoldToOnHand < ActiveRecord::Migration[4.2]
+ def up
+ Spree::InventoryUnit.where(state: 'sold').update_all(state: 'on_hand')
+ end
+
+ def down
+ Spree::InventoryUnit.where(state: 'on_hand').update_all(state: 'sold')
+ end
+end
diff --git a/core/db/migrate/20130326175857_add_stock_location_to_rma.rb b/core/db/migrate/20130326175857_add_stock_location_to_rma.rb
new file mode 100644
index 00000000000..640cc472f95
--- /dev/null
+++ b/core/db/migrate/20130326175857_add_stock_location_to_rma.rb
@@ -0,0 +1,5 @@
+class AddStockLocationToRma < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_return_authorizations, :stock_location_id, :integer
+ end
+end
diff --git a/core/db/migrate/20130328130308_update_shipment_state_for_canceled_orders.rb b/core/db/migrate/20130328130308_update_shipment_state_for_canceled_orders.rb
new file mode 100644
index 00000000000..ea5a2f4610c
--- /dev/null
+++ b/core/db/migrate/20130328130308_update_shipment_state_for_canceled_orders.rb
@@ -0,0 +1,15 @@
+class UpdateShipmentStateForCanceledOrders < ActiveRecord::Migration[4.2]
+ def up
+ shipments = Spree::Shipment.joins(:order).
+ where("spree_orders.state = 'canceled'")
+ case Spree::Shipment.connection.adapter_name
+ when "SQLite3"
+ shipments.update_all("state = 'cancelled'")
+ when "MySQL" || "PostgreSQL"
+ shipments.update_all("spree_shipments.state = 'cancelled'")
+ end
+ end
+
+ def down
+ end
+end
diff --git a/core/db/migrate/20130328195253_add_seo_metas_to_taxons.rb b/core/db/migrate/20130328195253_add_seo_metas_to_taxons.rb
new file mode 100644
index 00000000000..0cc3ed3841b
--- /dev/null
+++ b/core/db/migrate/20130328195253_add_seo_metas_to_taxons.rb
@@ -0,0 +1,9 @@
+class AddSeoMetasToTaxons < ActiveRecord::Migration[4.2]
+ def change
+ change_table :spree_taxons do |t|
+ t.string :meta_title
+ t.string :meta_description
+ t.string :meta_keywords
+ end
+ end
+end
diff --git a/core/db/migrate/20130329134939_remove_stock_item_and_variant_lock.rb b/core/db/migrate/20130329134939_remove_stock_item_and_variant_lock.rb
new file mode 100644
index 00000000000..7b388fd8192
--- /dev/null
+++ b/core/db/migrate/20130329134939_remove_stock_item_and_variant_lock.rb
@@ -0,0 +1,14 @@
+class RemoveStockItemAndVariantLock < ActiveRecord::Migration[4.2]
+ def up
+ # we are moving to pessimistic locking on stock_items
+ remove_column :spree_stock_items, :lock_version
+
+ # variants no longer manage their count_on_hand so we are removing their lock
+ remove_column :spree_variants, :lock_version
+ end
+
+ def down
+ add_column :spree_stock_items, :lock_version, :integer
+ add_column :spree_variants, :lock_version, :integer
+ end
+end
diff --git a/core/db/migrate/20130413230529_add_name_to_spree_credit_cards.rb b/core/db/migrate/20130413230529_add_name_to_spree_credit_cards.rb
new file mode 100644
index 00000000000..854e63baf53
--- /dev/null
+++ b/core/db/migrate/20130413230529_add_name_to_spree_credit_cards.rb
@@ -0,0 +1,5 @@
+class AddNameToSpreeCreditCards < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_credit_cards, :name, :string
+ end
+end
diff --git a/core/db/migrate/20130414000512_update_name_fields_on_spree_credit_cards.rb b/core/db/migrate/20130414000512_update_name_fields_on_spree_credit_cards.rb
new file mode 100644
index 00000000000..d5ca3c69e3a
--- /dev/null
+++ b/core/db/migrate/20130414000512_update_name_fields_on_spree_credit_cards.rb
@@ -0,0 +1,13 @@
+class UpdateNameFieldsOnSpreeCreditCards < ActiveRecord::Migration[4.2]
+ def up
+ if ApplicationRecord.connection.adapter_name.downcase.include? "mysql"
+ execute "UPDATE spree_credit_cards SET name = CONCAT(first_name, ' ', last_name)"
+ else
+ execute "UPDATE spree_credit_cards SET name = first_name || ' ' || last_name"
+ end
+ end
+
+ def down
+ execute "UPDATE spree_credit_cards SET name = NULL"
+ end
+end
diff --git a/core/db/migrate/20130417120034_add_index_to_source_columns_on_adjustments.rb b/core/db/migrate/20130417120034_add_index_to_source_columns_on_adjustments.rb
new file mode 100644
index 00000000000..608b7303b77
--- /dev/null
+++ b/core/db/migrate/20130417120034_add_index_to_source_columns_on_adjustments.rb
@@ -0,0 +1,5 @@
+class AddIndexToSourceColumnsOnAdjustments < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_adjustments, [:source_type, :source_id]
+ end
+end
diff --git a/core/db/migrate/20130417120035_update_adjustment_states.rb b/core/db/migrate/20130417120035_update_adjustment_states.rb
new file mode 100644
index 00000000000..617726e6dc7
--- /dev/null
+++ b/core/db/migrate/20130417120035_update_adjustment_states.rb
@@ -0,0 +1,16 @@
+class UpdateAdjustmentStates < ActiveRecord::Migration[4.2]
+ 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/core/db/migrate/20130417123427_add_shipping_rates_to_shipments.rb b/core/db/migrate/20130417123427_add_shipping_rates_to_shipments.rb
new file mode 100644
index 00000000000..4ada655df82
--- /dev/null
+++ b/core/db/migrate/20130417123427_add_shipping_rates_to_shipments.rb
@@ -0,0 +1,15 @@
+class AddShippingRatesToShipments < ActiveRecord::Migration[4.2]
+ def up
+ Spree::Shipment.find_each do |shipment|
+ shipment.shipping_rates.create(shipping_method_id: shipment.shipping_method_id,
+ cost: shipment.cost,
+ selected: true)
+ end
+
+ remove_column :spree_shipments, :shipping_method_id
+ end
+
+ def down
+ add_column :spree_shipments, :shipping_method_id, :integer
+ end
+end
diff --git a/core/db/migrate/20130418125341_create_spree_stock_transfers.rb b/core/db/migrate/20130418125341_create_spree_stock_transfers.rb
new file mode 100644
index 00000000000..801347678ec
--- /dev/null
+++ b/core/db/migrate/20130418125341_create_spree_stock_transfers.rb
@@ -0,0 +1,14 @@
+class CreateSpreeStockTransfers < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_stock_transfers do |t|
+ t.string :type
+ t.string :reference_number
+ t.integer :source_location_id
+ t.integer :destination_location_id
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_stock_transfers, :source_location_id
+ add_index :spree_stock_transfers, :destination_location_id
+ end
+end
diff --git a/core/db/migrate/20130423110707_drop_products_count_on_hand.rb b/core/db/migrate/20130423110707_drop_products_count_on_hand.rb
new file mode 100644
index 00000000000..692b6c58fd2
--- /dev/null
+++ b/core/db/migrate/20130423110707_drop_products_count_on_hand.rb
@@ -0,0 +1,5 @@
+class DropProductsCountOnHand < ActiveRecord::Migration[4.2]
+ def up
+ remove_column :spree_products, :count_on_hand
+ end
+end
diff --git a/core/db/migrate/20130423223847_set_default_shipping_rate_cost.rb b/core/db/migrate/20130423223847_set_default_shipping_rate_cost.rb
new file mode 100644
index 00000000000..faf88a92e52
--- /dev/null
+++ b/core/db/migrate/20130423223847_set_default_shipping_rate_cost.rb
@@ -0,0 +1,5 @@
+class SetDefaultShippingRateCost < ActiveRecord::Migration[4.2]
+ def change
+ change_column :spree_shipping_rates, :cost, :decimal, default: 0, precision: 8, scale: 2
+ end
+end
diff --git a/core/db/migrate/20130509115210_add_number_to_stock_transfer.rb b/core/db/migrate/20130509115210_add_number_to_stock_transfer.rb
new file mode 100644
index 00000000000..8373a3cf169
--- /dev/null
+++ b/core/db/migrate/20130509115210_add_number_to_stock_transfer.rb
@@ -0,0 +1,23 @@
+class AddNumberToStockTransfer < ActiveRecord::Migration[4.2]
+ def up
+ remove_index :spree_stock_transfers, :source_location_id
+ remove_index :spree_stock_transfers, :destination_location_id
+
+ rename_column :spree_stock_transfers, :reference_number, :reference
+ add_column :spree_stock_transfers, :number, :string
+
+ Spree::StockTransfer.find_each do |transfer|
+ transfer.send(:generate_stock_transfer_number)
+ transfer.save!
+ end
+
+ add_index :spree_stock_transfers, :number
+ add_index :spree_stock_transfers, :source_location_id
+ add_index :spree_stock_transfers, :destination_location_id
+ end
+
+ def down
+ rename_column :spree_stock_transfers, :reference, :reference_number
+ remove_column :spree_stock_transfers, :number, :string
+ end
+end
diff --git a/core/db/migrate/20130514151929_add_sku_index_to_spree_variants.rb b/core/db/migrate/20130514151929_add_sku_index_to_spree_variants.rb
new file mode 100644
index 00000000000..17d88713791
--- /dev/null
+++ b/core/db/migrate/20130514151929_add_sku_index_to_spree_variants.rb
@@ -0,0 +1,5 @@
+class AddSkuIndexToSpreeVariants < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_variants, :sku
+ end
+end
diff --git a/core/db/migrate/20130515180736_add_backorderable_default_to_spree_stock_location.rb b/core/db/migrate/20130515180736_add_backorderable_default_to_spree_stock_location.rb
new file mode 100644
index 00000000000..6c42c8cbcb8
--- /dev/null
+++ b/core/db/migrate/20130515180736_add_backorderable_default_to_spree_stock_location.rb
@@ -0,0 +1,5 @@
+class AddBackorderableDefaultToSpreeStockLocation < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_stock_locations, :backorderable_default, :boolean, default: true
+ end
+end
diff --git a/core/db/migrate/20130516151222_add_propage_all_variants_to_spree_stock_location.rb b/core/db/migrate/20130516151222_add_propage_all_variants_to_spree_stock_location.rb
new file mode 100644
index 00000000000..72780bd830b
--- /dev/null
+++ b/core/db/migrate/20130516151222_add_propage_all_variants_to_spree_stock_location.rb
@@ -0,0 +1,5 @@
+class AddPropageAllVariantsToSpreeStockLocation < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_stock_locations, :propagate_all_variants, :boolean, default: true
+ end
+end
diff --git a/core/db/migrate/20130611054351_rename_shipping_methods_zones_to_spree_shipping_methods_zones.rb b/core/db/migrate/20130611054351_rename_shipping_methods_zones_to_spree_shipping_methods_zones.rb
new file mode 100644
index 00000000000..f1d405e416b
--- /dev/null
+++ b/core/db/migrate/20130611054351_rename_shipping_methods_zones_to_spree_shipping_methods_zones.rb
@@ -0,0 +1,5 @@
+class RenameShippingMethodsZonesToSpreeShippingMethodsZones < ActiveRecord::Migration[4.2]
+ def change
+ rename_table :shipping_methods_zones, :spree_shipping_methods_zones
+ end
+end
diff --git a/core/db/migrate/20130611185927_add_user_id_index_to_spree_orders.rb b/core/db/migrate/20130611185927_add_user_id_index_to_spree_orders.rb
new file mode 100644
index 00000000000..46e760f04d1
--- /dev/null
+++ b/core/db/migrate/20130611185927_add_user_id_index_to_spree_orders.rb
@@ -0,0 +1,5 @@
+class AddUserIdIndexToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_orders, :user_id
+ end
+end
diff --git a/core/db/migrate/20130618041418_add_updated_at_to_spree_countries.rb b/core/db/migrate/20130618041418_add_updated_at_to_spree_countries.rb
new file mode 100644
index 00000000000..5aab89dbc05
--- /dev/null
+++ b/core/db/migrate/20130618041418_add_updated_at_to_spree_countries.rb
@@ -0,0 +1,9 @@
+class AddUpdatedAtToSpreeCountries < ActiveRecord::Migration[4.2]
+ def up
+ add_column :spree_countries, :updated_at, :datetime
+ end
+
+ def down
+ remove_column :spree_countries, :updated_at
+ end
+end
diff --git a/core/db/migrate/20130619012236_add_updated_at_to_spree_states.rb b/core/db/migrate/20130619012236_add_updated_at_to_spree_states.rb
new file mode 100644
index 00000000000..b9e06fccee8
--- /dev/null
+++ b/core/db/migrate/20130619012236_add_updated_at_to_spree_states.rb
@@ -0,0 +1,9 @@
+class AddUpdatedAtToSpreeStates < ActiveRecord::Migration[4.2]
+ def up
+ add_column :spree_states, :updated_at, :datetime
+ end
+
+ def down
+ remove_column :spree_states, :updated_at
+ end
+end
diff --git a/core/db/migrate/20130626232741_add_cvv_result_code_and_cvv_result_message_to_spree_payments.rb b/core/db/migrate/20130626232741_add_cvv_result_code_and_cvv_result_message_to_spree_payments.rb
new file mode 100644
index 00000000000..1f2dbf8953d
--- /dev/null
+++ b/core/db/migrate/20130626232741_add_cvv_result_code_and_cvv_result_message_to_spree_payments.rb
@@ -0,0 +1,6 @@
+class AddCvvResultCodeAndCvvResultMessageToSpreePayments < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_payments, :cvv_response_code, :string
+ add_column :spree_payments, :cvv_response_message, :string
+ end
+end
diff --git a/core/db/migrate/20130628021056_add_unique_index_to_permalink_on_spree_products.rb b/core/db/migrate/20130628021056_add_unique_index_to_permalink_on_spree_products.rb
new file mode 100644
index 00000000000..9c8e61865dd
--- /dev/null
+++ b/core/db/migrate/20130628021056_add_unique_index_to_permalink_on_spree_products.rb
@@ -0,0 +1,5 @@
+class AddUniqueIndexToPermalinkOnSpreeProducts < ActiveRecord::Migration[4.2]
+ def change
+ add_index "spree_products", ["permalink"], name: "permalink_idx_unique", unique: true
+ end
+end
diff --git a/core/db/migrate/20130628022817_add_unique_index_to_orders_shipments_and_stock_transfers.rb b/core/db/migrate/20130628022817_add_unique_index_to_orders_shipments_and_stock_transfers.rb
new file mode 100644
index 00000000000..7f89daadd33
--- /dev/null
+++ b/core/db/migrate/20130628022817_add_unique_index_to_orders_shipments_and_stock_transfers.rb
@@ -0,0 +1,7 @@
+class AddUniqueIndexToOrdersShipmentsAndStockTransfers < ActiveRecord::Migration[4.2]
+ def add
+ add_index "spree_orders", ["number"], name: "number_idx_unique", unique: true
+ add_index "spree_shipments", ["number"], name: "number_idx_unique", unique: true
+ add_index "spree_stock_transfers", ["number"], name: "number_idx_unique", unique: true
+ end
+end
diff --git a/core/db/migrate/20130708052307_add_deleted_at_to_spree_tax_rates.rb b/core/db/migrate/20130708052307_add_deleted_at_to_spree_tax_rates.rb
new file mode 100644
index 00000000000..103517b2a71
--- /dev/null
+++ b/core/db/migrate/20130708052307_add_deleted_at_to_spree_tax_rates.rb
@@ -0,0 +1,5 @@
+class AddDeletedAtToSpreeTaxRates < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_tax_rates, :deleted_at, :datetime
+ end
+end
diff --git a/core/db/migrate/20130711200933_remove_lock_version_from_inventory_units.rb b/core/db/migrate/20130711200933_remove_lock_version_from_inventory_units.rb
new file mode 100644
index 00000000000..b45b9ba78a4
--- /dev/null
+++ b/core/db/migrate/20130711200933_remove_lock_version_from_inventory_units.rb
@@ -0,0 +1,6 @@
+class RemoveLockVersionFromInventoryUnits < ActiveRecord::Migration[4.2]
+ def change
+ # we are moving to pessimistic locking on stock_items
+ remove_column :spree_inventory_units, :lock_version
+ end
+end
diff --git a/core/db/migrate/20130718042445_add_cost_price_to_line_item.rb b/core/db/migrate/20130718042445_add_cost_price_to_line_item.rb
new file mode 100644
index 00000000000..9f768163139
--- /dev/null
+++ b/core/db/migrate/20130718042445_add_cost_price_to_line_item.rb
@@ -0,0 +1,5 @@
+class AddCostPriceToLineItem < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_line_items, :cost_price, :decimal, precision: 8, scale: 2
+ end
+end
diff --git a/core/db/migrate/20130718233855_set_backorderable_to_default_to_false.rb b/core/db/migrate/20130718233855_set_backorderable_to_default_to_false.rb
new file mode 100644
index 00000000000..08ff1ba31ac
--- /dev/null
+++ b/core/db/migrate/20130718233855_set_backorderable_to_default_to_false.rb
@@ -0,0 +1,6 @@
+class SetBackorderableToDefaultToFalse < ActiveRecord::Migration[4.2]
+ def change
+ change_column :spree_stock_items, :backorderable, :boolean, default: false
+ change_column :spree_stock_locations, :backorderable_default, :boolean, default: false
+ end
+end
diff --git a/core/db/migrate/20130725031716_add_created_by_id_to_spree_orders.rb b/core/db/migrate/20130725031716_add_created_by_id_to_spree_orders.rb
new file mode 100644
index 00000000000..cea613e943c
--- /dev/null
+++ b/core/db/migrate/20130725031716_add_created_by_id_to_spree_orders.rb
@@ -0,0 +1,5 @@
+class AddCreatedByIdToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :created_by_id, :integer
+ end
+end
diff --git a/core/db/migrate/20130729214043_index_completed_at_on_spree_orders.rb b/core/db/migrate/20130729214043_index_completed_at_on_spree_orders.rb
new file mode 100644
index 00000000000..24e06b6235a
--- /dev/null
+++ b/core/db/migrate/20130729214043_index_completed_at_on_spree_orders.rb
@@ -0,0 +1,5 @@
+class IndexCompletedAtOnSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_orders, :completed_at
+ end
+end
diff --git a/core/db/migrate/20130802014537_add_tax_category_id_to_spree_line_items.rb b/core/db/migrate/20130802014537_add_tax_category_id_to_spree_line_items.rb
new file mode 100644
index 00000000000..bffa364245e
--- /dev/null
+++ b/core/db/migrate/20130802014537_add_tax_category_id_to_spree_line_items.rb
@@ -0,0 +1,5 @@
+class AddTaxCategoryIdToSpreeLineItems < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_line_items, :tax_category_id, :integer
+ end
+end
diff --git a/core/db/migrate/20130802022321_migrate_tax_categories_to_line_items.rb b/core/db/migrate/20130802022321_migrate_tax_categories_to_line_items.rb
new file mode 100644
index 00000000000..d3d3fe68342
--- /dev/null
+++ b/core/db/migrate/20130802022321_migrate_tax_categories_to_line_items.rb
@@ -0,0 +1,10 @@
+class MigrateTaxCategoriesToLineItems < ActiveRecord::Migration[4.2]
+ def change
+ Spree::LineItem.find_each do |line_item|
+ next if line_item.variant.nil?
+ next if line_item.variant.product.nil?
+ next if line_item.product.nil?
+ line_item.update_column(:tax_category_id, line_item.product.tax_category_id)
+ end
+ end
+end
diff --git a/core/db/migrate/20130806022521_drop_spree_mail_methods.rb b/core/db/migrate/20130806022521_drop_spree_mail_methods.rb
new file mode 100644
index 00000000000..f54d6af23cc
--- /dev/null
+++ b/core/db/migrate/20130806022521_drop_spree_mail_methods.rb
@@ -0,0 +1,12 @@
+class DropSpreeMailMethods < ActiveRecord::Migration[4.2]
+ def up
+ drop_table :spree_mail_methods
+ end
+
+ def down
+ create_table(:spree_mail_methods) do |t|
+ t.string :environment
+ t.boolean :active
+ end
+ end
+end
diff --git a/core/db/migrate/20130806145853_set_default_stock_location_on_shipments.rb b/core/db/migrate/20130806145853_set_default_stock_location_on_shipments.rb
new file mode 100644
index 00000000000..cb421593cee
--- /dev/null
+++ b/core/db/migrate/20130806145853_set_default_stock_location_on_shipments.rb
@@ -0,0 +1,8 @@
+class SetDefaultStockLocationOnShipments < ActiveRecord::Migration[4.2]
+ def change
+ if Spree::Shipment.where('stock_location_id IS NULL').count > 0
+ location = Spree::StockLocation.find_by(name: 'default') || Spree::StockLocation.first
+ Spree::Shipment.where('stock_location_id IS NULL').update_all(stock_location_id: location.id)
+ end
+ end
+end
diff --git a/core/db/migrate/20130807024301_upgrade_adjustments.rb b/core/db/migrate/20130807024301_upgrade_adjustments.rb
new file mode 100644
index 00000000000..ee9dbd2f6c2
--- /dev/null
+++ b/core/db/migrate/20130807024301_upgrade_adjustments.rb
@@ -0,0 +1,46 @@
+class UpgradeAdjustments < ActiveRecord::Migration[4.2]
+ def up
+ # Add Temporary index
+ add_index :spree_adjustments, :originator_type unless index_exists?(:spree_adjustments, :originator_type)
+
+ # Temporarily make originator association available
+ Spree::Adjustment.class_eval do
+ belongs_to :originator, polymorphic: true
+ end
+ # Shipping adjustments are now tracked as fields on the object
+ Spree::Adjustment.where(source_type: "Spree::Shipment").find_each do |adjustment|
+ # Account for possible invalid data
+ next if adjustment.source.nil?
+ adjustment.source.update_column(:cost, adjustment.amount)
+ adjustment.destroy!
+ end
+
+ # Tax adjustments have their sources altered
+ Spree::Adjustment.where(originator_type: "Spree::TaxRate").find_each do |adjustment|
+ adjustment.source_id = adjustment.originator_id
+ adjustment.source_type = "Spree::TaxRate"
+ adjustment.save!
+ end
+
+ # Promotion adjustments have their source altered also
+ Spree::Adjustment.where(originator_type: "Spree::PromotionAction").find_each do |adjustment|
+ next if adjustment.originator.nil?
+ adjustment.source = adjustment.originator
+ begin
+ if adjustment.source.calculator_type == "Spree::Calculator::FreeShipping"
+ # Previously this was a Spree::Promotion::Actions::CreateAdjustment
+ # And it had a calculator to work out FreeShipping
+ # In Spree 2.2, the "calculator" is now the action itself.
+ adjustment.source.becomes(Spree::Promotion::Actions::FreeShipping)
+ end
+ rescue
+ # Fail silently. This is primarily in instances where the calculator no longer exists
+ end
+
+ adjustment.save!
+ end
+
+ # Remove Temporary index
+ remove_index :spree_adjustments, :originator_type if index_exists?(:spree_adjustments, :originator_type)
+ end
+end
diff --git a/core/db/migrate/20130807024302_rename_adjustment_fields.rb b/core/db/migrate/20130807024302_rename_adjustment_fields.rb
new file mode 100644
index 00000000000..f6b362b347c
--- /dev/null
+++ b/core/db/migrate/20130807024302_rename_adjustment_fields.rb
@@ -0,0 +1,20 @@
+class RenameAdjustmentFields < ActiveRecord::Migration[4.2]
+ def up
+ # Add Temporary index
+ add_index :spree_adjustments, :adjustable_type unless index_exists?(:spree_adjustments, :adjustable_type)
+
+ remove_column :spree_adjustments, :originator_id
+ remove_column :spree_adjustments, :originator_type
+
+ add_column :spree_adjustments, :order_id, :integer unless column_exists?(:spree_adjustments, :order_id)
+
+ # This enables the Spree::Order#all_adjustments association to work correctly
+ Spree::Adjustment.reset_column_information
+ Spree::Adjustment.where(adjustable_type: "Spree::Order").find_each do |adjustment|
+ adjustment.update_column(:order_id, adjustment.adjustable_id)
+ end
+
+ # Remove Temporary index
+ remove_index :spree_adjustments, :adjustable_type if index_exists?(:spree_adjustments, :adjustable_type)
+ end
+end
diff --git a/core/db/migrate/20130809164245_add_admin_name_column_to_spree_shipping_methods.rb b/core/db/migrate/20130809164245_add_admin_name_column_to_spree_shipping_methods.rb
new file mode 100644
index 00000000000..af7655cdc98
--- /dev/null
+++ b/core/db/migrate/20130809164245_add_admin_name_column_to_spree_shipping_methods.rb
@@ -0,0 +1,5 @@
+class AddAdminNameColumnToSpreeShippingMethods < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_shipping_methods, :admin_name, :string
+ end
+end
diff --git a/core/db/migrate/20130809164330_add_admin_name_column_to_spree_stock_locations.rb b/core/db/migrate/20130809164330_add_admin_name_column_to_spree_stock_locations.rb
new file mode 100644
index 00000000000..39b8b17a763
--- /dev/null
+++ b/core/db/migrate/20130809164330_add_admin_name_column_to_spree_stock_locations.rb
@@ -0,0 +1,5 @@
+class AddAdminNameColumnToSpreeStockLocations < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_stock_locations, :admin_name, :string
+ end
+end
diff --git a/core/db/migrate/20130813004002_add_shipment_total_to_spree_orders.rb b/core/db/migrate/20130813004002_add_shipment_total_to_spree_orders.rb
new file mode 100644
index 00000000000..0722caba0d7
--- /dev/null
+++ b/core/db/migrate/20130813004002_add_shipment_total_to_spree_orders.rb
@@ -0,0 +1,5 @@
+class AddShipmentTotalToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :shipment_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ end
+end
diff --git a/core/db/migrate/20130813140619_expand_order_number_size.rb b/core/db/migrate/20130813140619_expand_order_number_size.rb
new file mode 100644
index 00000000000..347e1f5e3e3
--- /dev/null
+++ b/core/db/migrate/20130813140619_expand_order_number_size.rb
@@ -0,0 +1,9 @@
+class ExpandOrderNumberSize < ActiveRecord::Migration[4.2]
+ def up
+ change_column :spree_orders, :number, :string, limit: 32
+ end
+
+ def down
+ change_column :spree_orders, :number, :string, limit: 15
+ end
+end
diff --git a/core/db/migrate/20130813232134_rename_activators_to_promotions.rb b/core/db/migrate/20130813232134_rename_activators_to_promotions.rb
new file mode 100644
index 00000000000..fedd42715ba
--- /dev/null
+++ b/core/db/migrate/20130813232134_rename_activators_to_promotions.rb
@@ -0,0 +1,5 @@
+class RenameActivatorsToPromotions < ActiveRecord::Migration[4.2]
+ def change
+ rename_table :spree_activators, :spree_promotions
+ end
+end
diff --git a/core/db/migrate/20130815000406_add_adjustment_total_to_line_items.rb b/core/db/migrate/20130815000406_add_adjustment_total_to_line_items.rb
new file mode 100644
index 00000000000..6a91e39bd24
--- /dev/null
+++ b/core/db/migrate/20130815000406_add_adjustment_total_to_line_items.rb
@@ -0,0 +1,5 @@
+class AddAdjustmentTotalToLineItems < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_line_items, :adjustment_total, :decimal, precision: 10, scale: 2, default: 0.0
+ end
+end
diff --git a/core/db/migrate/20130815024413_add_adjustment_total_to_shipments.rb b/core/db/migrate/20130815024413_add_adjustment_total_to_shipments.rb
new file mode 100644
index 00000000000..26a0b6b2756
--- /dev/null
+++ b/core/db/migrate/20130815024413_add_adjustment_total_to_shipments.rb
@@ -0,0 +1,5 @@
+class AddAdjustmentTotalToShipments < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_shipments, :adjustment_total, :decimal, precision: 10, scale: 2, default: 0.0
+ end
+end
diff --git a/core/db/migrate/20130826062534_add_depth_to_spree_taxons.rb b/core/db/migrate/20130826062534_add_depth_to_spree_taxons.rb
new file mode 100644
index 00000000000..4cbc4399d2c
--- /dev/null
+++ b/core/db/migrate/20130826062534_add_depth_to_spree_taxons.rb
@@ -0,0 +1,16 @@
+class AddDepthToSpreeTaxons < ActiveRecord::Migration[4.2]
+ def up
+ if !Spree::Taxon.column_names.include?('depth')
+ add_column :spree_taxons, :depth, :integer
+
+ say_with_time 'Update depth on all taxons' do
+ Spree::Taxon.reset_column_information
+ Spree::Taxon.all.each { |t| t.save }
+ end
+ end
+ end
+
+ def down
+ remove_column :spree_taxons, :depth
+ end
+end
diff --git a/core/db/migrate/20130828234942_add_tax_total_to_line_items_shipments_and_orders.rb b/core/db/migrate/20130828234942_add_tax_total_to_line_items_shipments_and_orders.rb
new file mode 100644
index 00000000000..61ba63c5bb8
--- /dev/null
+++ b/core/db/migrate/20130828234942_add_tax_total_to_line_items_shipments_and_orders.rb
@@ -0,0 +1,8 @@
+class AddTaxTotalToLineItemsShipmentsAndOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_line_items, :tax_total, :decimal, precision: 10, scale: 2, default: 0.0
+ add_column :spree_shipments, :tax_total, :decimal, precision: 10, scale: 2, default: 0.0
+ # This column may already be here from a 2.1.x migration
+ add_column :spree_orders, :tax_total, :decimal, precision: 10, scale: 2, default: 0.0 unless column_exists? :spree_orders, :tax_total, :decimal
+ end
+end
diff --git a/core/db/migrate/20130830001033_add_shipping_category_to_shipping_methods_and_products.rb b/core/db/migrate/20130830001033_add_shipping_category_to_shipping_methods_and_products.rb
new file mode 100644
index 00000000000..e3047872c48
--- /dev/null
+++ b/core/db/migrate/20130830001033_add_shipping_category_to_shipping_methods_and_products.rb
@@ -0,0 +1,15 @@
+class AddShippingCategoryToShippingMethodsAndProducts < ActiveRecord::Migration[4.2]
+ def up
+ default_category = Spree::ShippingCategory.first
+ default_category ||= Spree::ShippingCategory.create!(name: "Default")
+
+ Spree::ShippingMethod.all.each do |method|
+ method.shipping_categories << default_category if method.shipping_categories.blank?
+ end
+
+ Spree::Product.where(shipping_category_id: nil).update_all(shipping_category_id: default_category.id)
+ end
+
+ def down
+ end
+end
diff --git a/core/db/migrate/20130830001159_migrate_old_shipping_calculators.rb b/core/db/migrate/20130830001159_migrate_old_shipping_calculators.rb
new file mode 100644
index 00000000000..78475beb41b
--- /dev/null
+++ b/core/db/migrate/20130830001159_migrate_old_shipping_calculators.rb
@@ -0,0 +1,19 @@
+class MigrateOldShippingCalculators < ActiveRecord::Migration[4.2]
+ def up
+ Spree::ShippingMethod.all.each do |shipping_method|
+ old_calculator = shipping_method.calculator
+ next if old_calculator.class < Spree::ShippingCalculator # We don't want to mess with new shipping calculators
+ new_calculator = eval(old_calculator.class.name.sub("::Calculator::", "::Calculator::Shipping::")).new
+ new_calculator.preferences.keys.each do |pref|
+ # Preferences can't be read/set by name, you have to prefix preferred_
+ pref_method = "preferred_#{pref}"
+ new_calculator.send("#{pref_method}=", old_calculator.send(pref_method))
+ end
+ new_calculator.calculable = old_calculator.calculable
+ new_calculator.save
+ end
+ end
+
+ def down
+ end
+end
diff --git a/core/db/migrate/20130903183026_add_code_to_spree_promotion_rules.rb b/core/db/migrate/20130903183026_add_code_to_spree_promotion_rules.rb
new file mode 100644
index 00000000000..8aa9c2351ca
--- /dev/null
+++ b/core/db/migrate/20130903183026_add_code_to_spree_promotion_rules.rb
@@ -0,0 +1,5 @@
+class AddCodeToSpreePromotionRules < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_promotion_rules, :code, :string
+ end
+end
diff --git a/core/db/migrate/20130909115621_change_states_required_for_countries.rb b/core/db/migrate/20130909115621_change_states_required_for_countries.rb
new file mode 100644
index 00000000000..98703554697
--- /dev/null
+++ b/core/db/migrate/20130909115621_change_states_required_for_countries.rb
@@ -0,0 +1,9 @@
+class ChangeStatesRequiredForCountries < ActiveRecord::Migration[4.2]
+ def up
+ change_column_default :spree_countries, :states_required, false
+ end
+
+ def down
+ change_column_default :spree_countries, :states_required, true
+ end
+end
diff --git a/core/db/migrate/20130915032339_add_deleted_at_to_spree_stock_items.rb b/core/db/migrate/20130915032339_add_deleted_at_to_spree_stock_items.rb
new file mode 100644
index 00000000000..eca5b4c6376
--- /dev/null
+++ b/core/db/migrate/20130915032339_add_deleted_at_to_spree_stock_items.rb
@@ -0,0 +1,5 @@
+class AddDeletedAtToSpreeStockItems < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_stock_items, :deleted_at, :datetime
+ end
+end
diff --git a/core/db/migrate/20130917024658_remove_promotions_event_name_field.rb b/core/db/migrate/20130917024658_remove_promotions_event_name_field.rb
new file mode 100644
index 00000000000..095ac72dcdc
--- /dev/null
+++ b/core/db/migrate/20130917024658_remove_promotions_event_name_field.rb
@@ -0,0 +1,5 @@
+class RemovePromotionsEventNameField < ActiveRecord::Migration[4.2]
+ def change
+ remove_column :spree_promotions, :event_name, :string
+ end
+end
diff --git a/core/db/migrate/20130924040529_add_promo_total_to_line_items_and_shipments_and_orders.rb b/core/db/migrate/20130924040529_add_promo_total_to_line_items_and_shipments_and_orders.rb
new file mode 100644
index 00000000000..f7f34d07161
--- /dev/null
+++ b/core/db/migrate/20130924040529_add_promo_total_to_line_items_and_shipments_and_orders.rb
@@ -0,0 +1,7 @@
+class AddPromoTotalToLineItemsAndShipmentsAndOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_line_items, :promo_total, :decimal, precision: 10, scale: 2, default: 0.0
+ add_column :spree_shipments, :promo_total, :decimal, precision: 10, scale: 2, default: 0.0
+ add_column :spree_orders, :promo_total, :decimal, precision: 10, scale: 2, default: 0.0
+ end
+end
diff --git a/core/db/migrate/20131001013410_remove_unused_credit_card_fields.rb b/core/db/migrate/20131001013410_remove_unused_credit_card_fields.rb
new file mode 100644
index 00000000000..1c2403d54b6
--- /dev/null
+++ b/core/db/migrate/20131001013410_remove_unused_credit_card_fields.rb
@@ -0,0 +1,16 @@
+class RemoveUnusedCreditCardFields < ActiveRecord::Migration[4.2]
+ def up
+ remove_column :spree_credit_cards, :start_month if column_exists?(:spree_credit_cards, :start_month)
+ remove_column :spree_credit_cards, :start_year if column_exists?(:spree_credit_cards, :start_year)
+ remove_column :spree_credit_cards, :issue_number if column_exists?(:spree_credit_cards, :issue_number)
+ end
+ def down
+ add_column :spree_credit_cards, :start_month, :string
+ add_column :spree_credit_cards, :start_year, :string
+ add_column :spree_credit_cards, :issue_number, :string
+ end
+
+ def column_exists?(table, column)
+ ApplicationRecord.connection.column_exists?(table, column)
+ end
+end
diff --git a/core/db/migrate/20131026154747_add_track_inventory_to_variant.rb b/core/db/migrate/20131026154747_add_track_inventory_to_variant.rb
new file mode 100644
index 00000000000..58f46b81521
--- /dev/null
+++ b/core/db/migrate/20131026154747_add_track_inventory_to_variant.rb
@@ -0,0 +1,5 @@
+class AddTrackInventoryToVariant < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_variants, :track_inventory, :boolean, default: true
+ end
+end
diff --git a/core/db/migrate/20131107132123_add_tax_category_to_variants.rb b/core/db/migrate/20131107132123_add_tax_category_to_variants.rb
new file mode 100644
index 00000000000..c7793146a5c
--- /dev/null
+++ b/core/db/migrate/20131107132123_add_tax_category_to_variants.rb
@@ -0,0 +1,6 @@
+class AddTaxCategoryToVariants < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_variants, :tax_category_id, :integer
+ add_index :spree_variants, :tax_category_id
+ end
+end
diff --git a/core/db/migrate/20131113035136_add_channel_to_spree_orders.rb b/core/db/migrate/20131113035136_add_channel_to_spree_orders.rb
new file mode 100644
index 00000000000..86d2a023c68
--- /dev/null
+++ b/core/db/migrate/20131113035136_add_channel_to_spree_orders.rb
@@ -0,0 +1,5 @@
+class AddChannelToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :channel, :string, default: "spree"
+ end
+end
diff --git a/core/db/migrate/20131118043959_add_included_to_adjustments.rb b/core/db/migrate/20131118043959_add_included_to_adjustments.rb
new file mode 100644
index 00000000000..c562c94ad4a
--- /dev/null
+++ b/core/db/migrate/20131118043959_add_included_to_adjustments.rb
@@ -0,0 +1,5 @@
+class AddIncludedToAdjustments < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_adjustments, :included, :boolean, default: false unless Spree::Adjustment.column_names.include?("included")
+ end
+end
diff --git a/core/db/migrate/20131118050234_rename_tax_total_fields.rb b/core/db/migrate/20131118050234_rename_tax_total_fields.rb
new file mode 100644
index 00000000000..23239bec2e2
--- /dev/null
+++ b/core/db/migrate/20131118050234_rename_tax_total_fields.rb
@@ -0,0 +1,11 @@
+class RenameTaxTotalFields < ActiveRecord::Migration[4.2]
+ def change
+ rename_column :spree_line_items, :tax_total, :additional_tax_total
+ rename_column :spree_shipments, :tax_total, :additional_tax_total
+ rename_column :spree_orders, :tax_total, :additional_tax_total
+
+ add_column :spree_line_items, :included_tax_total, :decimal, precision: 10, scale: 2, null: false, default: 0.0
+ add_column :spree_shipments, :included_tax_total, :decimal, precision: 10, scale: 2, null: false, default: 0.0
+ add_column :spree_orders, :included_tax_total, :decimal, precision: 10, scale: 2, null: false, default: 0.0
+ end
+end
diff --git a/core/db/migrate/20131118183431_add_line_item_id_to_spree_inventory_units.rb b/core/db/migrate/20131118183431_add_line_item_id_to_spree_inventory_units.rb
new file mode 100644
index 00000000000..8380cf197b6
--- /dev/null
+++ b/core/db/migrate/20131118183431_add_line_item_id_to_spree_inventory_units.rb
@@ -0,0 +1,21 @@
+class AddLineItemIdToSpreeInventoryUnits < ActiveRecord::Migration[4.2]
+ def change
+ # Stores running the product-assembly extension already have a line_item_id column
+ unless column_exists? Spree::InventoryUnit.table_name, :line_item_id
+ add_column :spree_inventory_units, :line_item_id, :integer
+ add_index :spree_inventory_units, :line_item_id
+
+ shipments = Spree::Shipment.includes(:inventory_units, :order)
+
+ shipments.find_each do |shipment|
+ shipment.inventory_units.group_by(&:variant_id).each do |variant_id, units|
+
+ line_item = shipment.order.line_items.find_by(variant_id: variant_id)
+ next unless line_item
+
+ Spree::InventoryUnit.where(id: units.map(&:id)).update_all(line_item_id: line_item.id)
+ end
+ end
+ end
+ end
+end
diff --git a/core/db/migrate/20131120234456_add_updated_at_to_variants.rb b/core/db/migrate/20131120234456_add_updated_at_to_variants.rb
new file mode 100644
index 00000000000..409c5a1db23
--- /dev/null
+++ b/core/db/migrate/20131120234456_add_updated_at_to_variants.rb
@@ -0,0 +1,5 @@
+class AddUpdatedAtToVariants < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_variants, :updated_at, :datetime
+ end
+end
diff --git a/core/db/migrate/20131127001002_add_position_to_classifications.rb b/core/db/migrate/20131127001002_add_position_to_classifications.rb
new file mode 100644
index 00000000000..15e46057e24
--- /dev/null
+++ b/core/db/migrate/20131127001002_add_position_to_classifications.rb
@@ -0,0 +1,5 @@
+class AddPositionToClassifications < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_products_taxons, :position, :integer
+ end
+end
diff --git a/core/db/migrate/20131211112807_create_spree_orders_promotions.rb b/core/db/migrate/20131211112807_create_spree_orders_promotions.rb
new file mode 100644
index 00000000000..1e013cd48d6
--- /dev/null
+++ b/core/db/migrate/20131211112807_create_spree_orders_promotions.rb
@@ -0,0 +1,8 @@
+class CreateSpreeOrdersPromotions < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_orders_promotions, id: false do |t|
+ t.references :order
+ t.references :promotion
+ end
+ end
+end
diff --git a/core/db/migrate/20131211192741_unique_shipping_method_categories.rb b/core/db/migrate/20131211192741_unique_shipping_method_categories.rb
new file mode 100644
index 00000000000..13da4c89894
--- /dev/null
+++ b/core/db/migrate/20131211192741_unique_shipping_method_categories.rb
@@ -0,0 +1,24 @@
+class UniqueShippingMethodCategories < ActiveRecord::Migration[4.2]
+ def change
+ klass = Spree::ShippingMethodCategory
+ columns = %w[shipping_category_id shipping_method_id]
+
+ say "Find duplicate #{klass} records"
+ duplicates = klass.
+ select((columns + %w[COUNT(*)]).join(',')).
+ group(columns.join(',')).
+ having('COUNT(*) > 1').
+ map { |row| row.attributes.slice(*columns) }
+
+ say "Delete all but the oldest duplicate #{klass} record"
+ duplicates.each do |conditions|
+ klass.where(conditions).order(:created_at).drop(1).each(&:destroy)
+ end
+
+ say "Add unique index to #{klass.table_name} for #{columns.inspect}"
+ add_index klass.table_name, columns, unique: true, name: 'unique_spree_shipping_method_categories'
+
+ say "Remove redundant simple index on #{klass.table_name}"
+ remove_index klass.table_name, name: 'index_spree_shipping_method_categories_on_shipping_category_id'
+ end
+end
diff --git a/core/db/migrate/20131218054603_add_item_count_to_spree_orders.rb b/core/db/migrate/20131218054603_add_item_count_to_spree_orders.rb
new file mode 100644
index 00000000000..6ee215c564b
--- /dev/null
+++ b/core/db/migrate/20131218054603_add_item_count_to_spree_orders.rb
@@ -0,0 +1,5 @@
+class AddItemCountToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :item_count, :integer, default: 0
+ end
+end
diff --git a/core/db/migrate/20140106065820_remove_value_type_from_spree_preferences.rb b/core/db/migrate/20140106065820_remove_value_type_from_spree_preferences.rb
new file mode 100644
index 00000000000..2a8678e3ae7
--- /dev/null
+++ b/core/db/migrate/20140106065820_remove_value_type_from_spree_preferences.rb
@@ -0,0 +1,8 @@
+class RemoveValueTypeFromSpreePreferences < ActiveRecord::Migration[4.2]
+ def up
+ remove_column :spree_preferences, :value_type
+ end
+ def down
+ raise ActiveRecord::IrreversableMigration
+ end
+end
diff --git a/core/db/migrate/20140106224208_rename_permalink_to_slug_for_products.rb b/core/db/migrate/20140106224208_rename_permalink_to_slug_for_products.rb
new file mode 100644
index 00000000000..e71b093c0db
--- /dev/null
+++ b/core/db/migrate/20140106224208_rename_permalink_to_slug_for_products.rb
@@ -0,0 +1,5 @@
+class RenamePermalinkToSlugForProducts < ActiveRecord::Migration[4.2]
+ def change
+ rename_column :spree_products, :permalink, :slug
+ end
+end
diff --git a/core/db/migrate/20140120160805_add_index_to_variant_id_and_currency_on_prices.rb b/core/db/migrate/20140120160805_add_index_to_variant_id_and_currency_on_prices.rb
new file mode 100644
index 00000000000..c1d8143968c
--- /dev/null
+++ b/core/db/migrate/20140120160805_add_index_to_variant_id_and_currency_on_prices.rb
@@ -0,0 +1,5 @@
+class AddIndexToVariantIdAndCurrencyOnPrices < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_prices, [:variant_id, :currency]
+ end
+end
diff --git a/core/db/migrate/20140124023232_rename_activator_id_in_rules_and_actions_to_promotion_id.rb b/core/db/migrate/20140124023232_rename_activator_id_in_rules_and_actions_to_promotion_id.rb
new file mode 100644
index 00000000000..c69c0f5d0c1
--- /dev/null
+++ b/core/db/migrate/20140124023232_rename_activator_id_in_rules_and_actions_to_promotion_id.rb
@@ -0,0 +1,6 @@
+class RenameActivatorIdInRulesAndActionsToPromotionId < ActiveRecord::Migration[4.2]
+ def change
+ rename_column :spree_promotion_rules, :activator_id, :promotion_id
+ rename_column :spree_promotion_actions, :activator_id, :promotion_id
+ end
+end
diff --git a/core/db/migrate/20140129024326_add_deleted_at_to_spree_prices.rb b/core/db/migrate/20140129024326_add_deleted_at_to_spree_prices.rb
new file mode 100644
index 00000000000..c1159f8e5aa
--- /dev/null
+++ b/core/db/migrate/20140129024326_add_deleted_at_to_spree_prices.rb
@@ -0,0 +1,5 @@
+class AddDeletedAtToSpreePrices < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_prices, :deleted_at, :datetime
+ end
+end
diff --git a/core/db/migrate/20140203161722_add_approver_id_and_approved_at_to_orders.rb b/core/db/migrate/20140203161722_add_approver_id_and_approved_at_to_orders.rb
new file mode 100644
index 00000000000..81b3570a705
--- /dev/null
+++ b/core/db/migrate/20140203161722_add_approver_id_and_approved_at_to_orders.rb
@@ -0,0 +1,6 @@
+class AddApproverIdAndApprovedAtToOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :approver_id, :integer
+ add_column :spree_orders, :approved_at, :datetime
+ end
+end
diff --git a/core/db/migrate/20140204115338_add_confirmation_delivered_to_spree_orders.rb b/core/db/migrate/20140204115338_add_confirmation_delivered_to_spree_orders.rb
new file mode 100644
index 00000000000..50d82f14f3e
--- /dev/null
+++ b/core/db/migrate/20140204115338_add_confirmation_delivered_to_spree_orders.rb
@@ -0,0 +1,5 @@
+class AddConfirmationDeliveredToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :confirmation_delivered, :boolean, default: false
+ end
+end
diff --git a/core/db/migrate/20140204192230_add_auto_capture_to_payment_methods.rb b/core/db/migrate/20140204192230_add_auto_capture_to_payment_methods.rb
new file mode 100644
index 00000000000..f531a8ac5fb
--- /dev/null
+++ b/core/db/migrate/20140204192230_add_auto_capture_to_payment_methods.rb
@@ -0,0 +1,5 @@
+class AddAutoCaptureToPaymentMethods < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_payment_methods, :auto_capture, :boolean
+ end
+end
diff --git a/core/db/migrate/20140205120320_create_spree_payment_capture_events.rb b/core/db/migrate/20140205120320_create_spree_payment_capture_events.rb
new file mode 100644
index 00000000000..9107981526d
--- /dev/null
+++ b/core/db/migrate/20140205120320_create_spree_payment_capture_events.rb
@@ -0,0 +1,12 @@
+class CreateSpreePaymentCaptureEvents < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_payment_capture_events do |t|
+ t.decimal :amount, precision: 10, scale: 2, default: 0.0
+ t.integer :payment_id
+
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_payment_capture_events, :payment_id
+ end
+end
diff --git a/core/db/migrate/20140205144710_add_uncaptured_amount_to_payments.rb b/core/db/migrate/20140205144710_add_uncaptured_amount_to_payments.rb
new file mode 100644
index 00000000000..2856f3af609
--- /dev/null
+++ b/core/db/migrate/20140205144710_add_uncaptured_amount_to_payments.rb
@@ -0,0 +1,5 @@
+class AddUncapturedAmountToPayments < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_payments, :uncaptured_amount, :decimal, precision: 10, scale: 2, default: 0.0
+ end
+end
diff --git a/core/db/migrate/20140205181631_default_variant_weight_to_zero.rb b/core/db/migrate/20140205181631_default_variant_weight_to_zero.rb
new file mode 100644
index 00000000000..3041cf1b185
--- /dev/null
+++ b/core/db/migrate/20140205181631_default_variant_weight_to_zero.rb
@@ -0,0 +1,11 @@
+class DefaultVariantWeightToZero < ActiveRecord::Migration[4.2]
+ def up
+ Spree::Variant.unscoped.where(weight: nil).update_all("weight = 0.0")
+
+ change_column :spree_variants, :weight, :decimal, precision: 8, scale: 2, default: 0.0
+ end
+
+ def down
+ change_column :spree_variants, :weight, :decimal, precision: 8, scale: 2
+ end
+end
diff --git a/core/db/migrate/20140207085910_add_tax_category_id_to_shipping_methods.rb b/core/db/migrate/20140207085910_add_tax_category_id_to_shipping_methods.rb
new file mode 100644
index 00000000000..cf2db907545
--- /dev/null
+++ b/core/db/migrate/20140207085910_add_tax_category_id_to_shipping_methods.rb
@@ -0,0 +1,5 @@
+class AddTaxCategoryIdToShippingMethods < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_shipping_methods, :tax_category_id, :integer
+ end
+end
diff --git a/core/db/migrate/20140207093021_add_tax_rate_id_to_shipping_rates.rb b/core/db/migrate/20140207093021_add_tax_rate_id_to_shipping_rates.rb
new file mode 100644
index 00000000000..f148ec8288b
--- /dev/null
+++ b/core/db/migrate/20140207093021_add_tax_rate_id_to_shipping_rates.rb
@@ -0,0 +1,5 @@
+class AddTaxRateIdToShippingRates < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_shipping_rates, :tax_rate_id, :integer
+ end
+end
diff --git a/core/db/migrate/20140211040159_add_pre_tax_amount_to_line_items_and_shipments.rb b/core/db/migrate/20140211040159_add_pre_tax_amount_to_line_items_and_shipments.rb
new file mode 100644
index 00000000000..586aef84594
--- /dev/null
+++ b/core/db/migrate/20140211040159_add_pre_tax_amount_to_line_items_and_shipments.rb
@@ -0,0 +1,6 @@
+class AddPreTaxAmountToLineItemsAndShipments < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_line_items, :pre_tax_amount, :decimal, precision: 8, scale: 2
+ add_column :spree_shipments, :pre_tax_amount, :decimal, precision: 8, scale: 2
+ end
+end
diff --git a/core/db/migrate/20140213184916_add_more_indexes.rb b/core/db/migrate/20140213184916_add_more_indexes.rb
new file mode 100644
index 00000000000..15381ee3b4f
--- /dev/null
+++ b/core/db/migrate/20140213184916_add_more_indexes.rb
@@ -0,0 +1,13 @@
+class AddMoreIndexes < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_payment_methods, [:id, :type]
+ add_index :spree_calculators, [:id, :type]
+ add_index :spree_calculators, [:calculable_id, :calculable_type]
+ add_index :spree_payments, :payment_method_id
+ add_index :spree_promotion_actions, [:id, :type]
+ add_index :spree_promotion_actions, :promotion_id
+ add_index :spree_promotions, [:id, :type]
+ add_index :spree_option_values, :option_type_id
+ add_index :spree_shipments, :stock_location_id
+ end
+end
diff --git a/core/db/migrate/20140219060952_add_considered_risky_to_orders.rb b/core/db/migrate/20140219060952_add_considered_risky_to_orders.rb
new file mode 100644
index 00000000000..5a3eb04a9ba
--- /dev/null
+++ b/core/db/migrate/20140219060952_add_considered_risky_to_orders.rb
@@ -0,0 +1,5 @@
+class AddConsideredRiskyToOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :considered_risky, :boolean, default: false
+ end
+end
diff --git a/core/db/migrate/20140227112348_add_preference_store_to_everything.rb b/core/db/migrate/20140227112348_add_preference_store_to_everything.rb
new file mode 100644
index 00000000000..79668077657
--- /dev/null
+++ b/core/db/migrate/20140227112348_add_preference_store_to_everything.rb
@@ -0,0 +1,8 @@
+class AddPreferenceStoreToEverything < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_calculators, :preferences, :text
+ add_column :spree_gateways, :preferences, :text
+ add_column :spree_payment_methods, :preferences, :text
+ add_column :spree_promotion_rules, :preferences, :text
+ end
+end
diff --git a/core/db/migrate/20140307235515_add_user_id_to_spree_credit_cards.rb b/core/db/migrate/20140307235515_add_user_id_to_spree_credit_cards.rb
new file mode 100644
index 00000000000..904b8725907
--- /dev/null
+++ b/core/db/migrate/20140307235515_add_user_id_to_spree_credit_cards.rb
@@ -0,0 +1,13 @@
+class AddUserIdToSpreeCreditCards < ActiveRecord::Migration[4.2]
+ 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
+
+ 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/core/db/migrate/20140309023735_migrate_old_preferences.rb b/core/db/migrate/20140309023735_migrate_old_preferences.rb
new file mode 100644
index 00000000000..548673f2580
--- /dev/null
+++ b/core/db/migrate/20140309023735_migrate_old_preferences.rb
@@ -0,0 +1,27 @@
+class MigrateOldPreferences < ActiveRecord::Migration[4.2]
+ def up
+ if Spree::Calculator.respond_to?(:with_deleted)
+ migrate_preferences(Spree::Calculator.with_deleted)
+ else
+ migrate_preferences(Spree::Calculator)
+ end
+ migrate_preferences(Spree::PaymentMethod)
+ migrate_preferences(Spree::PromotionRule)
+ end
+
+ def down
+ end
+
+ private
+ def migrate_preferences klass
+ klass.reset_column_information
+ klass.find_each do |record|
+ store = Spree::Preferences::ScopedStore.new(record.class.name.underscore, record.id)
+ record.defined_preferences.each do |key|
+ value = store.fetch(key){}
+ record.preferences[key] = value unless value.nil?
+ end
+ record.save!
+ end
+ end
+end
diff --git a/core/db/migrate/20140309024355_create_spree_stores.rb b/core/db/migrate/20140309024355_create_spree_stores.rb
new file mode 100644
index 00000000000..2b6e0a1d65d
--- /dev/null
+++ b/core/db/migrate/20140309024355_create_spree_stores.rb
@@ -0,0 +1,25 @@
+class CreateSpreeStores < ActiveRecord::Migration[4.2]
+ def change
+ if data_source_exists?(:spree_stores)
+ rename_column :spree_stores, :domains, :url
+ rename_column :spree_stores, :email, :mail_from_address
+ add_column :spree_stores, :meta_description, :text
+ add_column :spree_stores, :meta_keywords, :text
+ add_column :spree_stores, :seo_title, :string
+ else
+ create_table :spree_stores do |t|
+ t.string :name
+ t.string :url
+ t.text :meta_description
+ t.text :meta_keywords
+ t.string :seo_title
+ t.string :mail_from_address
+ t.string :default_currency
+ t.string :code
+ t.boolean :default, default: false, null: false
+
+ t.timestamps null: false, precision: 6
+ end
+ end
+ end
+end
diff --git a/core/db/migrate/20140309033438_create_store_from_preferences.rb b/core/db/migrate/20140309033438_create_store_from_preferences.rb
new file mode 100644
index 00000000000..6fd88b30af5
--- /dev/null
+++ b/core/db/migrate/20140309033438_create_store_from_preferences.rb
@@ -0,0 +1,37 @@
+class CreateStoreFromPreferences < ActiveRecord::Migration[4.2]
+ def change
+ # workaround for spree_i18n and Store translations
+ Spree::Store.class_eval do
+ def self.translated?(name)
+ false
+ end
+ end
+
+ preference_store = Spree::Preferences::Store.instance
+ if store = Spree::Store.where(default: true).first
+ store.meta_description = preference_store.get('spree/app_configuration/default_meta_description') {}
+ store.meta_keywords = preference_store.get('spree/app_configuration/default_meta_keywords') {}
+ store.seo_title = preference_store.get('spree/app_configuration/default_seo_title') {}
+ store.save!
+ else
+ # we set defaults for the things we now require
+ Spree::Store.new do |s|
+ s.name = preference_store.get 'spree/app_configuration/site_name' do
+ 'Spree Demo Site'
+ end
+ s.url = preference_store.get 'spree/app_configuration/site_url' do
+ 'demo.spreecommerce.org'
+ end
+ s.mail_from_address = preference_store.get 'spree/app_configuration/mails_from' do
+ 'spree@example.com'
+ end
+
+ s.meta_description = preference_store.get('spree/app_configuration/default_meta_description') {}
+ s.meta_keywords = preference_store.get('spree/app_configuration/default_meta_keywords') {}
+ s.seo_title = preference_store.get('spree/app_configuration/default_seo_title') {}
+ s.default_currency = preference_store.get('spree/app_configuration/currency') {}
+ s.code = 'spree'
+ end.save!
+ end
+ end
+end
diff --git a/core/db/migrate/20140315053743_add_timestamps_to_spree_assets.rb b/core/db/migrate/20140315053743_add_timestamps_to_spree_assets.rb
new file mode 100644
index 00000000000..eefc6877992
--- /dev/null
+++ b/core/db/migrate/20140315053743_add_timestamps_to_spree_assets.rb
@@ -0,0 +1,6 @@
+class AddTimestampsToSpreeAssets < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_assets, :created_at, :datetime
+ add_column :spree_assets, :updated_at, :datetime
+ end
+end
diff --git a/core/db/migrate/20140318191500_create_spree_taxons_promotion_rules.rb b/core/db/migrate/20140318191500_create_spree_taxons_promotion_rules.rb
new file mode 100644
index 00000000000..135828cd91d
--- /dev/null
+++ b/core/db/migrate/20140318191500_create_spree_taxons_promotion_rules.rb
@@ -0,0 +1,8 @@
+class CreateSpreeTaxonsPromotionRules < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_taxons_promotion_rules do |t|
+ t.references :taxon, index: true
+ t.references :promotion_rule, index: true
+ end
+ end
+end
diff --git a/core/db/migrate/20140331100557_add_additional_store_fields.rb b/core/db/migrate/20140331100557_add_additional_store_fields.rb
new file mode 100644
index 00000000000..12630d1ad0e
--- /dev/null
+++ b/core/db/migrate/20140331100557_add_additional_store_fields.rb
@@ -0,0 +1,8 @@
+class AddAdditionalStoreFields < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_stores, :code, :string unless column_exists?(:spree_stores, :code)
+ add_column :spree_stores, :default, :boolean, default: false, null: false unless column_exists?(:spree_stores, :default)
+ add_index :spree_stores, :code
+ add_index :spree_stores, :default
+ end
+end
diff --git a/core/db/migrate/20140410141842_add_many_missing_indexes.rb b/core/db/migrate/20140410141842_add_many_missing_indexes.rb
new file mode 100644
index 00000000000..b4d2a353aec
--- /dev/null
+++ b/core/db/migrate/20140410141842_add_many_missing_indexes.rb
@@ -0,0 +1,18 @@
+class AddManyMissingIndexes < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_adjustments, [:adjustable_id, :adjustable_type]
+ add_index :spree_adjustments, :eligible
+ add_index :spree_adjustments, :order_id
+ add_index :spree_promotions, :code
+ add_index :spree_promotions, :expires_at
+ add_index :spree_states, :country_id
+ add_index :spree_stock_items, :deleted_at
+ add_index :spree_option_types, :position
+ add_index :spree_option_values, :position
+ add_index :spree_product_option_types, :option_type_id
+ add_index :spree_product_option_types, :product_id
+ add_index :spree_products_taxons, :position
+ add_index :spree_promotions, :starts_at
+ add_index :spree_stores, :url
+ end
+end
diff --git a/core/db/migrate/20140410150358_correct_some_polymorphic_index_and_add_more_missing.rb b/core/db/migrate/20140410150358_correct_some_polymorphic_index_and_add_more_missing.rb
new file mode 100644
index 00000000000..a705b84346e
--- /dev/null
+++ b/core/db/migrate/20140410150358_correct_some_polymorphic_index_and_add_more_missing.rb
@@ -0,0 +1,66 @@
+class CorrectSomePolymorphicIndexAndAddMoreMissing < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_addresses, :country_id
+ add_index :spree_addresses, :state_id
+ remove_index :spree_adjustments, [:source_type, :source_id]
+ add_index :spree_adjustments, [:source_id, :source_type]
+ add_index :spree_credit_cards, :address_id
+ add_index :spree_gateways, :active
+ add_index :spree_gateways, :test_mode
+ add_index :spree_inventory_units, :return_authorization_id
+ add_index :spree_line_items, :tax_category_id
+ add_index :spree_log_entries, [:source_id, :source_type]
+ add_index :spree_orders, :approver_id
+ add_index :spree_orders, :bill_address_id
+ add_index :spree_orders, :confirmation_delivered
+ add_index :spree_orders, :considered_risky
+ add_index :spree_orders, :created_by_id
+ add_index :spree_orders, :ship_address_id
+ add_index :spree_orders, :shipping_method_id
+ add_index :spree_orders_promotions, [:order_id, :promotion_id]
+ add_index :spree_payments, [:source_id, :source_type]
+ add_index :spree_prices, :deleted_at
+ add_index :spree_product_option_types, :position
+ add_index :spree_product_properties, :position
+ add_index :spree_product_properties, :property_id
+ add_index :spree_products, :shipping_category_id
+ add_index :spree_products, :tax_category_id
+ add_index :spree_promotion_action_line_items, :promotion_action_id
+ add_index :spree_promotion_action_line_items, :variant_id
+ add_index :spree_promotion_rules, :promotion_id
+ add_index :spree_promotions, :advertise
+ add_index :spree_return_authorizations, :number
+ add_index :spree_return_authorizations, :order_id
+ add_index :spree_return_authorizations, :stock_location_id
+ add_index :spree_shipments, :address_id
+ add_index :spree_shipping_methods, :deleted_at
+ add_index :spree_shipping_methods, :tax_category_id
+ add_index :spree_shipping_rates, :selected
+ add_index :spree_shipping_rates, :tax_rate_id
+ add_index :spree_state_changes, [:stateful_id, :stateful_type]
+ add_index :spree_state_changes, :user_id
+ add_index :spree_stock_items, :backorderable
+ add_index :spree_stock_locations, :active
+ add_index :spree_stock_locations, :backorderable_default
+ add_index :spree_stock_locations, :country_id
+ add_index :spree_stock_locations, :propagate_all_variants
+ add_index :spree_stock_locations, :state_id
+ add_index :spree_tax_categories, :deleted_at
+ add_index :spree_tax_categories, :is_default
+ add_index :spree_tax_rates, :deleted_at
+ add_index :spree_tax_rates, :included_in_price
+ add_index :spree_tax_rates, :show_rate_in_label
+ add_index :spree_tax_rates, :tax_category_id
+ add_index :spree_tax_rates, :zone_id
+ add_index :spree_taxonomies, :position
+ add_index :spree_taxons, :position
+ add_index :spree_trackers, :active
+ add_index :spree_variants, :deleted_at
+ add_index :spree_variants, :is_master
+ add_index :spree_variants, :position
+ add_index :spree_variants, :track_inventory
+ add_index :spree_zone_members, :zone_id
+ add_index :spree_zone_members, [:zoneable_id, :zoneable_type]
+ add_index :spree_zones, :default_tax
+ end
+end
diff --git a/core/db/migrate/20140415041315_add_user_id_created_by_id_index_to_order.rb b/core/db/migrate/20140415041315_add_user_id_created_by_id_index_to_order.rb
new file mode 100644
index 00000000000..318ade50a79
--- /dev/null
+++ b/core/db/migrate/20140415041315_add_user_id_created_by_id_index_to_order.rb
@@ -0,0 +1,5 @@
+class AddUserIdCreatedByIdIndexToOrder < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_orders, [:user_id, :created_by_id]
+ end
+end
diff --git a/core/db/migrate/20140508151342_change_spree_price_amount_precision.rb b/core/db/migrate/20140508151342_change_spree_price_amount_precision.rb
new file mode 100644
index 00000000000..c6ba863e339
--- /dev/null
+++ b/core/db/migrate/20140508151342_change_spree_price_amount_precision.rb
@@ -0,0 +1,8 @@
+class ChangeSpreePriceAmountPrecision < ActiveRecord::Migration[4.2]
+ def change
+ change_column :spree_prices, :amount, :decimal, precision: 10, scale: 2
+ change_column :spree_line_items, :price, :decimal, precision: 10, scale: 2
+ change_column :spree_line_items, :cost_price, :decimal, precision: 10, scale: 2
+ change_column :spree_variants, :cost_price, :decimal, precision: 10, scale: 2
+ end
+end
diff --git a/core/db/migrate/20140518174634_add_token_to_spree_orders.rb b/core/db/migrate/20140518174634_add_token_to_spree_orders.rb
new file mode 100644
index 00000000000..021b89a95fc
--- /dev/null
+++ b/core/db/migrate/20140518174634_add_token_to_spree_orders.rb
@@ -0,0 +1,5 @@
+class AddTokenToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :guest_token, :string
+ end
+end
diff --git a/core/db/migrate/20140530024945_move_order_token_from_tokenized_permission.rb b/core/db/migrate/20140530024945_move_order_token_from_tokenized_permission.rb
new file mode 100644
index 00000000000..5e496ab07cb
--- /dev/null
+++ b/core/db/migrate/20140530024945_move_order_token_from_tokenized_permission.rb
@@ -0,0 +1,29 @@
+class MoveOrderTokenFromTokenizedPermission < ActiveRecord::Migration[4.2]
+ class Spree::TokenizedPermission < Spree::Base
+ belongs_to :permissable, polymorphic: true
+ end
+
+ def up
+ case Spree::Order.connection.adapter_name
+ when 'SQLite'
+ Spree::Order.has_one :tokenized_permission, as: :permissable
+ Spree::Order.includes(:tokenized_permission).each do |o|
+ o.update_column :guest_token, o.tokenized_permission.token
+ end
+ when 'Mysql2', 'MySQL'
+ execute "UPDATE spree_orders, spree_tokenized_permissions
+ SET spree_orders.guest_token = spree_tokenized_permissions.token
+ WHERE spree_tokenized_permissions.permissable_id = spree_orders.id
+ AND spree_tokenized_permissions.permissable_type = 'Spree::Order'"
+ else
+ execute "UPDATE spree_orders
+ SET guest_token = spree_tokenized_permissions.token
+ FROM spree_tokenized_permissions
+ WHERE spree_tokenized_permissions.permissable_id = spree_orders.id
+ AND spree_tokenized_permissions.permissable_type = 'Spree::Order'"
+ end
+ end
+
+ def down
+ end
+end
diff --git a/core/db/migrate/20140601011216_set_shipment_total_for_users_upgrading.rb b/core/db/migrate/20140601011216_set_shipment_total_for_users_upgrading.rb
new file mode 100644
index 00000000000..cc9274dd519
--- /dev/null
+++ b/core/db/migrate/20140601011216_set_shipment_total_for_users_upgrading.rb
@@ -0,0 +1,10 @@
+class SetShipmentTotalForUsersUpgrading < ActiveRecord::Migration[4.2]
+ def up
+ # NOTE You might not need this at all unless you're upgrading from Spree 2.1.x
+ # or below. For those upgrading this should populate the Order#shipment_total
+ # for legacy orders
+ Spree::Order.complete.where('shipment_total = ?', 0).includes(:shipments).find_each do |order|
+ order.update_column(:shipment_total, order.shipments.sum(:cost))
+ end
+ end
+end
diff --git a/core/db/migrate/20140604135309_drop_credit_card_first_name_and_last_name.rb b/core/db/migrate/20140604135309_drop_credit_card_first_name_and_last_name.rb
new file mode 100644
index 00000000000..e06369ca293
--- /dev/null
+++ b/core/db/migrate/20140604135309_drop_credit_card_first_name_and_last_name.rb
@@ -0,0 +1,6 @@
+class DropCreditCardFirstNameAndLastName < ActiveRecord::Migration[4.2]
+ def change
+ remove_column :spree_credit_cards, :first_name, :string
+ remove_column :spree_credit_cards, :last_name, :string
+ end
+end
diff --git a/core/db/migrate/20140609201656_add_deleted_at_to_spree_promotion_actions.rb b/core/db/migrate/20140609201656_add_deleted_at_to_spree_promotion_actions.rb
new file mode 100644
index 00000000000..1addd26dd66
--- /dev/null
+++ b/core/db/migrate/20140609201656_add_deleted_at_to_spree_promotion_actions.rb
@@ -0,0 +1,6 @@
+class AddDeletedAtToSpreePromotionActions < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_promotion_actions, :deleted_at, :datetime
+ add_index :spree_promotion_actions, :deleted_at
+ end
+end
diff --git a/core/db/migrate/20140616202624_remove_uncaptured_amount_from_spree_payments.rb b/core/db/migrate/20140616202624_remove_uncaptured_amount_from_spree_payments.rb
new file mode 100644
index 00000000000..b7c8e8a819f
--- /dev/null
+++ b/core/db/migrate/20140616202624_remove_uncaptured_amount_from_spree_payments.rb
@@ -0,0 +1,5 @@
+class RemoveUncapturedAmountFromSpreePayments < ActiveRecord::Migration[4.2]
+ def change
+ remove_column :spree_payments, :uncaptured_amount
+ end
+end
diff --git a/core/db/migrate/20140625214618_create_spree_refunds.rb b/core/db/migrate/20140625214618_create_spree_refunds.rb
new file mode 100644
index 00000000000..63c90cd5134
--- /dev/null
+++ b/core/db/migrate/20140625214618_create_spree_refunds.rb
@@ -0,0 +1,12 @@
+class CreateSpreeRefunds < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_refunds do |t|
+ t.integer :payment_id
+ t.integer :return_authorization_id
+ t.decimal :amount, precision: 10, scale: 2, default: 0.0, null: false
+ t.string :transaction_id
+
+ t.timestamps null: false, precision: 6
+ end
+ end
+end
diff --git a/core/db/migrate/20140702140656_create_spree_return_authorization_inventory_unit.rb b/core/db/migrate/20140702140656_create_spree_return_authorization_inventory_unit.rb
new file mode 100644
index 00000000000..94d3047c40a
--- /dev/null
+++ b/core/db/migrate/20140702140656_create_spree_return_authorization_inventory_unit.rb
@@ -0,0 +1,12 @@
+class CreateSpreeReturnAuthorizationInventoryUnit < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_return_authorization_inventory_units do |t|
+ t.integer :return_authorization_id
+ t.integer :inventory_unit_id
+ t.integer :exchange_variant_id
+ t.datetime :received_at
+
+ t.timestamps null: false, precision: 6
+ end
+ end
+end
diff --git a/core/db/migrate/20140707125621_rename_return_authorization_inventory_unit_to_return_items.rb b/core/db/migrate/20140707125621_rename_return_authorization_inventory_unit_to_return_items.rb
new file mode 100644
index 00000000000..54385dd0337
--- /dev/null
+++ b/core/db/migrate/20140707125621_rename_return_authorization_inventory_unit_to_return_items.rb
@@ -0,0 +1,5 @@
+class RenameReturnAuthorizationInventoryUnitToReturnItems < ActiveRecord::Migration[4.2]
+ def change
+ rename_table :spree_return_authorization_inventory_units, :spree_return_items
+ end
+end
diff --git a/core/db/migrate/20140709160534_backfill_line_item_pre_tax_amount.rb b/core/db/migrate/20140709160534_backfill_line_item_pre_tax_amount.rb
new file mode 100644
index 00000000000..c43117afd6a
--- /dev/null
+++ b/core/db/migrate/20140709160534_backfill_line_item_pre_tax_amount.rb
@@ -0,0 +1,10 @@
+class BackfillLineItemPreTaxAmount < ActiveRecord::Migration[4.2]
+ def change
+ # set pre_tax_amount to discounted_amount - included_tax_total
+ execute(<<-SQL)
+ UPDATE spree_line_items
+ SET pre_tax_amount = ((price * quantity) + promo_total) - included_tax_total
+ WHERE pre_tax_amount IS NULL;
+ SQL
+ end
+end
diff --git a/core/db/migrate/20140710041921_recreate_spree_return_authorizations.rb b/core/db/migrate/20140710041921_recreate_spree_return_authorizations.rb
new file mode 100644
index 00000000000..eb676e3c9e4
--- /dev/null
+++ b/core/db/migrate/20140710041921_recreate_spree_return_authorizations.rb
@@ -0,0 +1,55 @@
+class RecreateSpreeReturnAuthorizations < ActiveRecord::Migration[4.2]
+ def up
+ # If the app has any legacy return authorizations then rename the table & columns and leave them there
+ # for the spree_legacy_return_authorizations extension to pick up with.
+ # Otherwise just drop the tables/columns as they are no longer used in stock spree. The spree_legacy_return_authorizations
+ # extension will recreate these tables for dev environments & etc as needed.
+ if Spree::ReturnAuthorization.exists?
+ rename_table :spree_return_authorizations, :spree_legacy_return_authorizations
+ rename_column :spree_inventory_units, :return_authorization_id, :legacy_return_authorization_id
+ else
+ drop_table :spree_return_authorizations
+ remove_column :spree_inventory_units, :return_authorization_id
+ end
+
+ Spree::Adjustment.where(source_type: 'Spree::ReturnAuthorization').update_all(source_type: 'Spree::LegacyReturnAuthorization')
+
+ # For now just recreate the table as it was. Future changes to the schema (including dropping "amount") will be coming in a
+ # separate commit.
+ create_table :spree_return_authorizations do |t|
+ t.string "number"
+ t.string "state"
+ t.decimal "amount", precision: 10, scale: 2, default: 0.0, null: false
+ t.integer "order_id"
+ t.text "reason"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "stock_location_id"
+ end
+
+ end
+
+ def down
+ drop_table :spree_return_authorizations
+
+ Spree::Adjustment.where(source_type: 'Spree::LegacyReturnAuthorization').update_all(source_type: 'Spree::ReturnAuthorization')
+
+ if data_source_exists?(:spree_legacy_return_authorizations)
+ rename_table :spree_legacy_return_authorizations, :spree_return_authorizations
+ rename_column :spree_inventory_units, :legacy_return_authorization_id, :return_authorization_id
+ else
+ create_table :spree_return_authorizations do |t|
+ t.string "number"
+ t.string "state"
+ t.decimal "amount", precision: 10, scale: 2, default: 0.0, null: false
+ t.integer "order_id"
+ t.text "reason"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "stock_location_id"
+ end
+ add_column :spree_inventory_units, :return_authorization_id, :integer, after: :shipment_id
+ add_index :spree_inventory_units, :return_authorization_id
+ end
+ end
+end
diff --git a/core/db/migrate/20140710181204_add_amount_fields_to_return_items.rb b/core/db/migrate/20140710181204_add_amount_fields_to_return_items.rb
new file mode 100644
index 00000000000..54f6c2ccea8
--- /dev/null
+++ b/core/db/migrate/20140710181204_add_amount_fields_to_return_items.rb
@@ -0,0 +1,7 @@
+class AddAmountFieldsToReturnItems < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_return_items, :pre_tax_amount, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ add_column :spree_return_items, :included_tax_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ add_column :spree_return_items, :additional_tax_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ end
+end
diff --git a/core/db/migrate/20140710190048_drop_return_authorization_amount.rb b/core/db/migrate/20140710190048_drop_return_authorization_amount.rb
new file mode 100644
index 00000000000..9decc257621
--- /dev/null
+++ b/core/db/migrate/20140710190048_drop_return_authorization_amount.rb
@@ -0,0 +1,5 @@
+class DropReturnAuthorizationAmount < ActiveRecord::Migration[4.2]
+ def change
+ remove_column :spree_return_authorizations, :amount
+ end
+end
diff --git a/core/db/migrate/20140713140455_create_spree_return_authorization_reasons.rb b/core/db/migrate/20140713140455_create_spree_return_authorization_reasons.rb
new file mode 100644
index 00000000000..1dafd6abbca
--- /dev/null
+++ b/core/db/migrate/20140713140455_create_spree_return_authorization_reasons.rb
@@ -0,0 +1,28 @@
+class CreateSpreeReturnAuthorizationReasons < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_return_authorization_reasons do |t|
+ t.string :name
+ t.boolean :active, default: true
+ t.boolean :mutable, default: true
+
+ t.timestamps null: false, precision: 6
+ end
+
+ reversible do |direction|
+ direction.up do
+ Spree::ReturnAuthorizationReason.create!(name: 'Better price available')
+ Spree::ReturnAuthorizationReason.create!(name: 'Missed estimated delivery date')
+ Spree::ReturnAuthorizationReason.create!(name: 'Missing parts or accessories')
+ Spree::ReturnAuthorizationReason.create!(name: 'Damaged/Defective')
+ Spree::ReturnAuthorizationReason.create!(name: 'Different from what was ordered')
+ Spree::ReturnAuthorizationReason.create!(name: 'Different from description')
+ Spree::ReturnAuthorizationReason.create!(name: 'No longer needed/wanted')
+ Spree::ReturnAuthorizationReason.create!(name: 'Accidental order')
+ Spree::ReturnAuthorizationReason.create!(name: 'Unauthorized purchase')
+ end
+ end
+
+ add_column :spree_return_authorizations, :return_authorization_reason_id, :integer
+ add_index :spree_return_authorizations, :return_authorization_reason_id, name: 'index_return_authorizations_on_return_authorization_reason_id'
+ end
+end
diff --git a/core/db/migrate/20140713140527_create_spree_refund_reasons.rb b/core/db/migrate/20140713140527_create_spree_refund_reasons.rb
new file mode 100644
index 00000000000..0c775be96ef
--- /dev/null
+++ b/core/db/migrate/20140713140527_create_spree_refund_reasons.rb
@@ -0,0 +1,14 @@
+class CreateSpreeRefundReasons < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_refund_reasons do |t|
+ t.string :name
+ t.boolean :active, default: true
+ t.boolean :mutable, default: true
+
+ t.timestamps null: false, precision: 6
+ end
+
+ add_column :spree_refunds, :refund_reason_id, :integer
+ add_index :spree_refunds, :refund_reason_id, name: 'index_refunds_on_refund_reason_id'
+ end
+end
diff --git a/core/db/migrate/20140713142214_rename_return_authorization_reason.rb b/core/db/migrate/20140713142214_rename_return_authorization_reason.rb
new file mode 100644
index 00000000000..9166ccd7c73
--- /dev/null
+++ b/core/db/migrate/20140713142214_rename_return_authorization_reason.rb
@@ -0,0 +1,5 @@
+class RenameReturnAuthorizationReason < ActiveRecord::Migration[4.2]
+ def change
+ rename_column :spree_return_authorizations, :reason, :memo
+ end
+end
diff --git a/core/db/migrate/20140715182625_create_spree_promotion_categories.rb b/core/db/migrate/20140715182625_create_spree_promotion_categories.rb
new file mode 100644
index 00000000000..562be6ac70e
--- /dev/null
+++ b/core/db/migrate/20140715182625_create_spree_promotion_categories.rb
@@ -0,0 +1,11 @@
+class CreateSpreePromotionCategories < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_promotion_categories do |t|
+ t.string :name
+ t.timestamps null: false, precision: 6
+ end
+
+ add_column :spree_promotions, :promotion_category_id, :integer
+ add_index :spree_promotions, :promotion_category_id
+ end
+end
diff --git a/core/db/migrate/20140716204111_drop_received_at_on_return_items.rb b/core/db/migrate/20140716204111_drop_received_at_on_return_items.rb
new file mode 100644
index 00000000000..af70d6f2b88
--- /dev/null
+++ b/core/db/migrate/20140716204111_drop_received_at_on_return_items.rb
@@ -0,0 +1,9 @@
+class DropReceivedAtOnReturnItems < ActiveRecord::Migration[4.2]
+ def up
+ remove_column :spree_return_items, :received_at
+ end
+
+ def down
+ add_column :spree_return_items, :received_at, :datetime
+ end
+end
diff --git a/core/db/migrate/20140716212330_add_reception_and_acceptance_status_to_return_items.rb b/core/db/migrate/20140716212330_add_reception_and_acceptance_status_to_return_items.rb
new file mode 100644
index 00000000000..5afebb3f0f5
--- /dev/null
+++ b/core/db/migrate/20140716212330_add_reception_and_acceptance_status_to_return_items.rb
@@ -0,0 +1,6 @@
+class AddReceptionAndAcceptanceStatusToReturnItems < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_return_items, :reception_status, :string
+ add_column :spree_return_items, :acceptance_status, :string
+ end
+end
diff --git a/core/db/migrate/20140717155155_create_default_refund_reason.rb b/core/db/migrate/20140717155155_create_default_refund_reason.rb
new file mode 100644
index 00000000000..c5f0eaae442
--- /dev/null
+++ b/core/db/migrate/20140717155155_create_default_refund_reason.rb
@@ -0,0 +1,9 @@
+class CreateDefaultRefundReason < ActiveRecord::Migration[4.2]
+ def up
+ Spree::RefundReason.create!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false)
+ end
+
+ def down
+ Spree::RefundReason.find_by(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false).destroy
+ end
+end
diff --git a/core/db/migrate/20140717185932_add_default_to_spree_stock_locations.rb b/core/db/migrate/20140717185932_add_default_to_spree_stock_locations.rb
new file mode 100644
index 00000000000..3050837898f
--- /dev/null
+++ b/core/db/migrate/20140717185932_add_default_to_spree_stock_locations.rb
@@ -0,0 +1,7 @@
+class AddDefaultToSpreeStockLocations < ActiveRecord::Migration[4.2]
+ def change
+ unless column_exists? :spree_stock_locations, :default
+ add_column :spree_stock_locations, :default, :boolean, null: false, default: false
+ end
+ end
+end
diff --git a/core/db/migrate/20140718133010_create_spree_customer_returns.rb b/core/db/migrate/20140718133010_create_spree_customer_returns.rb
new file mode 100644
index 00000000000..e3a3dc1abe1
--- /dev/null
+++ b/core/db/migrate/20140718133010_create_spree_customer_returns.rb
@@ -0,0 +1,9 @@
+class CreateSpreeCustomerReturns < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_customer_returns do |t|
+ t.string :number
+ t.integer :stock_location_id
+ t.timestamps null: false, precision: 6
+ end
+ end
+end
diff --git a/core/db/migrate/20140718133349_add_customer_return_id_to_return_item.rb b/core/db/migrate/20140718133349_add_customer_return_id_to_return_item.rb
new file mode 100644
index 00000000000..9266db8251e
--- /dev/null
+++ b/core/db/migrate/20140718133349_add_customer_return_id_to_return_item.rb
@@ -0,0 +1,6 @@
+class AddCustomerReturnIdToReturnItem < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_return_items, :customer_return_id, :integer
+ add_index :spree_return_items, :customer_return_id, name: 'index_return_items_on_customer_return_id'
+ end
+end
diff --git a/core/db/migrate/20140718195325_create_friendly_id_slugs.rb b/core/db/migrate/20140718195325_create_friendly_id_slugs.rb
new file mode 100644
index 00000000000..1b66194bcb2
--- /dev/null
+++ b/core/db/migrate/20140718195325_create_friendly_id_slugs.rb
@@ -0,0 +1,15 @@
+class CreateFriendlyIdSlugs < ActiveRecord::Migration[4.2]
+ def change
+ create_table :friendly_id_slugs do |t|
+ t.string :slug, null: false
+ t.integer :sluggable_id, null: false
+ t.string :sluggable_type, limit: 50
+ t.string :scope
+ t.datetime :created_at
+ end
+ add_index :friendly_id_slugs, :sluggable_id
+ add_index :friendly_id_slugs, [:slug, :sluggable_type]
+ add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], unique: true
+ add_index :friendly_id_slugs, :sluggable_type
+ end
+end
diff --git a/core/db/migrate/20140723004419_rename_spree_refund_return_authorization_id.rb b/core/db/migrate/20140723004419_rename_spree_refund_return_authorization_id.rb
new file mode 100644
index 00000000000..f15e60eb05b
--- /dev/null
+++ b/core/db/migrate/20140723004419_rename_spree_refund_return_authorization_id.rb
@@ -0,0 +1,5 @@
+class RenameSpreeRefundReturnAuthorizationId < ActiveRecord::Migration[4.2]
+ def change
+ rename_column :spree_refunds, :return_authorization_id, :customer_return_id
+ end
+end
diff --git a/core/db/migrate/20140723152808_increase_return_item_pre_tax_amount_precision.rb b/core/db/migrate/20140723152808_increase_return_item_pre_tax_amount_precision.rb
new file mode 100644
index 00000000000..13cbe3f0101
--- /dev/null
+++ b/core/db/migrate/20140723152808_increase_return_item_pre_tax_amount_precision.rb
@@ -0,0 +1,13 @@
+class IncreaseReturnItemPreTaxAmountPrecision < ActiveRecord::Migration[4.2]
+ def up
+ change_column :spree_return_items, :pre_tax_amount, :decimal, precision: 12, scale: 4, default: 0.0, null: false
+ change_column :spree_return_items, :included_tax_total, :decimal, precision: 12, scale: 4, default: 0.0, null: false
+ change_column :spree_return_items, :additional_tax_total, :decimal, precision: 12, scale: 4, default: 0.0, null: false
+ end
+
+ def down
+ change_column :spree_return_items, :pre_tax_amount, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ change_column :spree_return_items, :included_tax_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ change_column :spree_return_items, :additional_tax_total, :decimal, precision: 10, scale: 2, default: 0.0, null: false
+ end
+end
diff --git a/core/db/migrate/20140723214541_copy_product_slugs_to_slug_history.rb b/core/db/migrate/20140723214541_copy_product_slugs_to_slug_history.rb
new file mode 100644
index 00000000000..4eebf6def8e
--- /dev/null
+++ b/core/db/migrate/20140723214541_copy_product_slugs_to_slug_history.rb
@@ -0,0 +1,15 @@
+class CopyProductSlugsToSlugHistory < ActiveRecord::Migration[4.2]
+ def change
+
+ # do what sql does best: copy all slugs into history table in a single query
+ # rather than load potentially millions of products into memory
+ Spree::Product.connection.execute <<-SQL
+INSERT INTO #{FriendlyId::Slug.table_name} (slug, sluggable_id, sluggable_type, created_at)
+ SELECT slug, id, '#{Spree::Product.to_s}', #{ApplicationRecord.send(:sanitize_sql_array, ['?', Time.current])}
+ FROM #{Spree::Product.table_name}
+ WHERE slug IS NOT NULL
+ ORDER BY id
+SQL
+
+ end
+end
diff --git a/core/db/migrate/20140725131539_create_spree_reimbursements.rb b/core/db/migrate/20140725131539_create_spree_reimbursements.rb
new file mode 100644
index 00000000000..a4a44fe9d9a
--- /dev/null
+++ b/core/db/migrate/20140725131539_create_spree_reimbursements.rb
@@ -0,0 +1,21 @@
+class CreateSpreeReimbursements < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_reimbursements do |t|
+ t.string :number
+ t.string :reimbursement_status
+ t.integer :customer_return_id
+ t.integer :order_id
+ t.decimal :total, precision: 10, scale: 2
+
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_reimbursements, :customer_return_id
+ add_index :spree_reimbursements, :order_id
+
+ remove_column :spree_refunds, :customer_return_id, :integer
+ add_column :spree_refunds, :reimbursement_id, :integer
+
+ add_column :spree_return_items, :reimbursement_id, :integer
+ end
+end
diff --git a/core/db/migrate/20140728225422_add_promotionable_to_spree_products.rb b/core/db/migrate/20140728225422_add_promotionable_to_spree_products.rb
new file mode 100644
index 00000000000..3ed9e7fcc74
--- /dev/null
+++ b/core/db/migrate/20140728225422_add_promotionable_to_spree_products.rb
@@ -0,0 +1,5 @@
+class AddPromotionableToSpreeProducts < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_products, :promotionable, :boolean, default: true
+ end
+end
diff --git a/core/db/migrate/20140729133613_add_exchange_inventory_unit_foreign_keys.rb b/core/db/migrate/20140729133613_add_exchange_inventory_unit_foreign_keys.rb
new file mode 100644
index 00000000000..2e981a68bc4
--- /dev/null
+++ b/core/db/migrate/20140729133613_add_exchange_inventory_unit_foreign_keys.rb
@@ -0,0 +1,7 @@
+class AddExchangeInventoryUnitForeignKeys < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_return_items, :exchange_inventory_unit_id, :integer
+
+ add_index :spree_return_items, :exchange_inventory_unit_id
+ end
+end
diff --git a/core/db/migrate/20140730155938_add_acceptance_status_errors_to_return_item.rb b/core/db/migrate/20140730155938_add_acceptance_status_errors_to_return_item.rb
new file mode 100644
index 00000000000..abf8007b285
--- /dev/null
+++ b/core/db/migrate/20140730155938_add_acceptance_status_errors_to_return_item.rb
@@ -0,0 +1,5 @@
+class AddAcceptanceStatusErrorsToReturnItem < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_return_items, :acceptance_status_errors, :text
+ end
+end
diff --git a/core/db/migrate/20140731150017_create_spree_reimbursement_types.rb b/core/db/migrate/20140731150017_create_spree_reimbursement_types.rb
new file mode 100644
index 00000000000..3a8c4a0019a
--- /dev/null
+++ b/core/db/migrate/20140731150017_create_spree_reimbursement_types.rb
@@ -0,0 +1,20 @@
+class CreateSpreeReimbursementTypes < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_reimbursement_types do |t|
+ t.string :name
+ t.boolean :active, default: true
+ t.boolean :mutable, default: true
+
+ t.timestamps null: false, precision: 6
+ end
+
+ reversible do |direction|
+ direction.up do
+ Spree::ReimbursementType.create!(name: Spree::ReimbursementType::ORIGINAL)
+ end
+ end
+
+ add_column :spree_return_items, :preferred_reimbursement_type_id, :integer
+ add_column :spree_return_items, :override_reimbursement_type_id, :integer
+ end
+end
diff --git a/core/db/migrate/20140804185157_add_default_to_shipment_cost.rb b/core/db/migrate/20140804185157_add_default_to_shipment_cost.rb
new file mode 100644
index 00000000000..850757e7be7
--- /dev/null
+++ b/core/db/migrate/20140804185157_add_default_to_shipment_cost.rb
@@ -0,0 +1,10 @@
+class AddDefaultToShipmentCost < ActiveRecord::Migration[4.2]
+ def up
+ change_column :spree_shipments, :cost, :decimal, precision: 10, scale: 2, default: 0.0
+ Spree::Shipment.where(cost: nil).update_all(cost: 0)
+ end
+
+ def down
+ change_column :spree_shipments, :cost, :decimal, precision: 10, scale: 2
+ end
+end
diff --git a/core/db/migrate/20140805171035_add_default_to_spree_credit_cards.rb b/core/db/migrate/20140805171035_add_default_to_spree_credit_cards.rb
new file mode 100644
index 00000000000..853303ed55c
--- /dev/null
+++ b/core/db/migrate/20140805171035_add_default_to_spree_credit_cards.rb
@@ -0,0 +1,5 @@
+class AddDefaultToSpreeCreditCards < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_credit_cards, :default, :boolean, null: false, default: false
+ end
+end
diff --git a/core/db/migrate/20140805171219_make_existing_credit_cards_default.rb b/core/db/migrate/20140805171219_make_existing_credit_cards_default.rb
new file mode 100644
index 00000000000..4d50d96f963
--- /dev/null
+++ b/core/db/migrate/20140805171219_make_existing_credit_cards_default.rb
@@ -0,0 +1,10 @@
+class MakeExistingCreditCardsDefault < ActiveRecord::Migration[4.2]
+ def up
+ # set the newest credit card for every user to be the default; SQL technique from
+ # http://stackoverflow.com/questions/121387/fetch-the-row-which-has-the-max-value-for-a-column
+ Spree::CreditCard.where.not(user_id: nil).joins("LEFT OUTER JOIN spree_credit_cards cc2 ON cc2.user_id = spree_credit_cards.user_id AND spree_credit_cards.created_at < cc2.created_at").where("cc2.user_id IS NULL").update_all(default: true)
+ end
+ def down
+ # do nothing
+ end
+end
diff --git a/core/db/migrate/20140806144901_add_type_to_reimbursement_type.rb b/core/db/migrate/20140806144901_add_type_to_reimbursement_type.rb
new file mode 100644
index 00000000000..d1f8aa1da70
--- /dev/null
+++ b/core/db/migrate/20140806144901_add_type_to_reimbursement_type.rb
@@ -0,0 +1,9 @@
+class AddTypeToReimbursementType < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_reimbursement_types, :type, :string
+ add_index :spree_reimbursement_types, :type
+
+ Spree::ReimbursementType.reset_column_information
+ Spree::ReimbursementType.find_by(name: Spree::ReimbursementType::ORIGINAL).update_attributes!(type: 'Spree::ReimbursementType::OriginalPayment')
+ end
+end
diff --git a/core/db/migrate/20140808184039_create_spree_reimbursement_credits.rb b/core/db/migrate/20140808184039_create_spree_reimbursement_credits.rb
new file mode 100644
index 00000000000..0669db26e7d
--- /dev/null
+++ b/core/db/migrate/20140808184039_create_spree_reimbursement_credits.rb
@@ -0,0 +1,10 @@
+class CreateSpreeReimbursementCredits < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_reimbursement_credits do |t|
+ t.decimal :amount, precision: 10, scale: 2, default: 0.0, null: false
+ t.integer :reimbursement_id
+ t.integer :creditable_id
+ t.string :creditable_type
+ end
+ end
+end
diff --git a/core/db/migrate/20140827170513_add_meta_title_to_spree_products.rb b/core/db/migrate/20140827170513_add_meta_title_to_spree_products.rb
new file mode 100644
index 00000000000..338ec23e910
--- /dev/null
+++ b/core/db/migrate/20140827170513_add_meta_title_to_spree_products.rb
@@ -0,0 +1,7 @@
+class AddMetaTitleToSpreeProducts < ActiveRecord::Migration[4.2]
+ def change
+ change_table :spree_products do |t|
+ t.string :meta_title
+ end
+ end
+end
diff --git a/core/db/migrate/20140911173301_add_kind_to_zone.rb b/core/db/migrate/20140911173301_add_kind_to_zone.rb
new file mode 100644
index 00000000000..e04536447ae
--- /dev/null
+++ b/core/db/migrate/20140911173301_add_kind_to_zone.rb
@@ -0,0 +1,11 @@
+class AddKindToZone < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_zones, :kind, :string
+ add_index :spree_zones, :kind
+
+ Spree::Zone.find_each do |zone|
+ last_type = zone.members.where.not(zoneable_type: nil).pluck(:zoneable_type).last
+ zone.update_column :kind, last_type.demodulize.underscore if last_type
+ end
+ end
+end
diff --git a/core/db/migrate/20140924164824_add_code_to_spree_tax_categories.rb b/core/db/migrate/20140924164824_add_code_to_spree_tax_categories.rb
new file mode 100644
index 00000000000..755dca8dfc9
--- /dev/null
+++ b/core/db/migrate/20140924164824_add_code_to_spree_tax_categories.rb
@@ -0,0 +1,5 @@
+class AddCodeToSpreeTaxCategories < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_tax_categories, :tax_code, :string
+ end
+end
diff --git a/core/db/migrate/20140927193717_default_pre_tax_amount_should_be_zero.rb b/core/db/migrate/20140927193717_default_pre_tax_amount_should_be_zero.rb
new file mode 100644
index 00000000000..c0d2e6185c1
--- /dev/null
+++ b/core/db/migrate/20140927193717_default_pre_tax_amount_should_be_zero.rb
@@ -0,0 +1,6 @@
+class DefaultPreTaxAmountShouldBeZero < ActiveRecord::Migration[4.2]
+ def change
+ change_column :spree_line_items, :pre_tax_amount, :decimal, precision: 8, scale: 2, default: 0
+ change_column :spree_shipments, :pre_tax_amount, :decimal, precision: 8, scale: 2, default: 0
+ end
+end
diff --git a/core/db/migrate/20141002191113_add_code_to_spree_shipping_methods.rb b/core/db/migrate/20141002191113_add_code_to_spree_shipping_methods.rb
new file mode 100644
index 00000000000..ce92d70593d
--- /dev/null
+++ b/core/db/migrate/20141002191113_add_code_to_spree_shipping_methods.rb
@@ -0,0 +1,5 @@
+class AddCodeToSpreeShippingMethods < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_shipping_methods, :code, :string
+ end
+end
diff --git a/core/db/migrate/20141007230328_add_cancel_audit_fields_to_spree_orders.rb b/core/db/migrate/20141007230328_add_cancel_audit_fields_to_spree_orders.rb
new file mode 100644
index 00000000000..cc316f2e693
--- /dev/null
+++ b/core/db/migrate/20141007230328_add_cancel_audit_fields_to_spree_orders.rb
@@ -0,0 +1,6 @@
+class AddCancelAuditFieldsToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :canceled_at, :datetime
+ add_column :spree_orders, :canceler_id, :integer
+ end
+end
diff --git a/core/db/migrate/20141009204607_add_store_id_to_orders.rb b/core/db/migrate/20141009204607_add_store_id_to_orders.rb
new file mode 100644
index 00000000000..a73c4ee0aac
--- /dev/null
+++ b/core/db/migrate/20141009204607_add_store_id_to_orders.rb
@@ -0,0 +1,8 @@
+class AddStoreIdToOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :store_id, :integer
+ if Spree::Store.default.persisted?
+ Spree::Order.where(store_id: nil).update_all(store_id: Spree::Store.default.id)
+ end
+ end
+end
diff --git a/core/db/migrate/20141012083513_create_spree_taxons_prototypes.rb b/core/db/migrate/20141012083513_create_spree_taxons_prototypes.rb
new file mode 100644
index 00000000000..b83f5545140
--- /dev/null
+++ b/core/db/migrate/20141012083513_create_spree_taxons_prototypes.rb
@@ -0,0 +1,8 @@
+class CreateSpreeTaxonsPrototypes < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_taxons_prototypes do |t|
+ t.belongs_to :taxon, index: true
+ t.belongs_to :prototype, index: true
+ end
+ end
+end
diff --git a/core/db/migrate/20141021194502_add_state_lock_version_to_order.rb b/core/db/migrate/20141021194502_add_state_lock_version_to_order.rb
new file mode 100644
index 00000000000..9d04d8c1870
--- /dev/null
+++ b/core/db/migrate/20141021194502_add_state_lock_version_to_order.rb
@@ -0,0 +1,5 @@
+class AddStateLockVersionToOrder < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_orders, :state_lock_version, :integer, default: 0, null: false
+ end
+end
diff --git a/core/db/migrate/20141023005240_add_counter_cache_from_spree_variants_to_spree_stock_items.rb b/core/db/migrate/20141023005240_add_counter_cache_from_spree_variants_to_spree_stock_items.rb
new file mode 100644
index 00000000000..9d3480c0088
--- /dev/null
+++ b/core/db/migrate/20141023005240_add_counter_cache_from_spree_variants_to_spree_stock_items.rb
@@ -0,0 +1,8 @@
+class AddCounterCacheFromSpreeVariantsToSpreeStockItems < ActiveRecord::Migration[4.2]
+ # This was unnecessary and was removed
+ def up
+ end
+
+ def down
+ end
+end
diff --git a/core/db/migrate/20141101231208_fix_adjustment_order_presence.rb b/core/db/migrate/20141101231208_fix_adjustment_order_presence.rb
new file mode 100644
index 00000000000..f84a84bc5a7
--- /dev/null
+++ b/core/db/migrate/20141101231208_fix_adjustment_order_presence.rb
@@ -0,0 +1,13 @@
+class FixAdjustmentOrderPresence < ActiveRecord::Migration[4.2]
+ def change
+ say 'Fixing adjustments without direct order reference'
+ Spree::Adjustment.where(order: nil).find_each do |adjustment|
+ adjustable = adjustment.adjustable
+ if adjustable.is_a? Spree::Order
+ adjustment.update_attributes!(order_id: adjustable.id)
+ else
+ adjustment.update_attributes!(adjustable: adjustable.order)
+ end
+ end
+ end
+end
diff --git a/core/db/migrate/20141105213646_update_classifications_positions.rb b/core/db/migrate/20141105213646_update_classifications_positions.rb
new file mode 100644
index 00000000000..046b2bd30a8
--- /dev/null
+++ b/core/db/migrate/20141105213646_update_classifications_positions.rb
@@ -0,0 +1,9 @@
+class UpdateClassificationsPositions < ActiveRecord::Migration[4.2]
+ def up
+ Spree::Taxon.all.each do |taxon|
+ taxon.classifications.each_with_index do |c12n, i|
+ c12n.set_list_position(i + 1)
+ end
+ end
+ end
+end
diff --git a/core/db/migrate/20141120135441_add_guest_token_index_to_spree_orders.rb b/core/db/migrate/20141120135441_add_guest_token_index_to_spree_orders.rb
new file mode 100644
index 00000000000..9824e515d1b
--- /dev/null
+++ b/core/db/migrate/20141120135441_add_guest_token_index_to_spree_orders.rb
@@ -0,0 +1,5 @@
+class AddGuestTokenIndexToSpreeOrders < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_orders, :guest_token
+ end
+end
diff --git a/core/db/migrate/20141215232040_remove_token_permissions_table.rb b/core/db/migrate/20141215232040_remove_token_permissions_table.rb
new file mode 100644
index 00000000000..4ec659fc63b
--- /dev/null
+++ b/core/db/migrate/20141215232040_remove_token_permissions_table.rb
@@ -0,0 +1,6 @@
+class RemoveTokenPermissionsTable < ActiveRecord::Migration[4.2]
+ def change
+ # The MoveOrderTokenFromTokenizedPermission migration never dropped this.
+ drop_table :spree_tokenized_permissions
+ end
+end
diff --git a/core/db/migrate/20141215235502_remove_extra_products_slug_index.rb b/core/db/migrate/20141215235502_remove_extra_products_slug_index.rb
new file mode 100644
index 00000000000..0ca64503cad
--- /dev/null
+++ b/core/db/migrate/20141215235502_remove_extra_products_slug_index.rb
@@ -0,0 +1,5 @@
+class RemoveExtraProductsSlugIndex < ActiveRecord::Migration[4.2]
+ def change
+ remove_index :spree_products, name: :permalink_idx_unique
+ end
+end
diff --git a/core/db/migrate/20141217215630_update_product_slug_index.rb b/core/db/migrate/20141217215630_update_product_slug_index.rb
new file mode 100644
index 00000000000..20a3c150c1d
--- /dev/null
+++ b/core/db/migrate/20141217215630_update_product_slug_index.rb
@@ -0,0 +1,6 @@
+class UpdateProductSlugIndex < ActiveRecord::Migration[4.2]
+ def change
+ remove_index :spree_products, :slug if index_exists?(:spree_products, :slug)
+ add_index :spree_products, :slug, unique: true
+ end
+end
diff --git a/core/db/migrate/20141218025915_rename_identifier_to_number_for_payment.rb b/core/db/migrate/20141218025915_rename_identifier_to_number_for_payment.rb
new file mode 100644
index 00000000000..6a763e6be4e
--- /dev/null
+++ b/core/db/migrate/20141218025915_rename_identifier_to_number_for_payment.rb
@@ -0,0 +1,5 @@
+class RenameIdentifierToNumberForPayment < ActiveRecord::Migration[4.2]
+ def change
+ rename_column :spree_payments, :identifier, :number
+ end
+end
diff --git a/core/db/migrate/20150118210639_create_spree_store_credits.rb b/core/db/migrate/20150118210639_create_spree_store_credits.rb
new file mode 100644
index 00000000000..6be5cdf0b52
--- /dev/null
+++ b/core/db/migrate/20150118210639_create_spree_store_credits.rb
@@ -0,0 +1,24 @@
+class CreateSpreeStoreCredits < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_store_credits do |t|
+ t.references :user
+ t.references :category
+ t.references :created_by
+ t.decimal :amount, precision: 8, scale: 2, default: 0.0, null: false
+ t.decimal :amount_used, precision: 8, scale: 2, default: 0.0, null: false
+ t.text :memo
+ t.datetime :deleted_at
+ t.string :currency
+ t.decimal :amount_authorized, precision: 8, scale: 2, default: 0.0, null: false
+ t.integer :originator_id
+ t.string :originator_type
+ t.integer :type_id
+ t.timestamps null: false, precision: 6
+ end
+
+ add_index :spree_store_credits, :deleted_at
+ add_index :spree_store_credits, :user_id
+ add_index :spree_store_credits, :type_id
+ add_index :spree_store_credits, [:originator_id, :originator_type], name: :spree_store_credits_originator
+ end
+end
diff --git a/core/db/migrate/20150118211500_create_spree_store_credit_categories.rb b/core/db/migrate/20150118211500_create_spree_store_credit_categories.rb
new file mode 100644
index 00000000000..342ba69068c
--- /dev/null
+++ b/core/db/migrate/20150118211500_create_spree_store_credit_categories.rb
@@ -0,0 +1,8 @@
+class CreateSpreeStoreCreditCategories < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_store_credit_categories do |t|
+ t.string :name
+ t.timestamps null: false, precision: 6
+ end
+ end
+end
diff --git a/core/db/migrate/20150118212051_create_spree_store_credit_events.rb b/core/db/migrate/20150118212051_create_spree_store_credit_events.rb
new file mode 100644
index 00000000000..54038f8ff1b
--- /dev/null
+++ b/core/db/migrate/20150118212051_create_spree_store_credit_events.rb
@@ -0,0 +1,17 @@
+class CreateSpreeStoreCreditEvents < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_store_credit_events do |t|
+ t.integer :store_credit_id, null: false
+ t.string :action, null: false
+ t.decimal :amount, precision: 8, scale: 2
+ t.string :authorization_code, null: false
+ t.decimal :user_total_amount, precision: 8, scale: 2, default: 0.0, null: false
+ t.integer :originator_id
+ t.string :originator_type
+ t.datetime :deleted_at
+ t.timestamps null: false, precision: 6
+ end
+ add_index :spree_store_credit_events, :store_credit_id
+ add_index :spree_store_credit_events, [:originator_id, :originator_type], name: :spree_store_credit_events_originator
+ end
+end
diff --git a/core/db/migrate/20150118212101_create_spree_store_credit_types.rb b/core/db/migrate/20150118212101_create_spree_store_credit_types.rb
new file mode 100644
index 00000000000..894f17ee0dd
--- /dev/null
+++ b/core/db/migrate/20150118212101_create_spree_store_credit_types.rb
@@ -0,0 +1,10 @@
+class CreateSpreeStoreCreditTypes < ActiveRecord::Migration[4.2]
+ def change
+ create_table :spree_store_credit_types do |t|
+ t.string :name
+ t.integer :priority
+ t.timestamps null: false, precision: 6
+ end
+ add_index :spree_store_credit_types, :priority
+ end
+end
diff --git a/core/db/migrate/20150121022521_remove_environment_from_payment_method.rb b/core/db/migrate/20150121022521_remove_environment_from_payment_method.rb
new file mode 100644
index 00000000000..eac53ccd719
--- /dev/null
+++ b/core/db/migrate/20150121022521_remove_environment_from_payment_method.rb
@@ -0,0 +1,6 @@
+class RemoveEnvironmentFromPaymentMethod < ActiveRecord::Migration[4.2]
+ def up
+ Spree::PaymentMethod.where('environment != ?', Rails.env).update_all(active: false)
+ remove_column :spree_payment_methods, :environment
+ end
+end
diff --git a/core/db/migrate/20150122145607_add_resellable_to_return_items.rb b/core/db/migrate/20150122145607_add_resellable_to_return_items.rb
new file mode 100644
index 00000000000..a9acd96ea7a
--- /dev/null
+++ b/core/db/migrate/20150122145607_add_resellable_to_return_items.rb
@@ -0,0 +1,5 @@
+class AddResellableToReturnItems < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_return_items, :resellable, :boolean, default: true, null: false
+ end
+end
diff --git a/core/db/migrate/20150122202432_add_code_to_spree_promotion_categories.rb b/core/db/migrate/20150122202432_add_code_to_spree_promotion_categories.rb
new file mode 100644
index 00000000000..f6b640b2c8a
--- /dev/null
+++ b/core/db/migrate/20150122202432_add_code_to_spree_promotion_categories.rb
@@ -0,0 +1,5 @@
+class AddCodeToSpreePromotionCategories < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_promotion_categories, :code, :string
+ end
+end
diff --git a/core/db/migrate/20150128032538_remove_environment_from_tracker.rb b/core/db/migrate/20150128032538_remove_environment_from_tracker.rb
new file mode 100644
index 00000000000..ed697a46066
--- /dev/null
+++ b/core/db/migrate/20150128032538_remove_environment_from_tracker.rb
@@ -0,0 +1,8 @@
+class RemoveEnvironmentFromTracker < ActiveRecord::Migration[4.2]
+ class Spree::Tracker < Spree::Base; end
+
+ def up
+ Spree::Tracker.where('environment != ?', Rails.env).update_all(active: false)
+ remove_column :spree_trackers, :environment
+ end
+end
diff --git a/core/db/migrate/20150128060325_remove_spree_configurations.rb b/core/db/migrate/20150128060325_remove_spree_configurations.rb
new file mode 100644
index 00000000000..e37025aeef1
--- /dev/null
+++ b/core/db/migrate/20150128060325_remove_spree_configurations.rb
@@ -0,0 +1,16 @@
+class RemoveSpreeConfigurations < ActiveRecord::Migration[4.2]
+ def up
+ drop_table "spree_configurations"
+ end
+
+ def down
+ create_table "spree_configurations", force: true do |t|
+ t.string "name"
+ t.string "type", limit: 50
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "spree_configurations", ["name", "type"], name: "index_spree_configurations_on_name_and_type"
+ end
+end
diff --git a/core/db/migrate/20150216173445_add_index_to_spree_stock_items_variant_id.rb b/core/db/migrate/20150216173445_add_index_to_spree_stock_items_variant_id.rb
new file mode 100644
index 00000000000..45924d80ea7
--- /dev/null
+++ b/core/db/migrate/20150216173445_add_index_to_spree_stock_items_variant_id.rb
@@ -0,0 +1,13 @@
+class AddIndexToSpreeStockItemsVariantId < ActiveRecord::Migration[4.2]
+ def up
+ unless index_exists? :spree_stock_items, :variant_id
+ add_index :spree_stock_items, :variant_id
+ end
+ end
+
+ def down
+ if index_exists? :spree_stock_items, :variant_id
+ remove_index :spree_stock_items, :variant_id
+ end
+ end
+end
diff --git a/core/db/migrate/20150309161154_ensure_payments_have_numbers.rb b/core/db/migrate/20150309161154_ensure_payments_have_numbers.rb
new file mode 100644
index 00000000000..fe39b0945d7
--- /dev/null
+++ b/core/db/migrate/20150309161154_ensure_payments_have_numbers.rb
@@ -0,0 +1,13 @@
+class EnsurePaymentsHaveNumbers < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_payments, :number unless index_exists?(:spree_payments, :number)
+ Spree::Payment.where(number: nil).find_each do |payment|
+ begin
+ payment.save! # to generate a new number we need to save the record
+ rescue ActiveRecord::RecordNotSaved
+ Rails.logger.error("Payment with ID = #{payment.id} couldn't be saved")
+ Rails.logger.error(payment.errors.full_messages.to_sentence)
+ end
+ end
+ end
+end
diff --git a/core/db/migrate/20150314013438_add_missing_indexes_on_spree_tables.rb b/core/db/migrate/20150314013438_add_missing_indexes_on_spree_tables.rb
new file mode 100644
index 00000000000..e851ee3084d
--- /dev/null
+++ b/core/db/migrate/20150314013438_add_missing_indexes_on_spree_tables.rb
@@ -0,0 +1,67 @@
+class AddMissingIndexesOnSpreeTables < ActiveRecord::Migration[4.2]
+ def change
+ if data_source_exists?(:spree_promotion_rules_users) && !index_exists?(:spree_promotion_rules_users,
+ [:user_id, :promotion_rule_id],
+ name: 'index_promotion_rules_users_on_user_id_and_promotion_rule_id')
+ add_index :spree_promotion_rules_users,
+ [:user_id, :promotion_rule_id],
+ name: 'index_promotion_rules_users_on_user_id_and_promotion_rule_id'
+ end
+
+ if data_source_exists?(:spree_products_promotion_rules) && !index_exists?(:spree_products_promotion_rules,
+ [:promotion_rule_id, :product_id],
+ name: 'index_products_promotion_rules_on_promotion_rule_and_product')
+ add_index :spree_products_promotion_rules,
+ [:promotion_rule_id, :product_id],
+ name: 'index_products_promotion_rules_on_promotion_rule_and_product'
+ end
+
+ unless index_exists? :spree_orders, :canceler_id
+ add_index :spree_orders, :canceler_id
+ end
+
+ unless index_exists? :spree_orders, :store_id
+ add_index :spree_orders, :store_id
+ end
+
+ if data_source_exists?(:spree_orders_promotions) && !index_exists?(:spree_orders_promotions, [:promotion_id, :order_id])
+ add_index :spree_orders_promotions, [:promotion_id, :order_id]
+ end
+
+ if data_source_exists?(:spree_properties_prototypes) && !index_exists?(:spree_properties_prototypes, :prototype_id)
+ add_index :spree_properties_prototypes, :prototype_id
+ end
+
+ if data_source_exists?(:spree_properties_prototypes) && !index_exists?(:spree_properties_prototypes,
+ [:prototype_id, :property_id],
+ name: 'index_properties_prototypes_on_prototype_and_property')
+ add_index :spree_properties_prototypes,
+ [:prototype_id, :property_id],
+ name: 'index_properties_prototypes_on_prototype_and_property'
+ end
+
+ if data_source_exists?(:spree_taxons_prototypes) && !index_exists?(:spree_taxons_prototypes, [:prototype_id, :taxon_id])
+ add_index :spree_taxons_prototypes, [:prototype_id, :taxon_id]
+ end
+
+ if data_source_exists?(:spree_option_types_prototypes) && !index_exists?(:spree_option_types_prototypes, :prototype_id)
+ add_index :spree_option_types_prototypes, :prototype_id
+ end
+
+ if data_source_exists?(:spree_option_types_prototypes) && !index_exists?(:spree_option_types_prototypes,
+ [:prototype_id, :option_type_id],
+ name: 'index_option_types_prototypes_on_prototype_and_option_type')
+ add_index :spree_option_types_prototypes,
+ [:prototype_id, :option_type_id],
+ name: 'index_option_types_prototypes_on_prototype_and_option_type'
+ end
+
+ if data_source_exists?(:spree_option_values_variants) && !index_exists?(:spree_option_values_variants,
+ [:option_value_id, :variant_id],
+ name: 'index_option_values_variants_on_option_value_and_variant')
+ add_index :spree_option_values_variants,
+ [:option_value_id, :variant_id],
+ name: 'index_option_values_variants_on_option_value_and_variant'
+ end
+ end
+end
diff --git a/core/db/migrate/20150317174308_remove_duplicated_indexes_from_multi_columns.rb b/core/db/migrate/20150317174308_remove_duplicated_indexes_from_multi_columns.rb
new file mode 100644
index 00000000000..77f3d96ce22
--- /dev/null
+++ b/core/db/migrate/20150317174308_remove_duplicated_indexes_from_multi_columns.rb
@@ -0,0 +1,18 @@
+class RemoveDuplicatedIndexesFromMultiColumns < ActiveRecord::Migration[4.2]
+ def change
+ remove_index :spree_adjustments, name: "index_adjustments_on_order_id"
+ remove_index :spree_option_types_prototypes, :prototype_id
+ add_index :spree_option_types_prototypes, :option_type_id
+ remove_index :spree_option_values_variants, name: 'index_option_values_variants_on_option_value_and_variant'
+ remove_index :spree_option_values_variants, :variant_id
+ add_index :spree_option_values_variants, :option_value_id
+ remove_index :spree_orders, :user_id
+ remove_index :spree_orders_promotions, [:order_id, :promotion_id]
+ add_index :spree_orders_promotions, :order_id
+ remove_index :spree_products_promotion_rules, name: "index_products_promotion_rules_on_promotion_rule_id"
+ remove_index :spree_promotion_rules_users, name: "index_promotion_rules_users_on_user_id"
+ remove_index :spree_properties_prototypes, :prototype_id
+ remove_index :spree_stock_items, :stock_location_id
+ remove_index :spree_taxons_prototypes, :prototype_id
+ end
+end
diff --git a/core/db/migrate/20150324104002_remove_user_index_from_spree_state_changes.rb b/core/db/migrate/20150324104002_remove_user_index_from_spree_state_changes.rb
new file mode 100644
index 00000000000..c8688b255bc
--- /dev/null
+++ b/core/db/migrate/20150324104002_remove_user_index_from_spree_state_changes.rb
@@ -0,0 +1,14 @@
+class RemoveUserIndexFromSpreeStateChanges < ActiveRecord::Migration[4.2]
+ def up
+ if index_exists? :spree_state_changes, :user_id
+ remove_index :spree_state_changes, :user_id
+ end
+
+ end
+
+ def down
+ unless index_exists? :spree_state_changes, :user_id
+ add_index :spree_state_changes, :user_id
+ end
+ end
+end
diff --git a/core/db/migrate/20150515211137_fix_adjustment_order_id.rb b/core/db/migrate/20150515211137_fix_adjustment_order_id.rb
new file mode 100644
index 00000000000..207729c4d8a
--- /dev/null
+++ b/core/db/migrate/20150515211137_fix_adjustment_order_id.rb
@@ -0,0 +1,70 @@
+class FixAdjustmentOrderId < ActiveRecord::Migration[4.2]
+ def change
+ say 'Populate order_id from adjustable_id where appropriate'
+ execute(<<-SQL.squish)
+ UPDATE
+ spree_adjustments
+ SET
+ order_id = adjustable_id
+ WHERE
+ adjustable_type = 'Spree::Order'
+ ;
+ SQL
+
+ # Submitter of change does not care about MySQL, as it is not officially supported.
+ # Still spree officials decided to provide a working code path for MySQL users, hence
+ # submitter made a AR code path he could validate on PostgreSQL.
+ #
+ # Whoever runs a big enough MySQL installation where the AR solution hurts:
+ # Will have to write a better MySQL specific equivalent.
+ if Spree::Order.connection.adapter_name.eql?('MySQL')
+ Spree::Adjustment.where(adjustable_type: 'Spree::LineItem').find_each do |adjustment|
+ adjustment.update_columns(order_id: Spree::LineItem.find(adjustment.adjustable_id).order_id)
+ end
+ else
+ execute(<<-SQL.squish)
+ UPDATE
+ spree_adjustments
+ SET
+ order_id =
+ (SELECT order_id FROM spree_line_items WHERE spree_line_items.id = spree_adjustments.adjustable_id)
+ WHERE
+ adjustable_type = 'Spree::LineItem'
+ SQL
+ end
+
+ say 'Fix schema for spree_adjustments order_id column'
+ change_table :spree_adjustments do |t|
+ t.change :order_id, :integer, null: false
+ end
+
+ # Improved schema for postgresql, uncomment if you like it:
+ #
+ # # Negated Logical implication.
+ # #
+ # # When adjustable_type is 'Spree::Order' (p) the adjustable_id must be order_id (q).
+ # #
+ # # When adjustable_type is NOT 'Spree::Order' the adjustable id allowed to be any value (including of order_id in
+ # # case foreign keys match). XOR does not work here.
+ # #
+ # # Postgresql does not have an operator for logical implication. So we need to build the following truth table
+ # # via AND with OR:
+ # #
+ # # p q | CHECK = !(p -> q)
+ # # -----------
+ # # t t | t
+ # # t f | f
+ # # f t | t
+ # # f f | t
+ # #
+ # # According to de-morgans law the logical implication q -> p is equivalent to !p || q
+ # #
+ # execute(<<-SQL.squish)
+ # ALTER TABLE ONLY spree_adjustments
+ # ADD CONSTRAINT fk_spree_adjustments FOREIGN KEY (order_id)
+ # REFERENCES spree_orders(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
+ # ADD CONSTRAINT check_spree_adjustments_order_id CHECK
+ # (adjustable_type <> 'Spree::Order' OR order_id = adjustable_id);
+ # SQL
+ end
+end
diff --git a/core/db/migrate/20150522071831_add_position_to_spree_payment_methods.rb b/core/db/migrate/20150522071831_add_position_to_spree_payment_methods.rb
new file mode 100644
index 00000000000..07515d38583
--- /dev/null
+++ b/core/db/migrate/20150522071831_add_position_to_spree_payment_methods.rb
@@ -0,0 +1,5 @@
+class AddPositionToSpreePaymentMethods < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_payment_methods, :position, :integer, default: 0
+ end
+end
diff --git a/core/db/migrate/20150522181728_add_deleted_at_to_friendly_id_slugs.rb b/core/db/migrate/20150522181728_add_deleted_at_to_friendly_id_slugs.rb
new file mode 100644
index 00000000000..0cfd37fa007
--- /dev/null
+++ b/core/db/migrate/20150522181728_add_deleted_at_to_friendly_id_slugs.rb
@@ -0,0 +1,6 @@
+class AddDeletedAtToFriendlyIdSlugs < ActiveRecord::Migration[4.2]
+ def change
+ add_column :friendly_id_slugs, :deleted_at, :datetime
+ add_index :friendly_id_slugs, :deleted_at
+ end
+end
diff --git a/core/db/migrate/20150609093816_increase_scale_on_pre_tax_amounts.rb b/core/db/migrate/20150609093816_increase_scale_on_pre_tax_amounts.rb
new file mode 100644
index 00000000000..7c1ed07d035
--- /dev/null
+++ b/core/db/migrate/20150609093816_increase_scale_on_pre_tax_amounts.rb
@@ -0,0 +1,16 @@
+class IncreaseScaleOnPreTaxAmounts < ActiveRecord::Migration[4.2]
+ def change
+ # set pre_tax_amount on shipments to discounted_amount - included_tax_total
+ # so that the null: false option on the shipment pre_tax_amount doesn't generate
+ # errors.
+ #
+ execute(<<-SQL)
+ UPDATE spree_shipments
+ SET pre_tax_amount = (cost + promo_total) - included_tax_total
+ WHERE pre_tax_amount IS NULL;
+ SQL
+
+ change_column :spree_line_items, :pre_tax_amount, :decimal, precision: 12, scale: 4, default: 0.0, null: false
+ change_column :spree_shipments, :pre_tax_amount, :decimal, precision: 12, scale: 4, default: 0.0, null: false
+ end
+end
diff --git a/core/db/migrate/20150626181949_add_taxable_adjustment_total_to_line_item.rb b/core/db/migrate/20150626181949_add_taxable_adjustment_total_to_line_item.rb
new file mode 100644
index 00000000000..5811262a292
--- /dev/null
+++ b/core/db/migrate/20150626181949_add_taxable_adjustment_total_to_line_item.rb
@@ -0,0 +1,19 @@
+class AddTaxableAdjustmentTotalToLineItem < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_line_items, :taxable_adjustment_total, :decimal,
+ precision: 10, scale: 2, default: 0.0, null: false
+ add_column :spree_line_items, :non_taxable_adjustment_total, :decimal,
+ precision: 10, scale: 2, default: 0.0, null: false
+
+ add_column :spree_shipments, :taxable_adjustment_total, :decimal,
+ precision: 10, scale: 2, default: 0.0, null: false
+ add_column :spree_shipments, :non_taxable_adjustment_total, :decimal,
+ precision: 10, scale: 2, default: 0.0, null: false
+
+ add_column :spree_orders, :taxable_adjustment_total, :decimal,
+ precision: 10, scale: 2, default: 0.0, null: false
+ add_column :spree_orders, :non_taxable_adjustment_total, :decimal,
+ precision: 10, scale: 2, default: 0.0, null: false
+ # TODO migration that updates old orders
+ end
+end
diff --git a/core/db/migrate/20150627090949_migrate_payment_methods_display.rb b/core/db/migrate/20150627090949_migrate_payment_methods_display.rb
new file mode 100644
index 00000000000..7ff7d4f09bf
--- /dev/null
+++ b/core/db/migrate/20150627090949_migrate_payment_methods_display.rb
@@ -0,0 +1,12 @@
+class MigratePaymentMethodsDisplay < ActiveRecord::Migration[4.2]
+ def change
+ Spree::PaymentMethod.all.each do |method|
+ if method.display_on.blank?
+ method.display_on = "both"
+ method.save
+ end
+ end
+
+ change_column :spree_payment_methods, :display_on, :string, default: "both"
+ end
+end
diff --git a/core/db/migrate/20150707204155_enable_acts_as_paranoid_on_calculators.rb b/core/db/migrate/20150707204155_enable_acts_as_paranoid_on_calculators.rb
new file mode 100644
index 00000000000..d7d4a17bd6f
--- /dev/null
+++ b/core/db/migrate/20150707204155_enable_acts_as_paranoid_on_calculators.rb
@@ -0,0 +1,6 @@
+class EnableActsAsParanoidOnCalculators < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_calculators, :deleted_at, :datetime
+ add_index :spree_calculators, :deleted_at
+ end
+end
diff --git a/core/db/migrate/20150714154102_spree_payment_method_store_credits.rb b/core/db/migrate/20150714154102_spree_payment_method_store_credits.rb
new file mode 100644
index 00000000000..e3437e0ac45
--- /dev/null
+++ b/core/db/migrate/20150714154102_spree_payment_method_store_credits.rb
@@ -0,0 +1,12 @@
+class SpreePaymentMethodStoreCredits < ActiveRecord::Migration[4.2]
+ def up
+ # Reload to pick up new position column for acts_as_list
+ Spree::PaymentMethod.reset_column_information
+ Spree::PaymentMethod::StoreCredit.find_or_create_by(name: 'Store Credit', description: 'Store Credit',
+ active: true, display_on: 'back_end')
+ end
+
+ def down
+ Spree::PaymentMethod.find_by(type: 'Spree::PaymentMethod::StoreCredit', name: 'Store Credit').try(&:destroy)
+ end
+end
diff --git a/core/db/migrate/20150726141425_rename_has_and_belongs_to_associations_to_model_names.rb b/core/db/migrate/20150726141425_rename_has_and_belongs_to_associations_to_model_names.rb
new file mode 100644
index 00000000000..d3909cc16aa
--- /dev/null
+++ b/core/db/migrate/20150726141425_rename_has_and_belongs_to_associations_to_model_names.rb
@@ -0,0 +1,18 @@
+class RenameHasAndBelongsToAssociationsToModelNames < ActiveRecord::Migration[4.2]
+ def change
+ {
+ 'spree_option_types_prototypes' => 'spree_option_type_prototypes',
+ 'spree_option_values_variants' => 'spree_option_value_variants',
+ 'spree_orders_promotions' => 'spree_order_promotions',
+ 'spree_products_promotion_rules' => 'spree_product_promotion_rules',
+ 'spree_taxons_promotion_rules' => 'spree_promotion_rule_taxons',
+ 'spree_promotion_rules_users' => 'spree_promotion_rule_users',
+ 'spree_properties_prototypes' => 'spree_property_prototypes',
+ 'spree_taxons_prototypes' => 'spree_prototype_taxons',
+ 'spree_roles_users' => 'spree_role_users',
+ 'spree_shipping_methods_zones' => 'spree_shipping_method_zones'
+ }.each do |old_name, new_name|
+ rename_table old_name, new_name
+ end
+ end
+end
diff --git a/core/db/migrate/20150727191614_spree_store_credit_types.rb b/core/db/migrate/20150727191614_spree_store_credit_types.rb
new file mode 100644
index 00000000000..a8bc74a1d7f
--- /dev/null
+++ b/core/db/migrate/20150727191614_spree_store_credit_types.rb
@@ -0,0 +1,11 @@
+class SpreeStoreCreditTypes < ActiveRecord::Migration[4.2]
+ def up
+ Spree::StoreCreditType.find_or_create_by(name: 'Expiring', priority: 1)
+ Spree::StoreCreditType.find_or_create_by(name: 'Non-expiring', priority: 2)
+ end
+
+ def down
+ Spree::StoreCreditType.find_by(name: 'Expiring').try(&:destroy)
+ Spree::StoreCreditType.find_by(name: 'Non-expiring').try(&:destroy)
+ end
+end
diff --git a/core/db/migrate/20150819154308_add_discontinued_to_products_and_variants.rb b/core/db/migrate/20150819154308_add_discontinued_to_products_and_variants.rb
new file mode 100644
index 00000000000..1b37f2fe6d0
--- /dev/null
+++ b/core/db/migrate/20150819154308_add_discontinued_to_products_and_variants.rb
@@ -0,0 +1,68 @@
+class AddDiscontinuedToProductsAndVariants < ActiveRecord::Migration[4.2]
+ def up
+ add_column :spree_products, :discontinue_on, :datetime, after: :available_on
+ add_column :spree_variants, :discontinue_on, :datetime, after: :deleted_at
+
+ add_index :spree_products, :discontinue_on
+ add_index :spree_variants, :discontinue_on
+
+ puts "Warning: This migration changes the meaning of 'deleted'. Before this change, 'deleted' meant products that were no longer being sold in your store. After this change, you can only delete a product or variant if it has not already been sold to a customer (a model-level check enforces this). Instead, you should use the new field 'discontinue_on' for products or variants which were sold in the past but no longer for sale. This fixes bugs when other objects are attached to deleted products and variants. (Even though acts_as_paranoid gem keeps the records in the database, most associations are automatically scoped to exclude the deleted records.) In thew meaning of 'deleted,' you can still use the delete function on products & variants which are *truly user-error mistakes*, specifically before an order has been placed or the items have gone on sale. You also must use the soft-delete function (which still works after this change) to clean up slug (product) and SKU (variant) duplicates. Otherwise, you should generally over ever need to discontinue products.
+
+Data Fix: We will attempt to reverse engineer the old meaning of 'deleted' (no longer for sale) to the new database field 'discontinue_on'. However, since Slugs and SKUs cannot be duplicated on Products and Variants, we cannot gaurantee this to be foolproof if you have deteled Products and Variants that have duplicate Slugs or SKUs in non-deleted records. In these cases, we recommend you use the additional rake task to clean up your old records (see rake db:fix_orphan_line_items). If you have such records, this migration will leave them in place, preferring the non-deleted records over the deleted ones. However, since old line items will still be associated with deleted objects, you will still the bugs in your app until you run:
+
+rake db:fix_orphan_line_items
+
+We will print out a report of the data we are fixing now: "
+
+ Spree::Product.only_deleted.each do |product|
+ # determine if there is a slug duplicate
+ the_dup = Spree::Product.find_by(slug: product.slug)
+ if the_dup.nil?
+ # check to see if there are line items attached to any variants
+ if Spree::Variant.with_deleted.where(product_id: product.id).map(&:line_items).any?
+ puts "recovering deleted product id #{product.id} ... this will un-delete the record and set it to be discontinued"
+
+ old_deleted = product.deleted_at
+ product.update_column(:deleted_at, nil) # for some reason .recover doesn't appear to be a method
+ product.update_column(:discontinue_on, old_deleted)
+ else
+ puts "leaving product id #{product.id} deleted because there are no line items attached to it..."
+ end
+ else
+ puts "leaving product id #{product.id} deleted because there is a duplicate slug for '#{product.slug}' (product id #{the_dup.id}) "
+ if product.variants.map(&:line_items).any?
+ puts "WARNING: You may still have bugs with product id #{product.id} (#{product.name}) until you run rake db:fix_orphan_line_items"
+ end
+ end
+ end
+
+ Spree::Variant.only_deleted.each do |variant|
+ # determine if there is a slug duplicate
+ the_dup = Spree::Variant.find_by(sku: variant.sku)
+ if the_dup.nil?
+ # check to see if there are line items attached to any variants
+ if variant.line_items.any?
+ puts "recovering deleted variant id #{variant.id} ... this will un-delete the record and set it to be discontinued"
+ old_deleted = variant.deleted_at
+ variant.update_column(:deleted_at, nil) # for some reason .recover doesn't appear to be a method
+ variant.update_column(:discontinue_on, old_deleted)
+ else
+ puts "leaving variant id #{variant.id} deleted because there are no line items attached to it..."
+ end
+ else
+ puts "leaving variant id #{variant.id} deleted because there is a duplicate SKU for '#{variant.sku}' (variant id #{the_dup.id}) "
+ if variant.line_items.any?
+ puts "WARNING: You may still have bugs with variant id #{variant.id} (#{variant.name}) until you run rake db:fix_orphan_line_items"
+ end
+ end
+ end
+ end
+
+ def down
+ execute "UPDATE spree_products SET deleted_at = discontinue_on WHERE deleted_at IS NULL"
+ execute "UPDATE spree_variants SET deleted_at = discontinue_on WHERE deleted_at IS NULL"
+
+ remove_column :spree_products, :discontinue_on
+ remove_column :spree_variants, :discontinue_on
+ end
+end
diff --git a/core/db/migrate/20151220072838_remove_shipping_method_id_from_spree_orders.rb b/core/db/migrate/20151220072838_remove_shipping_method_id_from_spree_orders.rb
new file mode 100644
index 00000000000..d7250b03e6d
--- /dev/null
+++ b/core/db/migrate/20151220072838_remove_shipping_method_id_from_spree_orders.rb
@@ -0,0 +1,13 @@
+class RemoveShippingMethodIdFromSpreeOrders < ActiveRecord::Migration[4.2]
+ def up
+ if column_exists?(:spree_orders, :shipping_method_id, :integer)
+ remove_column :spree_orders, :shipping_method_id, :integer
+ end
+ end
+
+ def down
+ unless column_exists?(:spree_orders, :shipping_method_id, :integer)
+ add_column :spree_orders, :shipping_method_id, :integer
+ end
+ end
+end
diff --git a/core/db/migrate/20160207191757_add_id_column_to_earlier_habtm_tables.rb b/core/db/migrate/20160207191757_add_id_column_to_earlier_habtm_tables.rb
new file mode 100644
index 00000000000..29292e5dfe8
--- /dev/null
+++ b/core/db/migrate/20160207191757_add_id_column_to_earlier_habtm_tables.rb
@@ -0,0 +1,16 @@
+class AddIdColumnToEarlierHabtmTables < ActiveRecord::Migration[4.2]
+ def up
+ add_column :spree_option_type_prototypes, :id, :primary_key
+ add_column :spree_option_value_variants, :id, :primary_key
+ add_column :spree_order_promotions, :id, :primary_key
+ add_column :spree_product_promotion_rules, :id, :primary_key
+ add_column :spree_promotion_rule_users, :id, :primary_key
+ add_column :spree_property_prototypes, :id, :primary_key
+ add_column :spree_role_users, :id, :primary_key
+ add_column :spree_shipping_method_zones, :id, :primary_key
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/core/db/migrate/20160219165458_add_indexes.rb b/core/db/migrate/20160219165458_add_indexes.rb
new file mode 100644
index 00000000000..c82c0258c22
--- /dev/null
+++ b/core/db/migrate/20160219165458_add_indexes.rb
@@ -0,0 +1,14 @@
+class AddIndexes < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_assets, :position
+ add_index :spree_option_types, :name
+ add_index :spree_option_values, :name
+ add_index :spree_prices, :variant_id
+ add_index :spree_properties, :name
+ add_index :spree_roles, :name
+ add_index :spree_shipping_categories, :name
+ add_index :spree_taxons, :lft
+ add_index :spree_taxons, :rgt
+ add_index :spree_taxons, :name
+ end
+end
diff --git a/core/db/migrate/20160509064646_remove_counter_cache_from_spree_variants_to_spree_stock_items.rb b/core/db/migrate/20160509064646_remove_counter_cache_from_spree_variants_to_spree_stock_items.rb
new file mode 100644
index 00000000000..7f4961ff1e2
--- /dev/null
+++ b/core/db/migrate/20160509064646_remove_counter_cache_from_spree_variants_to_spree_stock_items.rb
@@ -0,0 +1,10 @@
+class RemoveCounterCacheFromSpreeVariantsToSpreeStockItems < ActiveRecord::Migration[4.2]
+ def up
+ if column_exists?(:spree_variants, :stock_items_count)
+ remove_column :spree_variants, :stock_items_count
+ end
+ end
+
+ def down
+ end
+end
diff --git a/core/db/migrate/20160511071954_acts_as_taggable_on_spree_migration.rb b/core/db/migrate/20160511071954_acts_as_taggable_on_spree_migration.rb
new file mode 100644
index 00000000000..b5f01a86dea
--- /dev/null
+++ b/core/db/migrate/20160511071954_acts_as_taggable_on_spree_migration.rb
@@ -0,0 +1,40 @@
+class ActsAsTaggableOnSpreeMigration < ActiveRecord::Migration[4.2]
+ def self.up
+ create_table :spree_tags do |t|
+ t.string :name
+ t.integer :taggings_count, default: 0
+ end
+
+ create_table :spree_taggings do |t|
+ t.references :tag
+
+ # You should make sure that the column created is
+ # long enough to store the required class names.
+ t.references :taggable, polymorphic: true
+ t.references :tagger, polymorphic: true
+
+ # Limit is created to prevent MySQL error on index
+ # length for MyISAM table type: http://bit.ly/vgW2Ql
+ t.string :context, limit: 128
+
+ t.datetime :created_at
+ end
+
+ add_index :spree_tags, :name, unique: true
+ add_index :spree_taggings,
+ [
+ :tag_id,
+ :taggable_id,
+ :taggable_type,
+ :context,
+ :tagger_id,
+ :tagger_type
+ ],
+ unique: true, name: "spree_taggings_idx"
+ end
+
+ def self.down
+ drop_table :spree_taggings
+ drop_table :spree_tags
+ end
+end
diff --git a/core/db/migrate/20160511072249_change_collation_for_spree_tag_names.rb b/core/db/migrate/20160511072249_change_collation_for_spree_tag_names.rb
new file mode 100644
index 00000000000..993f8321cb4
--- /dev/null
+++ b/core/db/migrate/20160511072249_change_collation_for_spree_tag_names.rb
@@ -0,0 +1,9 @@
+# This migration is added to circumvent issue #623 and have special characters
+# work properly
+class ChangeCollationForSpreeTagNames < ActiveRecord::Migration[4.2]
+ def up
+ if ActsAsTaggableOn::Utils.using_mysql?
+ execute("ALTER TABLE spree_tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
+ end
+ end
+end
diff --git a/core/db/migrate/20160511072335_add_missing_indexes_to_spree_taggings.rb b/core/db/migrate/20160511072335_add_missing_indexes_to_spree_taggings.rb
new file mode 100644
index 00000000000..6828cd91ee7
--- /dev/null
+++ b/core/db/migrate/20160511072335_add_missing_indexes_to_spree_taggings.rb
@@ -0,0 +1,14 @@
+class AddMissingIndexesToSpreeTaggings < ActiveRecord::Migration[4.2]
+ def change
+ add_index :spree_taggings, :tag_id
+ add_index :spree_taggings, :taggable_id
+ add_index :spree_taggings, :taggable_type
+ add_index :spree_taggings, :tagger_id
+ add_index :spree_taggings, :context
+
+ add_index :spree_taggings, [:tagger_id, :tagger_type]
+ add_index :spree_taggings,
+ [:taggable_id, :taggable_type, :tagger_id, :context],
+ name: "spree_taggings_idy"
+ end
+end
diff --git a/core/db/migrate/20160608090604_add_zipcode_required_to_spree_countries.rb b/core/db/migrate/20160608090604_add_zipcode_required_to_spree_countries.rb
new file mode 100644
index 00000000000..c5c1e2d91aa
--- /dev/null
+++ b/core/db/migrate/20160608090604_add_zipcode_required_to_spree_countries.rb
@@ -0,0 +1,7 @@
+class AddZipcodeRequiredToSpreeCountries < ActiveRecord::Migration[4.2]
+ def change
+ add_column :spree_countries, :zipcode_required, :boolean, default: true
+ Spree::Country.reset_column_information
+ Spree::Country.where(iso: Spree::Address::NO_ZIPCODE_ISO_CODES).update_all(zipcode_required: false)
+ end
+end
diff --git a/core/db/migrate/20161014145148_add_created_at_to_variant.rb b/core/db/migrate/20161014145148_add_created_at_to_variant.rb
new file mode 100644
index 00000000000..61fb0e295b6
--- /dev/null
+++ b/core/db/migrate/20161014145148_add_created_at_to_variant.rb
@@ -0,0 +1,8 @@
+class AddCreatedAtToVariant < ActiveRecord::Migration[5.0]
+ def change
+ add_column :spree_variants, :created_at, :datetime
+ Spree::Variant.reset_column_information
+ Spree::Variant.unscoped.where.not(updated_at: nil).update_all('created_at = updated_at')
+ Spree::Variant.unscoped.where(updated_at: nil).update_all(created_at: Time.current, updated_at: Time.current)
+ end
+end
diff --git a/core/db/migrate/20161014152814_add_null_false_to_spree_variants_timestamps.rb b/core/db/migrate/20161014152814_add_null_false_to_spree_variants_timestamps.rb
new file mode 100644
index 00000000000..8cf0901761f
--- /dev/null
+++ b/core/db/migrate/20161014152814_add_null_false_to_spree_variants_timestamps.rb
@@ -0,0 +1,6 @@
+class AddNullFalseToSpreeVariantsTimestamps < ActiveRecord::Migration[5.0]
+ def change
+ change_column_null :spree_variants, :created_at, false
+ change_column_null :spree_variants, :updated_at, false
+ end
+end
diff --git a/core/db/migrate/20161125065505_add_quantity_to_inventory_units.rb b/core/db/migrate/20161125065505_add_quantity_to_inventory_units.rb
new file mode 100644
index 00000000000..5bd8628b66b
--- /dev/null
+++ b/core/db/migrate/20161125065505_add_quantity_to_inventory_units.rb
@@ -0,0 +1,5 @@
+class AddQuantityToInventoryUnits < ActiveRecord::Migration[5.0]
+ def change
+ add_column :spree_inventory_units, :quantity, :integer, default: 1
+ end
+end
diff --git a/core/db/migrate/20170119122701_add_original_return_item_id_to_spree_inventory_units.rb b/core/db/migrate/20170119122701_add_original_return_item_id_to_spree_inventory_units.rb
new file mode 100644
index 00000000000..04942b4eae9
--- /dev/null
+++ b/core/db/migrate/20170119122701_add_original_return_item_id_to_spree_inventory_units.rb
@@ -0,0 +1,29 @@
+class AddOriginalReturnItemIdToSpreeInventoryUnits < ActiveRecord::Migration[5.0]
+ def up
+ add_reference :spree_inventory_units, :original_return_item, references: :spree_return_items, index: true
+
+ Spree::InventoryUnit.reset_column_information
+
+ Spree::ReturnItem.where.not(exchange_inventory_unit_id: nil).find_each do |return_item|
+ if (inventory_unit = Spree::InventoryUnit.find_by(id: return_item.exchange_inventory_unit_id)).present?
+ inventory_unit.update_column(:original_return_item_id, return_item.id)
+ end
+ end
+
+ remove_column :spree_return_items, :exchange_inventory_unit_id
+ end
+
+ def down
+ add_reference :spree_return_items, :exchange_inventory_unit, references: :spree_inventory_units, index: true
+
+ Spree::InventoryUnit.reset_column_information
+
+ Spree::InventoryUnit.where.not(original_return_item_id: nil).find_each do |inventory_unit|
+ if (return_item = Spree::ReturnItem.find_by(id: inventory_unit.original_return_item_id)).present?
+ return_item.update_column(:exchange_inventory_unit_id, inventory_unit.id)
+ end
+ end
+
+ remove_reference :spree_inventory_units, :original_return_item
+ end
+end
diff --git a/core/db/migrate/20170315152755_add_unique_index_on_number_to_spree_orders.rb b/core/db/migrate/20170315152755_add_unique_index_on_number_to_spree_orders.rb
new file mode 100644
index 00000000000..b56e6ed3976
--- /dev/null
+++ b/core/db/migrate/20170315152755_add_unique_index_on_number_to_spree_orders.rb
@@ -0,0 +1,16 @@
+class AddUniqueIndexOnNumberToSpreeOrders < ActiveRecord::Migration[5.0]
+ def change
+ unless index_exists?(:spree_orders, :number, unique: true)
+ numbers = Spree::Order.group(:number).having('sum(1) > 1').pluck(:number)
+ orders = Spree::Order.where(number: numbers)
+
+ orders.find_each do |order|
+ order.number = order.class.number_generator.method(:generate_permalink).call(order.class)
+ order.save
+ end
+
+ remove_index :spree_orders, :number if index_exists?(:spree_orders, :number)
+ add_index :spree_orders, :number, unique: true
+ end
+ end
+end
diff --git a/core/db/migrate/20170316154338_add_unique_index_on_number_to_spree_stock_transfer.rb b/core/db/migrate/20170316154338_add_unique_index_on_number_to_spree_stock_transfer.rb
new file mode 100644
index 00000000000..292f5b8eb81
--- /dev/null
+++ b/core/db/migrate/20170316154338_add_unique_index_on_number_to_spree_stock_transfer.rb
@@ -0,0 +1,16 @@
+class AddUniqueIndexOnNumberToSpreeStockTransfer < ActiveRecord::Migration[5.0]
+ def change
+ unless index_exists?(:spree_stock_transfers, :number, unique: true)
+ numbers = Spree::StockTransfer.group(:number).having('sum(1) > 1').pluck(:number)
+ transfers = Spree::StockTransfer.where(number: numbers)
+
+ transfers.find_each do |transfer|
+ transfer.number = transfer.class.number_generator.method(:generate_permalink).call(transfer.class)
+ transfer.save
+ end
+
+ remove_index :spree_stock_transfers, :number if index_exists?(:spree_stock_transfers, :number)
+ add_index :spree_stock_transfers, :number, unique: true
+ end
+ end
+end
diff --git a/core/db/migrate/20170316205511_add_unique_index_on_number_to_spree_shipment.rb b/core/db/migrate/20170316205511_add_unique_index_on_number_to_spree_shipment.rb
new file mode 100644
index 00000000000..167d45f0be1
--- /dev/null
+++ b/core/db/migrate/20170316205511_add_unique_index_on_number_to_spree_shipment.rb
@@ -0,0 +1,16 @@
+class AddUniqueIndexOnNumberToSpreeShipment < ActiveRecord::Migration[5.0]
+ def change
+ unless index_exists?(:spree_shipments, :number, unique: true)
+ numbers = Spree::Shipment.group(:number).having('sum(1) > 1').pluck(:number)
+ shipments = Spree::Shipment.where(number: numbers)
+
+ shipments.find_each do |shipment|
+ shipment.number = shipment.class.number_generator.method(:generate_permalink).call(shipment.class)
+ shipment.save
+ end
+
+ remove_index :spree_shipments, :number if index_exists?(:spree_shipments, :number)
+ add_index :spree_shipments, :number, unique: true
+ end
+ end
+end
diff --git a/core/db/migrate/20170320134043_add_unique_index_on_number_to_spree_payments.rb b/core/db/migrate/20170320134043_add_unique_index_on_number_to_spree_payments.rb
new file mode 100644
index 00000000000..98f1204da69
--- /dev/null
+++ b/core/db/migrate/20170320134043_add_unique_index_on_number_to_spree_payments.rb
@@ -0,0 +1,17 @@
+class AddUniqueIndexOnNumberToSpreePayments < ActiveRecord::Migration[5.0]
+ def change
+ unless index_exists?(:spree_payments, :number, unique: true)
+ # default scope in Spree::Payment disturbs Postgres, hence `unscoped` is needed.
+ numbers = Spree::Payment.unscoped.group(:number).having('sum(1) > 1').pluck(:number)
+ payments = Spree::Payment.where(number: numbers)
+
+ payments.find_each do |payment|
+ payment.number = payment.class.number_generator.method(:generate_permalink).call(payment.class)
+ payment.save
+ end
+
+ remove_index :spree_payments, :number if index_exists?(:spree_payments, :number)
+ add_index :spree_payments, :number, unique: true
+ end
+ end
+end
diff --git a/core/db/migrate/20170320142750_add_unique_index_on_number_to_spree_return_authorizations.rb b/core/db/migrate/20170320142750_add_unique_index_on_number_to_spree_return_authorizations.rb
new file mode 100644
index 00000000000..0aedf02e304
--- /dev/null
+++ b/core/db/migrate/20170320142750_add_unique_index_on_number_to_spree_return_authorizations.rb
@@ -0,0 +1,16 @@
+class AddUniqueIndexOnNumberToSpreeReturnAuthorizations < ActiveRecord::Migration[5.0]
+ def change
+ unless index_exists?(:spree_return_authorizations, :number, unique: true)
+ numbers = Spree::ReturnAuthorization.group(:number).having('sum(1) > 1').pluck(:number)
+ authorizations = Spree::ReturnAuthorization.where(number: numbers)
+
+ authorizations.find_each do |authorization|
+ authorization.number = authorization.class.number_generator.method(:generate_permalink).call(authorization.class)
+ authorization.save
+ end
+
+ remove_index :spree_return_authorizations, :number if index_exists?(:spree_return_authorizations, :number)
+ add_index :spree_return_authorizations, :number, unique: true
+ end
+ end
+end
diff --git a/core/db/migrate/20170320145040_add_unique_index_on_number_to_spree_customer_returns.rb b/core/db/migrate/20170320145040_add_unique_index_on_number_to_spree_customer_returns.rb
new file mode 100644
index 00000000000..5c33510caa4
--- /dev/null
+++ b/core/db/migrate/20170320145040_add_unique_index_on_number_to_spree_customer_returns.rb
@@ -0,0 +1,16 @@
+class AddUniqueIndexOnNumberToSpreeCustomerReturns < ActiveRecord::Migration[5.0]
+ def change
+ unless index_exists?(:spree_customer_returns, :number, unique: true)
+ numbers = Spree::CustomerReturn.group(:number).having('sum(1) > 1').pluck(:number)
+ returns = Spree::CustomerReturn.where(number: numbers)
+
+ returns.find_each do |r|
+ r.number = r.class.number_generator.method(:generate_permalink).call(r.class)
+ r.save
+ end
+
+ remove_index :spree_customer_returns, :number if index_exists?(:spree_customer_returns, :number)
+ add_index :spree_customer_returns, :number, unique: true
+ end
+ end
+end
diff --git a/core/db/migrate/20170320145518_add_unique_index_on_number_to_spree_reimbursements.rb b/core/db/migrate/20170320145518_add_unique_index_on_number_to_spree_reimbursements.rb
new file mode 100644
index 00000000000..af338dc2eb9
--- /dev/null
+++ b/core/db/migrate/20170320145518_add_unique_index_on_number_to_spree_reimbursements.rb
@@ -0,0 +1,16 @@
+class AddUniqueIndexOnNumberToSpreeReimbursements < ActiveRecord::Migration[5.0]
+ def change
+ unless index_exists?(:spree_reimbursements, :number, unique: true)
+ numbers = Spree::Reimbursement.group(:number).having('sum(1) > 1').pluck(:number)
+ reimbursements = Spree::Reimbursement.where(number: numbers)
+
+ reimbursements.find_each do |reimbursement|
+ reimbursement.number = reimbursement.class.number_generator.method(:generate_permalink).call(reimbursement.class)
+ reimbursement.save
+ end
+
+ remove_index :spree_reimbursements, :number if index_exists?(:spree_reimbursements, :number)
+ add_index :spree_reimbursements, :number, unique: true
+ end
+ end
+end
diff --git a/core/db/migrate/20170323151450_add_missing_unique_indexes_for_unique_attributes.rb b/core/db/migrate/20170323151450_add_missing_unique_indexes_for_unique_attributes.rb
new file mode 100644
index 00000000000..a67746bfdbe
--- /dev/null
+++ b/core/db/migrate/20170323151450_add_missing_unique_indexes_for_unique_attributes.rb
@@ -0,0 +1,37 @@
+class AddMissingUniqueIndexesForUniqueAttributes < ActiveRecord::Migration[5.0]
+ def change
+ tables = {
+ country: [:name, :iso_name],
+ refund_reason: [:name],
+ reimbursement_type: [:name],
+ return_authorization_reason: [:name],
+ role: [:name],
+ store: [:code]
+ }
+
+ tables.each do |table, columns|
+ table_class = "Spree::#{table.to_s.classify}".constantize
+ table_name = table_class.table_name
+
+ columns.each do |column|
+ unless index_exists?(table_name, column, unique: true)
+ attributes = table_class.unscoped.group(column).having('sum(1) > 1').pluck(column)
+ instances = table_class.where(column => [nil, attributes])
+
+ instances.find_each do |instance|
+ column_value = 'Unique String ' + SecureRandom.urlsafe_base64(8).upcase.delete('/+=_-')[0, 8]
+ instance.send("#{column}=", column_value)
+ instance.save
+ end
+
+ remove_index table_name, column if index_exists?(table_name, column)
+ if supports_expression_index?
+ add_index table_name, "lower(#{column})", unique: true
+ else
+ add_index table_name, column, unique: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/db/migrate/20170329110859_add_index_on_stock_location_to_spree_customer_returns.rb b/core/db/migrate/20170329110859_add_index_on_stock_location_to_spree_customer_returns.rb
new file mode 100644
index 00000000000..79346747dd2
--- /dev/null
+++ b/core/db/migrate/20170329110859_add_index_on_stock_location_to_spree_customer_returns.rb
@@ -0,0 +1,5 @@
+class AddIndexOnStockLocationToSpreeCustomerReturns < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_customer_returns, :stock_location_id
+ end
+end
diff --git a/core/db/migrate/20170329113917_add_index_on_prototype_to_spree_option_type_prototype.rb b/core/db/migrate/20170329113917_add_index_on_prototype_to_spree_option_type_prototype.rb
new file mode 100644
index 00000000000..b9f1a5db2b8
--- /dev/null
+++ b/core/db/migrate/20170329113917_add_index_on_prototype_to_spree_option_type_prototype.rb
@@ -0,0 +1,19 @@
+class AddIndexOnPrototypeToSpreeOptionTypePrototype < ActiveRecord::Migration[5.0]
+ def change
+ duplicates = Spree::OptionTypePrototype.group(:prototype_id, :option_type_id).having('sum(1) > 1').size
+
+ duplicates.each do |f|
+ prototype_id, option_type_id = f.first
+ count = f.last - 1 # we want to leave one record
+ otp = Spree::OptionTypePrototype.where(prototype_id: prototype_id, option_type_id: option_type_id).last(count)
+ otp.map(&:destroy)
+ end
+
+ if index_exists? :spree_option_type_prototypes, [:prototype_id, :option_type_id]
+ remove_index :spree_option_type_prototypes, [:prototype_id, :option_type_id]
+ add_index :spree_option_type_prototypes, [:prototype_id, :option_type_id], unique: true, name: 'spree_option_type_prototypes_prototype_id_option_type_id'
+ end
+
+ add_index :spree_option_type_prototypes, :prototype_id
+ end
+end
diff --git a/core/db/migrate/20170330082155_add_indexes_to_spree_option_value_variant.rb b/core/db/migrate/20170330082155_add_indexes_to_spree_option_value_variant.rb
new file mode 100644
index 00000000000..4356af01a60
--- /dev/null
+++ b/core/db/migrate/20170330082155_add_indexes_to_spree_option_value_variant.rb
@@ -0,0 +1,19 @@
+class AddIndexesToSpreeOptionValueVariant < ActiveRecord::Migration[5.0]
+ def change
+ duplicates = Spree::OptionValueVariant.group(:variant_id, :option_value_id).having('sum(1) > 1').size
+
+ duplicates.each do |f|
+ variant_id, option_value_id = f.first
+ count = f.last - 1 # we want to leave one record
+ ov = Spree::OptionValueVariant.where(variant_id: variant_id, option_value_id: option_value_id).last(count)
+ ov.map(&:destroy)
+ end
+
+ if index_exists? :spree_option_value_variants, [:variant_id, :option_value_id], name: "index_option_values_variants_on_variant_id_and_option_value_id"
+ remove_index :spree_option_value_variants, [:variant_id, :option_value_id]
+ add_index :spree_option_value_variants, [:variant_id, :option_value_id], unique: true, name: "index_option_values_variants_on_variant_id_and_option_value_id"
+ end
+
+ add_index :spree_option_value_variants, :variant_id
+ end
+end
diff --git a/core/db/migrate/20170330132215_add_index_on_promotion_id_to_order_promotions.rb b/core/db/migrate/20170330132215_add_index_on_promotion_id_to_order_promotions.rb
new file mode 100644
index 00000000000..b97d69af9df
--- /dev/null
+++ b/core/db/migrate/20170330132215_add_index_on_promotion_id_to_order_promotions.rb
@@ -0,0 +1,5 @@
+class AddIndexOnPromotionIdToOrderPromotions < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_order_promotions, :promotion_id
+ end
+end
diff --git a/core/db/migrate/20170331101758_add_indexes_for_property_prototype.rb b/core/db/migrate/20170331101758_add_indexes_for_property_prototype.rb
new file mode 100644
index 00000000000..2c6d2f3ef29
--- /dev/null
+++ b/core/db/migrate/20170331101758_add_indexes_for_property_prototype.rb
@@ -0,0 +1,20 @@
+class AddIndexesForPropertyPrototype < ActiveRecord::Migration[5.0]
+ def change
+ duplicates = Spree::PropertyPrototype.group(:prototype_id, :property_id).having('sum(1) > 1').size
+
+ duplicates.each do |f|
+ prototype_id, property_id = f.first
+ count = f.last - 1 # we want to leave one record
+ prototypes = Spree::PropertyPrototype.where(prototype_id: prototype_id, property_id: property_id).last(count)
+ prototypes.map(&:destroy)
+ end
+
+ if index_exists? :spree_property_prototypes, [:prototype_id, :property_id]
+ remove_index :spree_property_prototypes, [:prototype_id, :property_id]
+ add_index :spree_property_prototypes, [:prototype_id, :property_id], unique: true, name: 'index_property_prototypes_on_prototype_id_and_property_id'
+ end
+
+ add_index :spree_property_prototypes, :prototype_id
+ add_index :spree_property_prototypes, :property_id
+ end
+end
diff --git a/core/db/migrate/20170331103334_add_index_for_prototype_id_to_prototype_taxons.rb b/core/db/migrate/20170331103334_add_index_for_prototype_id_to_prototype_taxons.rb
new file mode 100644
index 00000000000..a0c86214111
--- /dev/null
+++ b/core/db/migrate/20170331103334_add_index_for_prototype_id_to_prototype_taxons.rb
@@ -0,0 +1,5 @@
+class AddIndexForPrototypeIdToPrototypeTaxons < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_prototype_taxons, :prototype_id
+ end
+end
diff --git a/core/db/migrate/20170331110454_add_indexes_to_refunds.rb b/core/db/migrate/20170331110454_add_indexes_to_refunds.rb
new file mode 100644
index 00000000000..d3d73687ef1
--- /dev/null
+++ b/core/db/migrate/20170331110454_add_indexes_to_refunds.rb
@@ -0,0 +1,6 @@
+class AddIndexesToRefunds < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_refunds, :payment_id
+ add_index :spree_refunds, :reimbursement_id
+ end
+end
diff --git a/core/db/migrate/20170331111757_add_indexes_to_reimbursement_credits.rb b/core/db/migrate/20170331111757_add_indexes_to_reimbursement_credits.rb
new file mode 100644
index 00000000000..fdc9ae205ec
--- /dev/null
+++ b/core/db/migrate/20170331111757_add_indexes_to_reimbursement_credits.rb
@@ -0,0 +1,6 @@
+class AddIndexesToReimbursementCredits < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_reimbursement_credits, :reimbursement_id
+ add_index :spree_reimbursement_credits, [:creditable_id, :creditable_type], name: 'index_reimbursement_credits_on_creditable_id_and_type'
+ end
+end
diff --git a/core/db/migrate/20170331115246_add_indexes_to_return_authorizations.rb b/core/db/migrate/20170331115246_add_indexes_to_return_authorizations.rb
new file mode 100644
index 00000000000..0764f7b3497
--- /dev/null
+++ b/core/db/migrate/20170331115246_add_indexes_to_return_authorizations.rb
@@ -0,0 +1,6 @@
+class AddIndexesToReturnAuthorizations < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_return_authorizations, :order_id
+ add_index :spree_return_authorizations, :stock_location_id
+ end
+end
diff --git a/core/db/migrate/20170331120125_add_indexes_to_return_items.rb b/core/db/migrate/20170331120125_add_indexes_to_return_items.rb
new file mode 100644
index 00000000000..449bbd20ed7
--- /dev/null
+++ b/core/db/migrate/20170331120125_add_indexes_to_return_items.rb
@@ -0,0 +1,11 @@
+class AddIndexesToReturnItems < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_return_items, :return_authorization_id
+ add_index :spree_return_items, :inventory_unit_id
+ add_index :spree_return_items, :reimbursement_id
+ add_index :spree_return_items, :exchange_variant_id
+ add_index :spree_return_items, :preferred_reimbursement_type_id
+ add_index :spree_return_items, :override_reimbursement_type_id
+ end
+end
+
diff --git a/core/db/migrate/20170331121725_add_index_to_role_users.rb b/core/db/migrate/20170331121725_add_index_to_role_users.rb
new file mode 100644
index 00000000000..145e2f90c72
--- /dev/null
+++ b/core/db/migrate/20170331121725_add_index_to_role_users.rb
@@ -0,0 +1,18 @@
+class AddIndexToRoleUsers < ActiveRecord::Migration[5.0]
+ def change
+
+ duplicates = Spree::RoleUser.group(:role_id, :user_id).having('sum(1) > 1').size
+
+ duplicates.each do |f|
+ role_id, user_id = f.first
+ count = f.last - 1 # we want to leave one record
+ roles = Spree::RoleUser.where(role_id: role_id, user_id: user_id).last(count)
+ roles.map(&:destroy)
+ end
+
+ if index_exists? :spree_role_users, [:role_id, :user_id]
+ remove_index :spree_role_users, [:role_id, :user_id]
+ add_index :spree_role_users, [:role_id, :user_id], unique: true
+ end
+ end
+end
diff --git a/core/db/migrate/20170331123625_add_index_to_shipping_method_categories.rb b/core/db/migrate/20170331123625_add_index_to_shipping_method_categories.rb
new file mode 100644
index 00000000000..5dbfb52d6da
--- /dev/null
+++ b/core/db/migrate/20170331123625_add_index_to_shipping_method_categories.rb
@@ -0,0 +1,5 @@
+class AddIndexToShippingMethodCategories < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_shipping_method_categories, :shipping_category_id
+ end
+end
diff --git a/core/db/migrate/20170331123832_add_index_to_shipping_method_zones.rb b/core/db/migrate/20170331123832_add_index_to_shipping_method_zones.rb
new file mode 100644
index 00000000000..6d2e1934850
--- /dev/null
+++ b/core/db/migrate/20170331123832_add_index_to_shipping_method_zones.rb
@@ -0,0 +1,20 @@
+class AddIndexToShippingMethodZones < ActiveRecord::Migration[5.0]
+ def change
+ duplicates = Spree::ShippingMethodZone.group(:shipping_method_id, :zone_id).having('sum(1) > 1').size
+
+ duplicates.each do |f|
+ shipping_method_id, zone_id = f.first
+ count = f.last - 1 # we want to leave one record
+ zones = Spree::ShippingMethodZone.where(shipping_method_id: shipping_method_id, zone_id: zone_id).last(count)
+ zones.map(&:destroy)
+ end
+
+ if index_exists? :spree_shipping_method_zones, [:shipping_method_id, :zone_id]
+ remove_index :spree_shipping_method_zones, [:shipping_method_id, :zone_id]
+ add_index :spree_shipping_method_zones, [:shipping_method_id, :zone_id], unique: true
+ end
+
+ add_index :spree_shipping_method_zones, :zone_id
+ add_index :spree_shipping_method_zones, :shipping_method_id
+ end
+end
diff --git a/core/db/migrate/20170331124251_add_index_to_spree_shipping_rates.rb b/core/db/migrate/20170331124251_add_index_to_spree_shipping_rates.rb
new file mode 100644
index 00000000000..5717203f835
--- /dev/null
+++ b/core/db/migrate/20170331124251_add_index_to_spree_shipping_rates.rb
@@ -0,0 +1,6 @@
+class AddIndexToSpreeShippingRates < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_shipping_rates, :shipment_id
+ add_index :spree_shipping_rates, :shipping_method_id
+ end
+end
diff --git a/core/db/migrate/20170331124513_add_index_to_spree_stock_items.rb b/core/db/migrate/20170331124513_add_index_to_spree_stock_items.rb
new file mode 100644
index 00000000000..498601ba1d8
--- /dev/null
+++ b/core/db/migrate/20170331124513_add_index_to_spree_stock_items.rb
@@ -0,0 +1,5 @@
+class AddIndexToSpreeStockItems < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_stock_items, :stock_location_id
+ end
+end
diff --git a/core/db/migrate/20170331124924_add_index_to_spree_stock_movement.rb b/core/db/migrate/20170331124924_add_index_to_spree_stock_movement.rb
new file mode 100644
index 00000000000..94403bed86f
--- /dev/null
+++ b/core/db/migrate/20170331124924_add_index_to_spree_stock_movement.rb
@@ -0,0 +1,5 @@
+class AddIndexToSpreeStockMovement < ActiveRecord::Migration[5.0]
+ def change
+ add_index :spree_stock_movements, [:originator_id, :originator_type], name: 'index_stock_movements_on_originator_id_and_originator_type'
+ end
+end
diff --git a/core/db/migrate/20170413211707_change_indexes_on_friendly_id_slugs.rb b/core/db/migrate/20170413211707_change_indexes_on_friendly_id_slugs.rb
new file mode 100644
index 00000000000..c100929de59
--- /dev/null
+++ b/core/db/migrate/20170413211707_change_indexes_on_friendly_id_slugs.rb
@@ -0,0 +1,10 @@
+class ChangeIndexesOnFriendlyIdSlugs < ActiveRecord::Migration[5.0]
+ def change
+ # Updating indexes to reflect changes in friendly_id v5.2
+ # See: https://github.com/norman/friendly_id/pull/694/commits/9f107f07ec9d2a58bda5a712b6e79a8d8013e0ab
+ remove_index :friendly_id_slugs, [:slug, :sluggable_type]
+ remove_index :friendly_id_slugs, [:slug, :sluggable_type, :scope]
+ add_index :friendly_id_slugs, [:slug, :sluggable_type], length: { name: 100, slug: 20, sluggable_type: 20 }
+ add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: { name: 100, slug: 20, sluggable_type: 20, scope: 20 }, unique: true
+ end
+end
diff --git a/core/db/migrate/20170722102643_add_analytics_kind_to_spree_trackers.rb b/core/db/migrate/20170722102643_add_analytics_kind_to_spree_trackers.rb
new file mode 100644
index 00000000000..7216a53560e
--- /dev/null
+++ b/core/db/migrate/20170722102643_add_analytics_kind_to_spree_trackers.rb
@@ -0,0 +1,5 @@
+class AddAnalyticsKindToSpreeTrackers < ActiveRecord::Migration[5.1]
+ def change
+ add_column :spree_trackers, :kind, :integer, default: 0, null: false, index: true
+ end
+end
diff --git a/core/db/migrate/20170727103056_rename_tracker_kind_field.rb b/core/db/migrate/20170727103056_rename_tracker_kind_field.rb
new file mode 100644
index 00000000000..b4a438239bf
--- /dev/null
+++ b/core/db/migrate/20170727103056_rename_tracker_kind_field.rb
@@ -0,0 +1,5 @@
+class RenameTrackerKindField < ActiveRecord::Migration[5.1]
+ def change
+ rename_column :spree_trackers, :kind, :engine
+ end
+end
diff --git a/core/db/migrate/20171004223836_remove_icon_from_taxons.rb b/core/db/migrate/20171004223836_remove_icon_from_taxons.rb
new file mode 100644
index 00000000000..2813be2cc7e
--- /dev/null
+++ b/core/db/migrate/20171004223836_remove_icon_from_taxons.rb
@@ -0,0 +1,8 @@
+class RemoveIconFromTaxons < ActiveRecord::Migration[5.1]
+ def change
+ remove_column :spree_taxons, :icon_file_name if column_exists? :spree_taxons, :icon_file_name
+ remove_column :spree_taxons, :icon_content_type if column_exists? :spree_taxons, :icon_content_type
+ remove_column :spree_taxons, :icon_file_size if column_exists? :spree_taxons, :icon_file_size
+ remove_column :spree_taxons, :icon_updated_at if column_exists? :spree_taxons, :icon_updated_at
+ end
+end
diff --git a/core/db/migrate/20180222133746_add_unique_index_on_spree_promotions_code.rb b/core/db/migrate/20180222133746_add_unique_index_on_spree_promotions_code.rb
new file mode 100644
index 00000000000..787c2c1cdc4
--- /dev/null
+++ b/core/db/migrate/20180222133746_add_unique_index_on_spree_promotions_code.rb
@@ -0,0 +1,6 @@
+class AddUniqueIndexOnSpreePromotionsCode < ActiveRecord::Migration[5.1]
+ def change
+ remove_index :spree_promotions, :code
+ add_index :spree_promotions, :code, unique: true
+ end
+end
diff --git a/core/db/migrate/20180613080857_rename_guest_token_to_token_in_orders.rb b/core/db/migrate/20180613080857_rename_guest_token_to_token_in_orders.rb
new file mode 100644
index 00000000000..1a0b96984b9
--- /dev/null
+++ b/core/db/migrate/20180613080857_rename_guest_token_to_token_in_orders.rb
@@ -0,0 +1,5 @@
+class RenameGuestTokenToTokenInOrders < ActiveRecord::Migration[5.2]
+ def change
+ rename_column :spree_orders, :guest_token, :token
+ end
+end
diff --git a/core/db/migrate/20180915160001_add_timestamps_to_spree_prices.rb b/core/db/migrate/20180915160001_add_timestamps_to_spree_prices.rb
new file mode 100644
index 00000000000..3e02aa60ae5
--- /dev/null
+++ b/core/db/migrate/20180915160001_add_timestamps_to_spree_prices.rb
@@ -0,0 +1,12 @@
+class AddTimestampsToSpreePrices < ActiveRecord::Migration[5.2]
+ def up
+ add_timestamps :spree_prices, default: Time.current
+ change_column_default :spree_prices, :created_at, nil
+ change_column_default :spree_prices, :updated_at, nil
+ end
+
+ def down
+ remove_column :spree_prices, :created_at
+ remove_column :spree_prices, :updated_at
+ end
+end
diff --git a/core/db/migrate/20181024100754_add_deleted_at_to_spree_credit_cards.rb b/core/db/migrate/20181024100754_add_deleted_at_to_spree_credit_cards.rb
new file mode 100644
index 00000000000..3814f7323d8
--- /dev/null
+++ b/core/db/migrate/20181024100754_add_deleted_at_to_spree_credit_cards.rb
@@ -0,0 +1,6 @@
+class AddDeletedAtToSpreeCreditCards < ActiveRecord::Migration[5.2]
+ def change
+ add_column :spree_credit_cards, :deleted_at, :datetime
+ add_index :spree_credit_cards, :deleted_at
+ end
+end
diff --git a/core/db/seeds.rb b/core/db/seeds.rb
new file mode 100644
index 00000000000..456e8cadccc
--- /dev/null
+++ b/core/db/seeds.rb
@@ -0,0 +1,5 @@
+# Loads seed data out of default dir
+default_path = File.join(File.dirname(__FILE__), 'default')
+
+Rake::Task['db:load_dir'].reenable
+Rake::Task['db:load_dir'].invoke(default_path)
diff --git a/core/lib/friendly_id/slug_rails5_patch.rb b/core/lib/friendly_id/slug_rails5_patch.rb
new file mode 100644
index 00000000000..1bb4c7c330e
--- /dev/null
+++ b/core/lib/friendly_id/slug_rails5_patch.rb
@@ -0,0 +1,11 @@
+# Temporary fix to FriendlyId in Rails5, so that we don't
+# encounter any validation errors when creating slugs via
+# FriendlyId::History on a paranoid model.
+# See: https://github.com/norman/friendly_id/issues/822
+if Rails::VERSION::STRING >= '5.0'
+ module FriendlyId
+ class Slug < ActiveRecord::Base
+ belongs_to :sluggable, polymorphic: true, optional: true
+ end
+ end
+end
diff --git a/core/lib/generators/spree/custom_user/custom_user_generator.rb b/core/lib/generators/spree/custom_user/custom_user_generator.rb
new file mode 100644
index 00000000000..fa8c8089f52
--- /dev/null
+++ b/core/lib/generators/spree/custom_user/custom_user_generator.rb
@@ -0,0 +1,51 @@
+module Spree
+ class CustomUserGenerator < Rails::Generators::NamedBase
+ include Rails::Generators::ResourceHelpers
+ include Rails::Generators::Migration
+
+ desc 'Set up a Spree installation with a custom User class'
+
+ def self.source_paths
+ paths = superclass.source_paths
+ paths << File.expand_path('templates', __dir__)
+ paths.flatten
+ end
+
+ def check_for_constant
+ klass
+ rescue NameError
+ @shell.say "Couldn't find #{class_name}. Are you sure that this class exists within your application and is loaded?", :red
+ exit(1)
+ end
+
+ def generate
+ migration_template 'migration.rb.tt', 'db/migrate/add_spree_fields_to_custom_user_table.rb'
+ template 'authentication_helpers.rb.tt', 'lib/spree/authentication_helpers.rb'
+
+ file_action = File.exist?('config/initializers/spree.rb') ? :append_file : :create_file
+ send(file_action, 'config/initializers/spree.rb') do
+ %Q{
+ Rails.application.config.to_prepare do
+ require_dependency 'spree/authentication_helpers'
+ end\n}
+ end
+ end
+
+ def self.next_migration_number(dirname)
+ if ApplicationRecord.timestamped_migrations
+ sleep 1 # make sure to get a different migration every time
+ Time.new.utc.strftime('%Y%m%d%H%M%S')
+ else
+ format('%.3d', (current_migration_number(dirname) + 1))
+ end
+ end
+
+ def klass
+ class_name.constantize
+ end
+
+ def table_name
+ klass.table_name
+ end
+ end
+end
diff --git a/core/lib/generators/spree/custom_user/templates/authentication_helpers.rb.tt b/core/lib/generators/spree/custom_user/templates/authentication_helpers.rb.tt
new file mode 100644
index 00000000000..6f301aa157f
--- /dev/null
+++ b/core/lib/generators/spree/custom_user/templates/authentication_helpers.rb.tt
@@ -0,0 +1,36 @@
+module Spree
+ module CurrentUserHelpers
+ def self.included(receiver)
+ receiver.send :helper_method, :spree_current_user
+ end
+
+ def spree_current_user
+ current_user
+ end
+ end
+
+ module AuthenticationHelpers
+ def self.included(receiver)
+ receiver.send :helper_method, :spree_login_path
+ receiver.send :helper_method, :spree_signup_path
+ receiver.send :helper_method, :spree_logout_path
+ end
+
+ def spree_login_path
+ main_app.login_path
+ end
+
+ def spree_signup_path
+ main_app.signup_path
+ end
+
+ def spree_logout_path
+ main_app.logout_path
+ end
+ end
+end
+
+ApplicationController.include Spree::AuthenticationHelpers
+ApplicationController.include Spree::CurrentUserHelpers
+
+Spree::Api::BaseController.include Spree::CurrentUserHelpers
diff --git a/core/lib/generators/spree/custom_user/templates/initializer.rb.tt b/core/lib/generators/spree/custom_user/templates/initializer.rb.tt
new file mode 100644
index 00000000000..b0b46438687
--- /dev/null
+++ b/core/lib/generators/spree/custom_user/templates/initializer.rb.tt
@@ -0,0 +1 @@
+Spree.user_class = "<%= class_name %>"
diff --git a/core/lib/generators/spree/custom_user/templates/migration.rb.tt b/core/lib/generators/spree/custom_user/templates/migration.rb.tt
new file mode 100644
index 00000000000..b21b1ee52c4
--- /dev/null
+++ b/core/lib/generators/spree/custom_user/templates/migration.rb.tt
@@ -0,0 +1,7 @@
+class AddSpreeFieldsToCustomUserTable < ActiveRecord::Migration[4.2]
+ def up
+ add_column <%= table_name.inspect %>, :spree_api_key, :string, limit: 48
+ add_column <%= table_name.inspect %>, :ship_address_id, :integer
+ add_column <%= table_name.inspect %>, :bill_address_id, :integer
+ end
+end
diff --git a/core/lib/generators/spree/dummy/dummy_generator.rb b/core/lib/generators/spree/dummy/dummy_generator.rb
new file mode 100644
index 00000000000..f5f8b266686
--- /dev/null
+++ b/core/lib/generators/spree/dummy/dummy_generator.rb
@@ -0,0 +1,152 @@
+require 'rails/generators/rails/app/app_generator'
+require 'active_support/core_ext/hash'
+require 'spree/core/version'
+
+module Spree
+ class DummyGenerator < Rails::Generators::Base
+ desc 'Creates blank Rails application, installs Spree and all sample data'
+
+ class_option :lib_name, default: ''
+ class_option :database, default: ''
+
+ def self.source_paths
+ paths = superclass.source_paths
+ paths << File.expand_path('templates', __dir__)
+ paths.flatten
+ end
+
+ def clean_up
+ remove_directory_if_exists(dummy_path)
+ end
+
+ PASSTHROUGH_OPTIONS = [
+ :skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip
+ ]
+
+ def generate_test_dummy
+ # calling slice on a Thor::CoreExtensions::HashWithIndifferentAccess
+ # object has been known to return nil
+ opts = {}.merge(options).slice(*PASSTHROUGH_OPTIONS)
+ opts[:database] = 'sqlite3' if opts[:database].blank?
+ opts[:force] = true
+ opts[:skip_bundle] = true
+ opts[:skip_gemfile] = true
+ opts[:skip_git] = true
+ opts[:skip_keeps] = true
+ opts[:skip_listen] = true
+ opts[:skip_rc] = true
+ opts[:skip_spring] = true
+ opts[:skip_test] = true
+ opts[:skip_yarn] = true
+
+ puts 'Generating dummy Rails application...'
+ invoke Rails::Generators::AppGenerator,
+ [File.expand_path(dummy_path, destination_root)], opts
+ end
+
+ def test_dummy_config
+ @lib_name = options[:lib_name]
+ @database = options[:database]
+
+ template 'rails/database.yml', "#{dummy_path}/config/database.yml", force: true
+ template 'rails/boot.rb', "#{dummy_path}/config/boot.rb", force: true
+ template 'rails/application.rb', "#{dummy_path}/config/application.rb", force: true
+ template 'rails/routes.rb', "#{dummy_path}/config/routes.rb", force: true
+ template 'rails/test.rb', "#{dummy_path}/config/environments/test.rb", force: true
+ template 'rails/script/rails', "#{dummy_path}/spec/dummy/script/rails", force: true
+ template 'initializers/devise.rb', "#{dummy_path}/config/initializers/devise.rb", force: true
+ end
+
+ def test_dummy_inject_extension_requirements
+ if DummyGeneratorHelper.inject_extension_requirements
+ inside dummy_path do
+ inject_require_for('spree_frontend')
+ inject_require_for('spree_backend')
+ inject_require_for('spree_api')
+ end
+ end
+ end
+
+ def test_dummy_clean
+ inside dummy_path do
+ remove_file '.gitignore'
+ remove_file 'doc'
+ remove_file 'Gemfile'
+ remove_file 'lib/tasks'
+ remove_file 'app/assets/images/rails.png'
+ remove_file 'app/assets/javascripts/application.js'
+ remove_file 'public/index.html'
+ remove_file 'public/robots.txt'
+ remove_file 'README'
+ remove_file 'test'
+ remove_file 'vendor'
+ remove_file 'spec'
+ end
+ end
+
+ def inject_content_security_policy
+ inside dummy_path do
+ inject_into_file 'config/initializers/content_security_policy.rb', %Q[
+ p.script_src :self, :https, :unsafe_inline, :http, :unsafe_eval
+ ], before: /^end/, verbose: true
+ end
+ end
+
+ attr_reader :lib_name
+ attr_reader :database
+
+ protected
+
+ def inject_require_for(requirement)
+ inject_into_file 'config/application.rb', %Q[
+begin
+ require '#{requirement}'
+rescue LoadError
+ # #{requirement} is not available.
+end
+ ], before: /require '#{@lib_name}'/, verbose: true
+ end
+
+ def dummy_path
+ ENV['DUMMY_PATH'] || 'spec/dummy'
+ end
+
+ def module_name
+ 'Dummy'
+ end
+
+ def application_definition
+ @application_definition ||= begin
+ dummy_application_path = File.expand_path("#{dummy_path}/config/application.rb", destination_root)
+ unless options[:pretend] || !File.exist?(dummy_application_path)
+ contents = File.read(dummy_application_path)
+ contents[(contents.index("module #{module_name}"))..-1]
+ end
+ end
+ end
+ alias store_application_definition! application_definition
+
+ def camelized
+ @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize
+ end
+
+ def remove_directory_if_exists(path)
+ remove_dir(path) if File.directory?(path)
+ end
+
+ def gemfile_path
+ core_gems = ['spree/core', 'spree/api', 'spree/backend', 'spree/frontend']
+
+ if core_gems.include?(lib_name)
+ '../../../../../Gemfile'
+ else
+ '../../../../Gemfile'
+ end
+ end
+ end
+end
+
+module Spree::DummyGeneratorHelper
+ mattr_accessor :inject_extension_requirements
+ self.inject_extension_requirements = false
+end
diff --git a/core/lib/generators/spree/dummy/templates/initializers/devise.rb b/core/lib/generators/spree/dummy/templates/initializers/devise.rb
new file mode 100644
index 00000000000..7dff9547df5
--- /dev/null
+++ b/core/lib/generators/spree/dummy/templates/initializers/devise.rb
@@ -0,0 +1,3 @@
+if Object.const_defined?("Devise")
+ Devise.secret_key = "<%= SecureRandom.hex(50) %>"
+end
\ No newline at end of file
diff --git a/core/lib/generators/spree/dummy/templates/rails/application.rb b/core/lib/generators/spree/dummy/templates/rails/application.rb
new file mode 100644
index 00000000000..e387ded83ba
--- /dev/null
+++ b/core/lib/generators/spree/dummy/templates/rails/application.rb
@@ -0,0 +1,10 @@
+require File.expand_path('../boot', __FILE__)
+
+require 'rails/all'
+
+Bundler.require(*Rails.groups(assets: %w(development test)))
+
+require '<%= lib_name %>'
+
+<%= application_definition %>
+
diff --git a/core/lib/generators/spree/dummy/templates/rails/boot.rb b/core/lib/generators/spree/dummy/templates/rails/boot.rb
new file mode 100644
index 00000000000..7c8a8d2d330
--- /dev/null
+++ b/core/lib/generators/spree/dummy/templates/rails/boot.rb
@@ -0,0 +1,6 @@
+require 'rubygems'
+gemfile = File.expand_path("<%= gemfile_path %>", __FILE__)
+
+ENV['BUNDLE_GEMFILE'] = gemfile
+require 'bundler'
+Bundler.setup
diff --git a/core/lib/generators/spree/dummy/templates/rails/database.yml b/core/lib/generators/spree/dummy/templates/rails/database.yml
new file mode 100644
index 00000000000..beea57f74e9
--- /dev/null
+++ b/core/lib/generators/spree/dummy/templates/rails/database.yml
@@ -0,0 +1,80 @@
+<% if agent_number = ENV['TC_AGENT_NUMBER']
+database_prefix = agent_number + '_'
+end %>
+<% if options[:lib_name]
+ lib_name = options[:lib_name].gsub('/', '_')
+end %>
+<% db_password = ENV['DB_PASSWORD'] %>
+<% db_username = ENV['DB_USERNAME'] %>
+<% db_host = ENV['DB_HOST'] %>
+<% case ENV['DB']
+ when 'sqlite' %>
+development:
+ adapter: sqlite3
+ database: db/spree_development.sqlite3
+test:
+ adapter: sqlite3
+ database: db/spree_test.sqlite3
+ timeout: 10000
+production:
+ adapter: sqlite3
+ database: db/spree_production.sqlite3
+<% when 'mysql' %>
+mysql: &mysql
+ adapter: mysql2
+ encoding: utf8
+ <% unless db_username.blank? %>
+ username: <%= db_username %>
+ <% end %>
+ <% unless db_password.blank? %>
+ password: <%= db_password %>
+ <% end %>
+ <% unless db_host.blank? %>
+ host: <%= db_host %>
+ <% end %>
+ reconnect: true
+ pool: 5
+
+development:
+ <<: *mysql
+ database: <%= database_prefix %><%= lib_name %>_spree_development
+test:
+ <<: *mysql
+ database: <%= database_prefix %><%= lib_name %>_spree_test
+production:
+ <<: *mysql
+ database: <%= database_prefix %><%= lib_name %>_spree_production
+<% when 'postgres' %>
+postgres: &postgres
+ adapter: postgresql
+ <% unless db_username.blank? %>
+ username: <%= db_username || 'postgres' %>
+ <% end %>
+ <% unless db_password.blank? %>
+ password: <%= db_password %>
+ <% end %>
+ <% unless db_host.blank? %>
+ host: <%= db_host %>
+ <% end %>
+ min_messages: warning
+
+development:
+ <<: *postgres
+ database: <%= database_prefix %><%= lib_name %>_spree_development
+test:
+ <<: *postgres
+ database: <%= database_prefix %><%= lib_name %>_spree_test
+production:
+ <<: *postgres
+ database: <%= database_prefix %><%= lib_name %>_spree_production
+<% else %>
+development:
+ adapter: sqlite3
+ database: db/spree_development.sqlite3
+test:
+ adapter: sqlite3
+ database: db/spree_test.sqlite3
+production:
+ adapter: sqlite3
+ database: db/spree_production.sqlite3
+<% end %>
diff --git a/core/lib/generators/spree/dummy/templates/rails/routes.rb b/core/lib/generators/spree/dummy/templates/rails/routes.rb
new file mode 100644
index 00000000000..1daf9a4121a
--- /dev/null
+++ b/core/lib/generators/spree/dummy/templates/rails/routes.rb
@@ -0,0 +1,2 @@
+Rails.application.routes.draw do
+end
diff --git a/core/lib/generators/spree/dummy/templates/rails/script/rails b/core/lib/generators/spree/dummy/templates/rails/script/rails
new file mode 100644
index 00000000000..f8da2cffd4d
--- /dev/null
+++ b/core/lib/generators/spree/dummy/templates/rails/script/rails
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
+
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require File.expand_path('../../config/boot', __FILE__)
+require 'rails/commands'
diff --git a/core/lib/generators/spree/dummy/templates/rails/test.rb b/core/lib/generators/spree/dummy/templates/rails/test.rb
new file mode 100644
index 00000000000..eb005f5cffd
--- /dev/null
+++ b/core/lib/generators/spree/dummy/templates/rails/test.rb
@@ -0,0 +1,38 @@
+Dummy::Application.configure do
+ # Settings specified here will take precedence over those in config/application.rb
+
+ # The test environment is used exclusively to run your application's
+ # test suite. You never need to work with it otherwise. Remember that
+ # your test database is "scratch space" for the test suite and is wiped
+ # and recreated between test runs. Don't rely on the data there!
+ config.cache_classes = true
+
+ # Configure static asset server for tests with Cache-Control for performance
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' }
+
+ # Show full error reports and disable caching
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ config.eager_load = false
+
+ # Raise exceptions instead of rendering exception templates
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment
+ config.action_controller.allow_forgery_protection = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+ ActionMailer::Base.default from: "spree@example.com"
+ # Store uploaded files on the local file system in a temporary directory
+ config.active_storage.service = :test
+
+ # Print deprecation notices to the stderr
+ config.active_support.deprecation = :stderr
+
+ config.active_job.queue_adapter = :test
+end
diff --git a/core/lib/generators/spree/dummy_model/dummy_model_generator.rb b/core/lib/generators/spree/dummy_model/dummy_model_generator.rb
new file mode 100644
index 00000000000..6d260d74559
--- /dev/null
+++ b/core/lib/generators/spree/dummy_model/dummy_model_generator.rb
@@ -0,0 +1,23 @@
+module Spree
+ class DummyModelGenerator < Rails::Generators::NamedBase
+ include Rails::Generators::ResourceHelpers
+ include Rails::Generators::Migration
+
+ desc 'Set up Dummy Model which is used for tests'
+
+ def self.source_paths
+ paths = superclass.source_paths
+ paths << File.expand_path('templates', __dir__)
+ paths.flatten
+ end
+
+ def generate
+ migration_template 'migration.rb.tt', 'db/migrate/create_spree_dummy_models.rb'
+ template 'model.rb.tt', 'app/models/spree/dummy_model.rb'
+ end
+
+ def self.next_migration_number(dirname)
+ format('%.3d', (current_migration_number(dirname) + 1))
+ end
+ end
+end
diff --git a/core/lib/generators/spree/dummy_model/templates/migration.rb.tt b/core/lib/generators/spree/dummy_model/templates/migration.rb.tt
new file mode 100644
index 00000000000..98df831cdd7
--- /dev/null
+++ b/core/lib/generators/spree/dummy_model/templates/migration.rb.tt
@@ -0,0 +1,10 @@
+class CreateSpreeDummyModels < ActiveRecord::Migration[5.1]
+ def change
+ create_table :spree_dummy_models do |t|
+ t.string :name
+ t.integer :position
+
+ t.timestamps
+ end
+ end
+end
diff --git a/core/lib/generators/spree/dummy_model/templates/model.rb.tt b/core/lib/generators/spree/dummy_model/templates/model.rb.tt
new file mode 100644
index 00000000000..fb0846746e8
--- /dev/null
+++ b/core/lib/generators/spree/dummy_model/templates/model.rb.tt
@@ -0,0 +1,6 @@
+module Spree
+ class Spree::DummyModel < ApplicationRecord
+ acts_as_list
+ validates :name, presence: true
+ end
+end
diff --git a/core/lib/generators/spree/install/install_generator.rb b/core/lib/generators/spree/install/install_generator.rb
new file mode 100644
index 00000000000..f84b57e3e9d
--- /dev/null
+++ b/core/lib/generators/spree/install/install_generator.rb
@@ -0,0 +1,252 @@
+require 'rails/generators'
+require 'highline/import'
+require 'bundler'
+require 'bundler/cli'
+require 'active_support/core_ext/string/indent'
+require 'spree/core'
+
+module Spree
+ class InstallGenerator < Rails::Generators::Base
+ class_option :migrate, type: :boolean, default: true, banner: 'Run Spree migrations'
+ class_option :seed, type: :boolean, default: true, banner: 'load seed data (migrations must be run)'
+ class_option :sample, type: :boolean, default: true, banner: 'load sample data (migrations must be run)'
+ class_option :copy_views, type: :boolean, default: true, banner: 'copy frontend views from spree to your application for easy customization'
+ class_option :auto_accept, type: :boolean
+ class_option :user_class, type: :string
+ class_option :admin_email, type: :string
+ class_option :admin_password, type: :string
+ class_option :lib_name, type: :string, default: 'spree'
+ class_option :enforce_available_locales, type: :boolean, default: nil
+
+ def self.source_paths
+ paths = superclass.source_paths
+ paths << File.expand_path('../templates', "../../#{__FILE__}")
+ paths << File.expand_path('../templates', "../#{__FILE__}")
+ paths << File.expand_path('templates', __dir__)
+ paths.flatten
+ end
+
+ def prepare_options
+ @run_migrations = options[:migrate]
+ @load_seed_data = options[:seed]
+ @load_sample_data = options[:sample]
+ @copy_views = options[:copy_views]
+
+ unless @run_migrations
+ @load_seed_data = false
+ @load_sample_data = false
+ end
+ end
+
+ def add_files
+ template 'config/initializers/spree.rb', 'config/initializers/spree.rb'
+ end
+
+ def additional_tweaks
+ return unless File.exist? 'public/robots.txt'
+
+ append_file 'public/robots.txt', <<-ROBOTS.strip_heredoc
+ User-agent: *
+ Disallow: /checkout
+ Disallow: /cart
+ Disallow: /orders
+ Disallow: /user
+ Disallow: /account
+ Disallow: /api
+ Disallow: /password
+ ROBOTS
+ end
+
+ def setup_assets
+ @lib_name = 'spree'
+ %w{javascripts stylesheets images}.each do |path|
+ if Spree::Core::Engine.frontend_available? || Rails.env.test?
+ empty_directory "vendor/assets/#{path}/spree/frontend"
+ end
+ if Spree::Core::Engine.backend_available? || Rails.env.test?
+ empty_directory "vendor/assets/#{path}/spree/backend"
+ end
+ end
+
+ if Spree::Core::Engine.frontend_available? || Rails.env.test?
+ template 'vendor/assets/javascripts/spree/frontend/all.js'
+ template 'vendor/assets/stylesheets/spree/frontend/all.css'
+ end
+
+ if Spree::Core::Engine.backend_available? || Rails.env.test?
+ template 'vendor/assets/javascripts/spree/backend/all.js'
+ template 'vendor/assets/stylesheets/spree/backend/all.css'
+ end
+ end
+
+ def create_overrides_directory
+ empty_directory 'app/overrides'
+ end
+
+ def copy_views
+ if @copy_views && Spree::Core::Engine.frontend_available?
+ generate 'spree:frontend:copy_views'
+ end
+ end
+
+ def configure_application
+ application <<-APP.strip_heredoc.indent!(4)
+
+ config.to_prepare do
+ # Load application's model / class decorators
+ Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c|
+ Rails.configuration.cache_classes ? require(c) : load(c)
+ end
+
+ # Load application's view overrides
+ Dir.glob(File.join(File.dirname(__FILE__), "../app/overrides/*.rb")) do |c|
+ Rails.configuration.cache_classes ? require(c) : load(c)
+ end
+ end
+ APP
+
+ unless options[:enforce_available_locales].nil?
+ application <<-APP.strip_heredoc.indent!(4)
+ # Prevent this deprecation message: https://github.com/svenfuchs/i18n/commit/3b6e56e
+ I18n.enforce_available_locales = #{options[:enforce_available_locales]}
+ APP
+ end
+ end
+
+ def include_seed_data
+ append_file 'db/seeds.rb', <<-SEEDS.strip_heredoc
+
+ Spree::Core::Engine.load_seed if defined?(Spree::Core)
+ Spree::Auth::Engine.load_seed if defined?(Spree::Auth)
+ SEEDS
+ end
+
+ def install_migrations
+ say_status :copying, 'migrations'
+ silence_stream(STDOUT) do
+ silence_warnings { rake 'railties:install:migrations' }
+ end
+ end
+
+ def create_database
+ say_status :creating, 'database'
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ silence_warnings { rake 'db:create' }
+ end
+ end
+ end
+
+ def run_migrations
+ if @run_migrations
+ say_status :running, 'migrations'
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ silence_warnings { rake 'db:migrate' }
+ end
+ end
+ else
+ say_status :skipping, "migrations (don't forget to run rake db:migrate)"
+ end
+ end
+
+ def populate_seed_data
+ if @load_seed_data
+ say_status :loading, 'seed data'
+ rake_options = []
+ rake_options << 'AUTO_ACCEPT=1' if options[:auto_accept]
+ rake_options << "ADMIN_EMAIL=#{options[:admin_email]}" if options[:admin_email]
+ rake_options << "ADMIN_PASSWORD=#{options[:admin_password]}" if options[:admin_password]
+
+ cmd = -> { rake("db:seed #{rake_options.join(' ')}") }
+ if options[:auto_accept] || (options[:admin_email] && options[:admin_password])
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ silence_warnings &cmd
+ end
+ end
+ else
+ cmd.call
+ end
+ else
+ say_status :skipping, 'seed data (you can always run rake db:seed)'
+ end
+ end
+
+ def load_sample_data
+ if @load_sample_data
+ say_status :loading, 'sample data'
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ silence_warnings { rake 'spree_sample:load' }
+ end
+ end
+ else
+ say_status :skipping, 'sample data (you can always run rake spree_sample:load)'
+ end
+ end
+
+ def notify_about_routes
+ insert_into_file(File.join('config', 'routes.rb'),
+ after: "Rails.application.routes.draw do\n") do
+ <<-ROUTES.strip_heredoc.indent!(2)
+ # This line mounts Spree's routes at the root of your application.
+ # This means, any requests to URLs such as /products, will go to
+ # Spree::ProductsController.
+ # If you would like to change where this engine is mounted, simply change the
+ # :at option to something different.
+ #
+ # We ask that you don't use the :as option here, as Spree relies on it being
+ # the default of "spree".
+ mount Spree::Core::Engine, at: '/'
+ ROUTES
+ end
+
+ unless options[:quiet]
+ puts '*' * 50
+ puts "We added the following line to your application's config/routes.rb file:"
+ puts ' '
+ puts " mount Spree::Core::Engine, at: '/'"
+ end
+ end
+
+ def complete
+ unless options[:quiet]
+ puts '*' * 50
+ puts "Spree has been installed successfully. You're all ready to go!"
+ puts ' '
+ puts 'Enjoy!'
+ end
+ end
+
+ protected
+
+ def javascript_exists?(script)
+ extensions = %w(.js.coffee .js.erb .js.coffee.erb .js)
+ file_exists?(extensions, script)
+ end
+
+ def stylesheet_exists?(stylesheet)
+ extensions = %w(.css.scss .css.erb .css.scss.erb .css)
+ file_exists?(extensions, stylesheet)
+ end
+
+ def file_exists?(extensions, filename)
+ extensions.detect do |extension|
+ File.exist?("#{filename}#{extension}")
+ end
+ end
+
+ private
+
+ def silence_stream(stream)
+ old_stream = stream.dup
+ stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
+ stream.sync = true
+ yield
+ ensure
+ stream.reopen(old_stream)
+ old_stream.close
+ end
+ end
+end
diff --git a/core/lib/generators/spree/install/templates/config/initializers/spree.rb b/core/lib/generators/spree/install/templates/config/initializers/spree.rb
new file mode 100644
index 00000000000..621e0b93061
--- /dev/null
+++ b/core/lib/generators/spree/install/templates/config/initializers/spree.rb
@@ -0,0 +1,30 @@
+# Configure Spree Preferences
+#
+# Note: Initializing preferences available within the Admin will overwrite any changes that were made through the user interface when you restart.
+# If you would like users to be able to update a setting with the Admin it should NOT be set here.
+#
+# Note: If a preference is set here it will be stored within the cache & database upon initialization.
+# Just removing an entry from this initializer will not make the preference value go away.
+# Instead you must either set a new value or remove entry, clear cache, and remove database entry.
+#
+# In order to initialize a setting do:
+# config.setting_name = 'new value'
+Spree.config do |config|
+ # Example:
+ # Uncomment to stop tracking inventory levels in the application
+ # config.track_inventory_levels = false
+end
+
+# Configure Spree Dependencies
+#
+# Note: If a dependency is set here it will NOT be stored within the cache & database upon initialization.
+# Just removing an entry from this initializer will make the dependency value go away.
+#
+Spree.dependencies do |dependencies|
+ # Example:
+ # Uncomment to change the default Service handling adding Items to Cart
+ # dependencies.cart_add_item_service = 'MyNewAwesomeService'
+end
+
+
+Spree.user_class = <%= (options[:user_class].blank? ? 'Spree::LegacyUser' : options[:user_class]).inspect %>
diff --git a/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/backend/all.js b/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/backend/all.js
new file mode 100644
index 00000000000..ef041560250
--- /dev/null
+++ b/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/backend/all.js
@@ -0,0 +1,17 @@
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
+//= require jquery
+//= require jquery_ujs
+//= require spree/backend
+<% unless options[:lib_name] == 'spree' || options[:lib_name] == 'spree/backend' %>
+ <% filename = "spree/backend/#{ options[:lib_name].gsub("/", "_") }" %>
+ <% filepath = File.join(File.dirname(__FILE__), "../../app/assets/javascripts/#{filename}") %>
+ <% if javascript_exists?(filepath) %>
+ //= require <%= filename %>
+ <% end %>
+<% end %>
+//= require_tree .
diff --git a/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/frontend/all.js b/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/frontend/all.js
new file mode 100644
index 00000000000..d8d2d7a694f
--- /dev/null
+++ b/core/lib/generators/spree/install/templates/vendor/assets/javascripts/spree/frontend/all.js
@@ -0,0 +1,18 @@
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
+//= require jquery
+//= require jquery_ujs
+//= require accounting.min
+//= require spree/frontend
+<% unless options[:lib_name] == 'spree' || options[:lib_name] == 'spree/frontend' %>
+ <% filename = "spree/frontend/#{ options[:lib_name].gsub("/", "_") }" %>
+ <% filepath = File.join(File.dirname(__FILE__), "../../app/assets/javascripts/#{ filename }") %>
+ <% if javascript_exists?(filepath) %>
+ //= require <%= filename %>
+ <% end %>
+<% end %>
+//= require_tree .
diff --git a/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/backend/all.css b/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/backend/all.css
new file mode 100644
index 00000000000..be13f93e671
--- /dev/null
+++ b/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/backend/all.css
@@ -0,0 +1,16 @@
+/*
+ * This is a manifest file that'll automatically include all the stylesheets available in this directory
+ * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
+ * the top of the compiled file, but it's generally better to create a new file per style scope.
+ *
+ *= require spree/backend
+<% unless options[:lib_name] == 'spree' || options[:lib_name] == 'spree/backend' %>
+ <% filename = "spree/backend/#{ options[:lib_name].gsub("/", "_") }" %>
+ <% filepath = File.join(File.dirname(__FILE__), "../../app/assets/stylesheets/#{ filename }") %>
+ <% if stylesheet_exists?(filepath) %>
+ *= require <%= filename %>
+ <% end %>
+<% end %>
+ *= require_self
+ *= require_tree .
+*/
diff --git a/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/frontend/all.css b/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/frontend/all.css
new file mode 100644
index 00000000000..e227bd4dd03
--- /dev/null
+++ b/core/lib/generators/spree/install/templates/vendor/assets/stylesheets/spree/frontend/all.css
@@ -0,0 +1,16 @@
+/*
+ * This is a manifest file that'll automatically include all the stylesheets available in this directory
+ * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
+ * the top of the compiled file, but it's generally better to create a new file per style scope.
+ *
+ *= require spree/frontend
+<% unless options[:lib_name] == 'spree' || options[:lib_name] == 'spree/frontend' %>
+ <% filename = "spree/frontend/#{ options[:lib_name].gsub("/", "_") }" %>
+ <% filepath = File.join(File.dirname(__FILE__), "../../app/assets/stylesheets/#{ filename }") %>
+ <% if stylesheet_exists?(filepath) %>
+ *= require <%= filename %>
+ <% end %>
+<% end %>
+ *= require_self
+ *= require_tree .
+*/
diff --git a/core/lib/spree/core.rb b/core/lib/spree/core.rb
new file mode 100644
index 00000000000..ad1cd51642b
--- /dev/null
+++ b/core/lib/spree/core.rb
@@ -0,0 +1,107 @@
+require 'rails/all'
+require 'active_merchant'
+require 'acts_as_list'
+require 'awesome_nested_set'
+require 'cancan'
+require 'friendly_id'
+require 'kaminari'
+require 'mail'
+require 'monetize'
+require 'paperclip'
+require 'paranoia'
+require 'mini_magick'
+require 'premailer/rails'
+require 'ransack'
+require 'responders'
+require 'state_machines-activerecord'
+
+# This is required because ActiveModel::Validations#invalid? conflicts with the
+# invalid state of a Payment. In the future this should be removed.
+StateMachines::Machine.ignore_method_conflicts = true
+
+module Spree
+ mattr_accessor :user_class
+
+ def self.user_class(constantize: true)
+ if @@user_class.is_a?(Class)
+ raise 'Spree.user_class MUST be a String or Symbol object, not a Class object.'
+ elsif @@user_class.is_a?(String) || @@user_class.is_a?(Symbol)
+ constantize ? @@user_class.to_s.constantize : @@user_class.to_s
+ end
+ end
+
+ def self.admin_path
+ Spree::Config[:admin_path]
+ end
+
+ # Used to configure admin_path for Spree
+ #
+ # Example:
+ #
+ # write the following line in `config/initializers/spree.rb`
+ # Spree.admin_path = '/custom-path'
+
+ def self.admin_path=(path)
+ Spree::Config[:admin_path] = path
+ end
+
+ # Used to configure Spree.
+ #
+ # Example:
+ #
+ # Spree.config do |config|
+ # config.track_inventory_levels = false
+ # end
+ #
+ # This method is defined within the core gem on purpose.
+ # Some people may only wish to use the Core part of Spree.
+ def self.config
+ yield(Spree::Config)
+ end
+
+ # Used to set dependencies for Spree.
+ #
+ # Example:
+ #
+ # Spree.dependencies do |dependency|
+ # dependency.cart_add_item_service = MyCustomAddToCart
+ # end
+ #
+ # This method is defined within the core gem on purpose.
+ # Some people may only wish to use the Core part of Spree.
+ def self.dependencies
+ yield(Spree::Dependencies)
+ end
+
+ module Core
+ autoload :ProductFilters, 'spree/core/product_filters'
+ autoload :TokenGenerator, 'spree/core/token_generator'
+
+ class GatewayError < RuntimeError; end
+ class DestroyWithOrdersError < StandardError; end
+ end
+end
+
+require 'spree/core/version'
+
+require 'spree/core/number_generator'
+require 'spree/migrations'
+require 'spree/core/engine'
+
+require 'spree/i18n'
+require 'spree/localized_number'
+require 'spree/money'
+require 'spree/permitted_attributes'
+require 'spree/service_module'
+require 'spree/dependencies_helper'
+
+require 'spree/core/importer'
+require 'spree/core/query_filters'
+require 'spree/core/product_duplicator'
+require 'spree/core/controller_helpers/auth'
+require 'spree/core/controller_helpers/common'
+require 'spree/core/controller_helpers/order'
+require 'spree/core/controller_helpers/respond_with'
+require 'spree/core/controller_helpers/search'
+require 'spree/core/controller_helpers/store'
+require 'spree/core/controller_helpers/strong_parameters'
diff --git a/core/lib/spree/core/components.rb b/core/lib/spree/core/components.rb
new file mode 100644
index 00000000000..29b035c4a74
--- /dev/null
+++ b/core/lib/spree/core/components.rb
@@ -0,0 +1,17 @@
+module Spree
+ module Core
+ class Engine < ::Rails::Engine
+ def self.api_available?
+ @@api_available ||= ::Rails::Engine.subclasses.map(&:instance).map { |e| e.class.to_s }.include?('Spree::Api::Engine')
+ end
+
+ def self.backend_available?
+ @@backend_available ||= ::Rails::Engine.subclasses.map(&:instance).map { |e| e.class.to_s }.include?('Spree::Backend::Engine')
+ end
+
+ def self.frontend_available?
+ @@frontend_available ||= ::Rails::Engine.subclasses.map(&:instance).map { |e| e.class.to_s }.include?('Spree::Frontend::Engine')
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/controller_helpers/auth.rb b/core/lib/spree/core/controller_helpers/auth.rb
new file mode 100644
index 00000000000..f47f38bb26b
--- /dev/null
+++ b/core/lib/spree/core/controller_helpers/auth.rb
@@ -0,0 +1,91 @@
+module Spree
+ module Core
+ module ControllerHelpers
+ module Auth
+ extend ActiveSupport::Concern
+ include Spree::Core::TokenGenerator
+
+ included do
+ before_action :set_token
+ helper_method :try_spree_current_user
+
+ rescue_from CanCan::AccessDenied do |_exception|
+ redirect_unauthorized_access
+ end
+ end
+
+ # Needs to be overriden so that we use Spree's Ability rather than anyone else's.
+ def current_ability
+ @current_ability ||= Spree::Dependencies.ability_class.constantize.new(try_spree_current_user)
+ end
+
+ def redirect_back_or_default(default)
+ redirect_to(session['spree_user_return_to'] || request.env['HTTP_REFERER'] || default)
+ session['spree_user_return_to'] = nil
+ end
+
+ def set_token
+ cookies.permanent.signed[:token] ||= cookies.signed[:guest_token]
+ cookies.permanent.signed[:token] ||= {
+ value: generate_token,
+ httponly: true
+ }
+ cookies.permanent.signed[:guest_token] ||= cookies.permanent.signed[:token]
+ end
+
+ def current_oauth_token
+ user = try_spree_current_user
+ return unless user
+
+ @current_oauth_token ||= Doorkeeper::AccessToken.active_for(user).last || Doorkeeper::AccessToken.create!(resource_owner_id: user.id)
+ end
+
+ def store_location
+ # disallow return to login, logout, signup pages
+ authentication_routes = [:spree_signup_path, :spree_login_path, :spree_logout_path]
+ disallowed_urls = []
+ authentication_routes.each do |route|
+ disallowed_urls << send(route) if respond_to?(route)
+ end
+
+ disallowed_urls.map! { |url| url[/\/\w+$/] }
+ unless disallowed_urls.include?(request.fullpath)
+ session['spree_user_return_to'] = request.fullpath.gsub('//', '/')
+ end
+ end
+
+ # proxy method to *possible* spree_current_user method
+ # Authentication extensions (such as spree_auth_devise) are meant to provide spree_current_user
+ def try_spree_current_user
+ # This one will be defined by apps looking to hook into Spree
+ # As per authentication_helpers.rb
+ if respond_to?(:spree_current_user)
+ spree_current_user
+ # This one will be defined by Devise
+ elsif respond_to?(:current_spree_user)
+ current_spree_user
+ end
+ end
+
+ # Redirect as appropriate when an access request fails. The default action is to redirect to the login screen.
+ # Override this method in your controllers if you want to have special behavior in case the user is not authorized
+ # to access the requested action. For example, a popup window might simply close itself.
+ def redirect_unauthorized_access
+ if try_spree_current_user
+ flash[:error] = Spree.t(:authorization_failure)
+ redirect_to spree.forbidden_path
+ else
+ store_location
+ if respond_to?(:spree_login_path)
+ redirect_to spree_login_path
+ elsif spree.respond_to?(:root_path)
+ redirect_to spree.root_path
+ else
+ redirect_to main_app.respond_to?(:root_path) ? main_app.root_path : '/'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/controller_helpers/common.rb b/core/lib/spree/core/controller_helpers/common.rb
new file mode 100644
index 00000000000..e86830e952e
--- /dev/null
+++ b/core/lib/spree/core/controller_helpers/common.rb
@@ -0,0 +1,66 @@
+module Spree
+ module Core
+ module ControllerHelpers
+ module Common
+ extend ActiveSupport::Concern
+ included do
+ helper_method :title
+ helper_method :title=
+ helper_method :accurate_title
+
+ layout :get_layout
+
+ before_action :set_user_language
+
+ protected
+
+ # can be used in views as well as controllers.
+ # e.g. <% self.title = 'This is a custom title for this view' %>
+ attr_writer :title
+
+ def title
+ title_string = @title.present? ? @title : accurate_title
+ if title_string.present?
+ if Spree::Config[:always_put_site_name_in_title]
+ [title_string, default_title].join(" #{Spree::Config[:title_site_name_separator]} ")
+ else
+ title_string
+ end
+ else
+ default_title
+ end
+ end
+
+ def default_title
+ current_store.name
+ end
+
+ # this is a hook for subclasses to provide title
+ def accurate_title
+ current_store.seo_title
+ end
+
+ private
+
+ def set_user_language
+ locale = session[:locale]
+ locale = config_locale if respond_to?(:config_locale, true) && locale.blank?
+ locale = Rails.application.config.i18n.default_locale if locale.blank?
+ locale = I18n.default_locale unless I18n.available_locales.map(&:to_s).include?(locale.to_s)
+ I18n.locale = locale
+ end
+
+ # Returns which layout to render.
+ #
+ # You can set the layout you want to render inside your Spree configuration with the +:layout+ option.
+ #
+ # Default layout is: +app/views/spree/layouts/spree_application+
+ #
+ def get_layout
+ layout ||= Spree::Config[:layout]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/controller_helpers/order.rb b/core/lib/spree/core/controller_helpers/order.rb
new file mode 100644
index 00000000000..80c5d0601e8
--- /dev/null
+++ b/core/lib/spree/core/controller_helpers/order.rb
@@ -0,0 +1,97 @@
+module Spree
+ module Core
+ module ControllerHelpers
+ module Order
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :set_current_order
+
+ helper_method :current_order
+ helper_method :simple_current_order
+ end
+
+ # Used in the link_to_cart helper.
+ def simple_current_order
+ return @simple_current_order if @simple_current_order
+
+ @simple_current_order = find_order_by_token_or_user
+
+ if @simple_current_order
+ @simple_current_order.last_ip_address = ip_address
+ return @simple_current_order
+ else
+ @simple_current_order = Spree::Order.new
+ end
+ end
+
+ # The current incomplete order from the token for use in cart and during checkout
+ def current_order(options = {})
+ options[:create_order_if_necessary] ||= false
+
+ if @current_order
+ @current_order.last_ip_address = ip_address
+ return @current_order
+ end
+
+ @current_order = find_order_by_token_or_user(options, true)
+
+ if options[:create_order_if_necessary] && (@current_order.nil? || @current_order.completed?)
+ @current_order = Spree::Order.create!(current_order_params)
+ @current_order.associate_user! try_spree_current_user if try_spree_current_user
+ @current_order.last_ip_address = ip_address
+ end
+
+ @current_order
+ end
+
+ def associate_user
+ @order ||= current_order
+ if try_spree_current_user && @order
+ @order.associate_user!(try_spree_current_user) if @order.user.blank? || @order.email.blank?
+ end
+ end
+
+ def set_current_order
+ if try_spree_current_user && current_order
+ try_spree_current_user.orders.incomplete.where('id != ?', current_order.id).each do |order|
+ current_order.merge!(order, try_spree_current_user)
+ end
+ end
+ end
+
+ def ip_address
+ request.remote_ip
+ end
+
+ private
+
+ def last_incomplete_order
+ @last_incomplete_order ||= try_spree_current_user.last_incomplete_spree_order(current_store)
+ end
+
+ def current_order_params
+ { currency: current_currency, token: cookies.signed[:token], store_id: current_store.id, user_id: try_spree_current_user.try(:id) }
+ end
+
+ def find_order_by_token_or_user(options = {}, with_adjustments = false)
+ options[:lock] ||= false
+
+ # Find any incomplete orders for the token
+ incomplete_orders = Spree::Order.incomplete.includes(line_items: [variant: [:images, :option_values, :product]])
+ token_order_params = current_order_params.except(:user_id)
+ order = if with_adjustments
+ incomplete_orders.includes(:adjustments).lock(options[:lock]).find_by(token_order_params)
+ else
+ incomplete_orders.lock(options[:lock]).find_by(token_order_params)
+ end
+
+ # Find any incomplete orders for the current user
+ order = last_incomplete_order if order.nil? && try_spree_current_user
+
+ order
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/controller_helpers/respond_with.rb b/core/lib/spree/core/controller_helpers/respond_with.rb
new file mode 100644
index 00000000000..65d3b279430
--- /dev/null
+++ b/core/lib/spree/core/controller_helpers/respond_with.rb
@@ -0,0 +1,67 @@
+require 'spree/responder'
+
+module ActionController
+ class Base
+ def respond_with(*resources, &block)
+ if Spree::BaseController.spree_responders.key?(self.class.to_s.to_sym)
+ # Checkout AS Array#extract_options! and original respond_with
+ # implementation for a better picture of this hack
+ if resources.last.is_a? Hash
+ resources.last[:action_name] = action_name.to_sym
+ else
+ resources.push action_name: action_name.to_sym
+ end
+ end
+
+ super
+ end
+ end
+end
+
+module Spree
+ module Core
+ module ControllerHelpers
+ module RespondWith
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor :spree_responders
+ self.spree_responders = {}
+ self.responder = Spree::Responder
+ end
+
+ module ClassMethods
+ def clear_overrides!
+ self.spree_responders = {}
+ end
+
+ def respond_override(options = {})
+ ActiveSupport::Deprecation.warn 'ControllerHelpers::RespondWith is deprecated and will be removed in Spree 4.0.'
+
+ unless options.blank?
+ action_name = options.keys.first
+ action_value = options.values.first
+
+ if action_name.blank? || action_value.blank?
+ raise ArgumentError, "invalid values supplied #{options.inspect}"
+ end
+
+ format_name = action_value.keys.first
+ format_value = action_value.values.first
+
+ if format_name.blank? || format_value.blank?
+ raise ArgumentError, "invalid values supplied #{options.inspect}"
+ end
+
+ if format_value.is_a?(Proc)
+ options = { action_name.to_sym => { format_name.to_sym => { success: format_value } } }
+ end
+
+ spree_responders.deep_merge!(name.to_sym => options)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/controller_helpers/search.rb b/core/lib/spree/core/controller_helpers/search.rb
new file mode 100644
index 00000000000..ee2356e5fdf
--- /dev/null
+++ b/core/lib/spree/core/controller_helpers/search.rb
@@ -0,0 +1,14 @@
+module Spree
+ module Core
+ module ControllerHelpers
+ module Search
+ def build_searcher(params)
+ Spree::Config.searcher_class.new(params).tap do |searcher|
+ searcher.current_user = try_spree_current_user
+ searcher.current_currency = current_currency
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/controller_helpers/store.rb b/core/lib/spree/core/controller_helpers/store.rb
new file mode 100644
index 00000000000..bc5399f9b11
--- /dev/null
+++ b/core/lib/spree/core/controller_helpers/store.rb
@@ -0,0 +1,51 @@
+module Spree
+ module Core
+ module ControllerHelpers
+ module Store
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :current_currency
+ helper_method :current_store
+ helper_method :current_price_options
+ end
+
+ def current_currency
+ Spree::Config[:currency]
+ end
+
+ def current_store
+ @current_store ||= Spree::Store.current(request.env['SERVER_NAME'])
+ end
+
+ # Return a Hash of things that influence the prices displayed in your shop.
+ #
+ # By default, the only thing that influences prices that is the current order's +tax_zone+
+ # (to facilitate differing prices depending on VAT rate for digital products in Europe, see
+ # https://github.com/spree/spree/pull/6295 and https://github.com/spree/spree/pull/6662).
+ #
+ # If your prices depend on something else, overwrite this method and add
+ # more key/value pairs to the Hash it returns.
+ #
+ # Be careful though to also patch the following parts of Spree accordingly:
+ #
+ # * `Spree::VatPriceCalculation#gross_amount`
+ # * `Spree::LineItem#update_price`
+ # * `Spree::Stock::Estimator#taxation_options_for`
+ # * Subclass the `DefaultTax` calculator
+ #
+ def current_price_options
+ {
+ tax_zone: current_tax_zone
+ }
+ end
+
+ private
+
+ def current_tax_zone
+ current_order.try(:tax_zone) || Spree::Zone.default_tax
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/controller_helpers/strong_parameters.rb b/core/lib/spree/core/controller_helpers/strong_parameters.rb
new file mode 100644
index 00000000000..835750752fb
--- /dev/null
+++ b/core/lib/spree/core/controller_helpers/strong_parameters.rb
@@ -0,0 +1,43 @@
+module Spree
+ module Core
+ module ControllerHelpers
+ module StrongParameters
+ def permitted_attributes
+ Spree::PermittedAttributes
+ end
+
+ delegate *Spree::PermittedAttributes::ATTRIBUTES,
+ to: :permitted_attributes,
+ prefix: :permitted
+
+ def permitted_payment_attributes
+ permitted_attributes.payment_attributes + [
+ source_attributes: permitted_source_attributes
+ ]
+ end
+
+ def permitted_checkout_attributes
+ permitted_attributes.checkout_attributes + [
+ bill_address_attributes: permitted_address_attributes,
+ ship_address_attributes: permitted_address_attributes,
+ payments_attributes: permitted_payment_attributes,
+ shipments_attributes: permitted_shipment_attributes
+ ]
+ end
+
+ def permitted_order_attributes
+ permitted_checkout_attributes + [
+ line_items_attributes: permitted_line_item_attributes
+ ]
+ end
+
+ def permitted_product_attributes
+ permitted_attributes.product_attributes + [
+ :store_id,
+ product_properties_attributes: permitted_product_properties_attributes
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/engine.rb b/core/lib/spree/core/engine.rb
new file mode 100644
index 00000000000..cf4f0003e9f
--- /dev/null
+++ b/core/lib/spree/core/engine.rb
@@ -0,0 +1,150 @@
+module Spree
+ module Core
+ class Engine < ::Rails::Engine
+ Environment = Struct.new(:calculators,
+ :preferences,
+ :dependencies,
+ :payment_methods,
+ :adjusters,
+ :stock_splitters,
+ :promotions,
+ :line_item_comparison_hooks)
+ SpreeCalculators = Struct.new(:shipping_methods, :tax_rates, :promotion_actions_create_adjustments, :promotion_actions_create_item_adjustments)
+ PromoEnvironment = Struct.new(:rules, :actions)
+ isolate_namespace Spree
+ engine_name 'spree'
+
+ rake_tasks do
+ load File.join(root, 'lib', 'tasks', 'exchanges.rake')
+ end
+
+ initializer 'spree.environment', before: :load_config_initializers do |app|
+ app.config.spree = Environment.new(SpreeCalculators.new, Spree::AppConfiguration.new, Spree::AppDependencies.new)
+ Spree::Config = app.config.spree.preferences
+ Spree::Dependencies = app.config.spree.dependencies
+ end
+
+ initializer 'spree.register.calculators' do |app|
+ app.config.spree.calculators.shipping_methods = [
+ Spree::Calculator::Shipping::FlatPercentItemTotal,
+ Spree::Calculator::Shipping::FlatRate,
+ Spree::Calculator::Shipping::FlexiRate,
+ Spree::Calculator::Shipping::PerItem,
+ Spree::Calculator::Shipping::PriceSack
+ ]
+
+ app.config.spree.calculators.tax_rates = [
+ Spree::Calculator::DefaultTax
+ ]
+ end
+
+ initializer 'spree.register.stock_splitters', before: :load_config_initializers do |app|
+ app.config.spree.stock_splitters = [
+ Spree::Stock::Splitter::ShippingCategory,
+ Spree::Stock::Splitter::Backordered
+ ]
+ end
+
+ initializer 'spree.register.line_item_comparison_hooks', before: :load_config_initializers do |app|
+ app.config.spree.line_item_comparison_hooks = Set.new
+ end
+
+ initializer 'spree.register.payment_methods', after: 'acts_as_list.insert_into_active_record' do |app|
+ app.config.spree.payment_methods = [
+ Spree::Gateway::Bogus,
+ Spree::Gateway::BogusSimple,
+ Spree::PaymentMethod::Check,
+ Spree::PaymentMethod::StoreCredit
+ ]
+ end
+
+ initializer 'spree.register.adjustable_adjusters' do |app|
+ app.config.spree.adjusters = [
+ Spree::Adjustable::Adjuster::Promotion,
+ Spree::Adjustable::Adjuster::Tax
+ ]
+ end
+
+ # We need to define promotions rules here so extensions and existing apps
+ # can add their custom classes on their initializer files
+ initializer 'spree.promo.environment' do |app|
+ app.config.spree.promotions = PromoEnvironment.new
+ app.config.spree.promotions.rules = []
+ end
+
+ initializer 'spree.promo.register.promotion.calculators' do |app|
+ app.config.spree.calculators.promotion_actions_create_adjustments = [
+ Spree::Calculator::FlatPercentItemTotal,
+ Spree::Calculator::FlatRate,
+ Spree::Calculator::FlexiRate,
+ Spree::Calculator::TieredPercent,
+ Spree::Calculator::TieredFlatRate
+ ]
+
+ app.config.spree.calculators.promotion_actions_create_item_adjustments = [
+ Spree::Calculator::PercentOnLineItem,
+ Spree::Calculator::FlatRate,
+ Spree::Calculator::FlexiRate
+ ]
+ end
+
+ # Promotion rules need to be evaluated on after initialize otherwise
+ # Spree.user_class would be nil and users might experience errors related
+ # to malformed model associations (Spree.user_class is only defined on
+ # the app initializer)
+ config.after_initialize do
+ Rails.application.config.spree.promotions.rules.concat [
+ Spree::Promotion::Rules::ItemTotal,
+ Spree::Promotion::Rules::Product,
+ Spree::Promotion::Rules::User,
+ Spree::Promotion::Rules::FirstOrder,
+ Spree::Promotion::Rules::UserLoggedIn,
+ Spree::Promotion::Rules::OneUsePerUser,
+ Spree::Promotion::Rules::Taxon,
+ Spree::Promotion::Rules::OptionValue,
+ Spree::Promotion::Rules::Country
+ ]
+ end
+
+ initializer 'spree.promo.register.promotions.actions' do |app|
+ app.config.spree.promotions.actions = [
+ Promotion::Actions::CreateAdjustment,
+ Promotion::Actions::CreateItemAdjustments,
+ Promotion::Actions::CreateLineItems,
+ Promotion::Actions::FreeShipping
+ ]
+ end
+
+ # filter sensitive information during logging
+ initializer 'spree.params.filter' do |app|
+ app.config.filter_parameters += [
+ :password,
+ :password_confirmation,
+ :number,
+ :verification_value
+ ]
+ end
+
+ initializer 'spree.core.checking_migrations' do
+ Migrations.new(config, engine_name).check
+ end
+
+ config.to_prepare do
+ # Ensure spree locale paths are present before decorators
+ I18n.load_path.unshift(*(Dir.glob(
+ File.join(
+ File.dirname(__FILE__), '../../../config/locales', '*.{rb,yml}'
+ )
+ ) - I18n.load_path))
+
+ # Load application's model / class decorators
+ Dir.glob(File.join(File.dirname(__FILE__), '../../../app/**/*_decorator*.rb')) do |c|
+ Rails.configuration.cache_classes ? require(c) : load(c)
+ end
+ end
+ end
+ end
+end
+
+require 'spree/core/routes'
+require 'spree/core/components'
diff --git a/core/lib/spree/core/importer.rb b/core/lib/spree/core/importer.rb
new file mode 100644
index 00000000000..e71b9e0d5ba
--- /dev/null
+++ b/core/lib/spree/core/importer.rb
@@ -0,0 +1,9 @@
+module Spree
+ module Core
+ module Importer
+ end
+ end
+end
+
+require 'spree/core/importer/order'
+require 'spree/core/importer/product'
diff --git a/core/lib/spree/core/importer/order.rb b/core/lib/spree/core/importer/order.rb
new file mode 100644
index 00000000000..109528b052a
--- /dev/null
+++ b/core/lib/spree/core/importer/order.rb
@@ -0,0 +1,281 @@
+module Spree
+ module Core
+ module Importer
+ class Order
+ def self.import(user, params)
+ ensure_country_id_from_params params[:ship_address_attributes]
+ ensure_state_id_from_params params[:ship_address_attributes]
+ ensure_country_id_from_params params[:bill_address_attributes]
+ ensure_state_id_from_params params[:bill_address_attributes]
+
+ create_params = params.slice :currency
+ order = Spree::Order.create! create_params
+ order.associate_user!(user)
+
+ shipments_attrs = params.delete(:shipments_attributes)
+
+ create_line_items_from_params(params.delete(:line_items_attributes), order)
+ create_shipments_from_params(shipments_attrs, order)
+ create_adjustments_from_params(params.delete(:adjustments_attributes), order)
+ create_payments_from_params(params.delete(:payments_attributes), order)
+
+ if completed_at = params.delete(:completed_at)
+ order.completed_at = completed_at
+ order.state = 'complete'
+ end
+
+ params.delete(:user_id) unless user.try(:has_spree_role?, 'admin') && params.key?(:user_id)
+
+ order.update_attributes!(params)
+
+ order.create_proposed_shipments unless shipments_attrs.present?
+
+ # Really ensure that the order totals & states are correct
+ order.updater.update
+ if shipments_attrs.present?
+ order.shipments.each_with_index do |shipment, index|
+ shipment.update_columns(cost: shipments_attrs[index][:cost].to_f) if shipments_attrs[index][:cost].present?
+ end
+ end
+ order.reload
+ rescue Exception => e
+ order.destroy if order&.persisted?
+ raise e.message
+ end
+
+ def self.create_shipments_from_params(shipments_hash, order)
+ return [] unless shipments_hash
+
+ shipments_hash.each do |s|
+ begin
+ shipment = order.shipments.build
+ shipment.tracking = s[:tracking]
+ shipment.stock_location = Spree::StockLocation.find_by(admin_name: s[:stock_location]) || Spree::StockLocation.find_by!(name: s[:stock_location])
+ inventory_units = create_inventory_units_from_order_and_params(order, s[:inventory_units])
+
+ inventory_units.each do |inventory_unit|
+ inventory_unit.shipment = shipment
+
+ if s[:shipped_at].present?
+ inventory_unit.pending = false
+ inventory_unit.state = 'shipped'
+ end
+
+ inventory_unit.save!
+ end
+
+ if s[:shipped_at].present?
+ shipment.shipped_at = s[:shipped_at]
+ shipment.state = 'shipped'
+ end
+
+ shipment.save!
+
+ shipping_method = Spree::ShippingMethod.find_by(name: s[:shipping_method]) || Spree::ShippingMethod.find_by!(admin_name: s[:shipping_method])
+ rate = shipment.shipping_rates.create!(shipping_method: shipping_method, cost: s[:cost])
+
+ shipment.selected_shipping_rate_id = rate.id
+ shipment.update_amounts
+
+ adjustments = s.delete(:adjustments_attributes)
+ create_adjustments_from_params(adjustments, order, shipment)
+ rescue Exception => e
+ raise "Order import shipments: #{e.message} #{s}"
+ end
+ end
+ end
+
+ def self.create_inventory_units_from_order_and_params(order, inventory_unit_params)
+ inventory_unit_params.each_with_object([]) do |inventory_unit_param, inventory_units|
+ ensure_variant_id_from_params(inventory_unit_param)
+ existing = inventory_units.detect { |unit| unit.variant_id == inventory_unit_param[:variant_id] }
+ if existing
+ existing.quantity += 1
+ else
+ line_item = order.line_items.detect { |ln| ln.variant_id == inventory_unit_param[:variant_id] }
+ inventory_units << InventoryUnit.new(line_item: line_item, order_id: order.id, variant: line_item.variant, quantity: 1)
+ end
+ end
+ end
+
+ def self.create_line_items_from_params(line_items, order)
+ return {} unless line_items
+
+ iterator = case line_items
+ when Hash
+ ActiveSupport::Deprecation.warn(<<-DEPRECATION, caller)
+ Passing a hash is now deprecated and will be removed in Spree 4.0.
+ It is recommended that you pass it as an array instead.
+
+ New Syntax:
+
+ {
+ "order": {
+ "line_items": [
+ { "variant_id": 123, "quantity": 1 },
+ { "variant_id": 456, "quantity": 1 }
+ ]
+ }
+ }
+
+ Old Syntax:
+
+ {
+ "order": {
+ "line_items": {
+ "1": { "variant_id": 123, "quantity": 1 },
+ "2": { "variant_id": 123, "quantity": 1 }
+ }
+ }
+ }
+ DEPRECATION
+ :each_value
+ when Array
+ :each
+ end
+
+ line_items.send(iterator) do |line_item|
+ begin
+ adjustments = line_item.delete(:adjustments_attributes)
+ extra_params = line_item.except(:variant_id, :quantity, :sku)
+ line_item = ensure_variant_id_from_params(line_item)
+ variant = Spree::Variant.find(line_item[:variant_id])
+ line_item = Cart::AddItem.call(order: order, variant: variant, quantity: line_item[:quantity]).value
+ # Raise any errors with saving to prevent import succeeding with line items
+ # failing silently.
+ if extra_params.present?
+ line_item.update_attributes!(extra_params)
+ else
+ line_item.save!
+ end
+ create_adjustments_from_params(adjustments, order, line_item)
+ rescue Exception => e
+ raise "Order import line items: #{e.message} #{line_item}"
+ end
+ end
+ end
+
+ def self.create_adjustments_from_params(adjustments, order, adjustable = nil)
+ return [] unless adjustments
+
+ adjustments.each do |a|
+ begin
+ adjustment = (adjustable || order).adjustments.build(
+ order: order,
+ amount: a[:amount].to_f,
+ label: a[:label],
+ source_type: source_type_from_adjustment(a)
+ )
+ adjustment.save!
+ adjustment.close!
+ rescue Exception => e
+ raise "Order import adjustments: #{e.message} #{a}"
+ end
+ end
+ end
+
+ def self.create_payments_from_params(payments_hash, order)
+ return [] unless payments_hash
+
+ payments_hash.each do |p|
+ begin
+ payment = order.payments.build order: order
+ payment.amount = p[:amount].to_f
+ # Order API should be using state as that's the normal payment field.
+ # spree_wombat serializes payment state as status so imported orders should fall back to status field.
+ payment.state = p[:state] || p[:status] || 'completed'
+ payment.created_at = p[:created_at] if p[:created_at]
+ payment.payment_method = Spree::PaymentMethod.find_by!(name: p[:payment_method])
+ payment.source = create_source_payment_from_params(p[:source], payment) if p[:source]
+ payment.save!
+ rescue Exception => e
+ raise "Order import payments: #{e.message} #{p}"
+ end
+ end
+ end
+
+ def self.create_source_payment_from_params(source_hash, payment)
+ Spree::CreditCard.create(
+ month: source_hash[:month],
+ year: source_hash[:year],
+ cc_type: source_hash[:cc_type],
+ last_digits: source_hash[:last_digits],
+ name: source_hash[:name],
+ payment_method: payment.payment_method,
+ gateway_customer_profile_id: source_hash[:gateway_customer_profile_id],
+ gateway_payment_profile_id: source_hash[:gateway_payment_profile_id],
+ imported: true
+ )
+ rescue Exception => e
+ raise "Order import source payments: #{e.message} #{source_hash}"
+ end
+
+ def self.ensure_variant_id_from_params(hash)
+ sku = hash.delete(:sku)
+ unless hash[:variant_id].present?
+ hash[:variant_id] = Spree::Variant.active.find_by!(sku: sku).id
+ end
+ hash
+ rescue ActiveRecord::RecordNotFound => e
+ raise "Ensure order import variant: Variant w/SKU #{sku} not found."
+ rescue Exception => e
+ raise "Ensure order import variant: #{e.message} #{hash}"
+ end
+
+ def self.ensure_country_id_from_params(address)
+ return if address.nil? || address[:country_id].present? || address[:country].nil?
+
+ begin
+ search = {}
+ if name = address[:country]['name']
+ search[:name] = name
+ elsif iso_name = address[:country]['iso_name']
+ search[:iso_name] = iso_name.upcase
+ elsif iso = address[:country]['iso']
+ search[:iso] = iso.upcase
+ elsif iso3 = address[:country]['iso3']
+ search[:iso3] = iso3.upcase
+ end
+
+ address.delete(:country)
+ address[:country_id] = Spree::Country.where(search).first!.id
+ rescue Exception => e
+ raise "Ensure order import address country: #{e.message} #{search}"
+ end
+ end
+
+ def self.ensure_state_id_from_params(address)
+ return if address.nil? || address[:state_id].present? || address[:state].nil?
+
+ begin
+ search = {}
+ if name = address[:state]['name']
+ search[:name] = name
+ elsif abbr = address[:state]['abbr']
+ search[:abbr] = abbr.upcase
+ end
+
+ address.delete(:state)
+ search[:country_id] = address[:country_id]
+
+ if state = Spree::State.where(search).first
+ address[:state_id] = state.id
+ else
+ address[:state_name] = search[:name] || search[:abbr]
+ end
+ rescue Exception => e
+ raise "Ensure order import address state: #{e.message} #{search}"
+ end
+ end
+
+ def self.source_type_from_adjustment(adjustment)
+ if adjustment[:tax]
+ 'Spree::TaxRate'
+ elsif adjustment[:promotion]
+ 'Spree::PromotionAction'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/importer/product.rb b/core/lib/spree/core/importer/product.rb
new file mode 100644
index 00000000000..7f63f0c8a82
--- /dev/null
+++ b/core/lib/spree/core/importer/product.rb
@@ -0,0 +1,63 @@
+module Spree
+ module Core
+ module Importer
+ class Product
+ attr_reader :product, :product_attrs, :variants_attrs, :options_attrs
+
+ def initialize(product, product_params, options = {})
+ @product = product || Spree::Product.new(product_params)
+
+ @product_attrs = product_params.to_h
+ @variants_attrs = (options[:variants_attrs] || []).map(&:to_h)
+ @options_attrs = options[:options_attrs] || []
+ end
+
+ def create
+ if product.save
+ variants_attrs.each do |variant_attribute|
+ # make sure the product is assigned before the options=
+ product.variants.create({ product: product }.merge(variant_attribute))
+ end
+
+ set_up_options
+ end
+
+ product
+ end
+
+ def update
+ if product.update_attributes(product_attrs)
+ variants_attrs.each do |variant_attribute|
+ # update the variant if the id is present in the payload
+ if variant_attribute['id'].present?
+ product.variants.find(variant_attribute['id'].to_i).update_attributes(variant_attribute)
+ else
+ # make sure the product is assigned before the options=
+ product.variants.create({ product: product }.merge(variant_attribute))
+ end
+ end
+
+ set_up_options
+ end
+
+ product
+ end
+
+ private
+
+ def set_up_options
+ options_attrs.each do |name|
+ option_type = Spree::OptionType.where(name: name).first_or_initialize do |option_type|
+ option_type.presentation = name
+ option_type.save!
+ end
+
+ unless product.option_types.include?(option_type)
+ product.option_types << option_type
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/number_generator.rb b/core/lib/spree/core/number_generator.rb
new file mode 100644
index 00000000000..80f214df8f7
--- /dev/null
+++ b/core/lib/spree/core/number_generator.rb
@@ -0,0 +1,50 @@
+module Spree
+ module Core
+ class NumberGenerator < Module
+ BASE = 10
+ DEFAULT_LENGTH = 9
+
+ attr_accessor :prefix, :length
+
+ def initialize(options)
+ @prefix = options.fetch(:prefix)
+ @length = options.fetch(:length, DEFAULT_LENGTH)
+ @letters = options[:letters]
+ end
+
+ def included(host)
+ generator_method = method(:generate_permalink)
+ generator_instance = self
+
+ host.class_eval do
+ validates(:number, presence: true, uniqueness: { allow_blank: true })
+
+ before_validation do |instance|
+ instance.number ||= generator_method.call(host)
+ end
+
+ define_singleton_method(:number_generator) { generator_instance }
+ end
+ end
+
+ private
+
+ def generate_permalink(host)
+ length = @length
+
+ loop do
+ candidate = new_candidate(length)
+ return candidate unless host.exists?(number: candidate)
+
+ # If over half of all possible options are taken add another digit.
+ length += 1 if host.count > Rational(BASE**length, 2)
+ end
+ end
+
+ def new_candidate(length)
+ characters = @letters ? 36 : 10
+ @prefix + SecureRandom.random_number(characters**length).to_s(characters).rjust(length, '0').upcase
+ end
+ end # Permalink
+ end # Core
+end # Spree
diff --git a/core/lib/spree/core/product_duplicator.rb b/core/lib/spree/core/product_duplicator.rb
new file mode 100644
index 00000000000..19ffe2adf16
--- /dev/null
+++ b/core/lib/spree/core/product_duplicator.rb
@@ -0,0 +1,83 @@
+module Spree
+ class ProductDuplicator
+ attr_accessor :product
+
+ @@clone_images_default = true
+ mattr_accessor :clone_images_default
+
+ def initialize(product, include_images = @@clone_images_default)
+ @product = product
+ @include_images = include_images
+ end
+
+ def duplicate
+ new_product = duplicate_product
+
+ # don't dup the actual variants, just the characterising types
+ new_product.option_types = product.option_types if product.has_variants?
+
+ # allow site to do some customization
+ new_product.send(:duplicate_extra, product) if new_product.respond_to?(:duplicate_extra)
+ new_product.save!
+ new_product
+ end
+
+ protected
+
+ def duplicate_product
+ product.dup.tap do |new_product|
+ new_product.name = "COPY OF #{product.name}"
+ new_product.taxons = product.taxons
+ new_product.created_at = nil
+ new_product.deleted_at = nil
+ new_product.updated_at = nil
+ new_product.product_properties = reset_properties
+ new_product.master = duplicate_master
+ new_product.variants = product.variants.map { |variant| duplicate_variant variant }
+ end
+ end
+
+ def duplicate_master
+ master = product.master
+ master.dup.tap do |new_master|
+ new_master.sku = sku_generator(master.sku)
+ new_master.deleted_at = nil
+ new_master.images = master.images.map { |image| duplicate_image image } if @include_images
+ new_master.price = master.price
+ new_master.currency = master.currency
+ end
+ end
+
+ def duplicate_variant(variant)
+ new_variant = variant.dup
+ new_variant.sku = sku_generator(new_variant.sku)
+ new_variant.deleted_at = nil
+ new_variant.option_values = variant.option_values.map { |option_value| option_value }
+ new_variant
+ end
+
+ def duplicate_image(image)
+ new_image = image.dup
+ if Rails.application.config.use_paperclip
+ new_image.assign_attributes(attachment: image.attachment.clone)
+ else
+ new_image.attachment.attach(image.attachment.blob)
+ end
+ new_image.save!
+ new_image
+ end
+
+ def reset_properties
+ product.product_properties.map do |prop|
+ prop.dup.tap do |new_prop|
+ new_prop.created_at = nil
+ new_prop.updated_at = nil
+ end
+ end
+ end
+
+ def sku_generator(sku)
+ "COPY OF #{Variant.unscoped.where('sku like ?', "%#{sku}").order(:created_at).last.sku}"
+ end
+ end
+end
diff --git a/core/lib/spree/core/product_filters.rb b/core/lib/spree/core/product_filters.rb
new file mode 100644
index 00000000000..8f534a29a88
--- /dev/null
+++ b/core/lib/spree/core/product_filters.rb
@@ -0,0 +1,194 @@
+module Spree
+ module Core
+ # THIS FILE SHOULD BE OVER-RIDDEN IN YOUR SITE EXTENSION!
+ # the exact code probably won't be useful, though you're welcome to modify and reuse
+ # the current contents are mainly for testing and documentation
+
+ # To override this file...
+ # 1) Make a copy of it in your sites local /lib/spree folder
+ # 2) Add it to the config load path, or require it in an initializer, e.g...
+ #
+ # # config/initializers/spree.rb
+ # require 'spree/core/product_filters'
+ #
+
+ # set up some basic filters for use with products
+ #
+ # Each filter has two parts
+ # * a parametrized named scope which expects a list of labels
+ # * an object which describes/defines the filter
+ #
+ # The filter description has three components
+ # * a name, for displaying on pages
+ # * a named scope which will 'execute' the filter
+ # * a mapping of presentation labels to the relevant condition (in the context of the named scope)
+ # * an optional list of labels and values (for use with object selection - see taxons examples below)
+ #
+ # The named scopes here have a suffix '_any', following Ransack's convention for a
+ # scope which returns results which match any of the inputs. This is purely a convention,
+ # but might be a useful reminder.
+ #
+ # When creating a form, the name of the checkbox group for a filter F should be
+ # the name of F's scope with [] appended, eg "price_range_any[]", and for
+ # each label you should have a checkbox with the label as its value. On submission,
+ # Rails will send the action a hash containing (among other things) an array named
+ # after the scope whose values are the active labels.
+ #
+ # Ransack will then convert this array to a call to the named scope with the array
+ # contents, and the named scope will build a query with the disjunction of the conditions
+ # relating to the labels, all relative to the scope's context.
+ #
+ # The details of how/when filters are used is a detail for specific models (eg products
+ # or taxons), eg see the taxon model/controller.
+
+ # See specific filters below for concrete examples.
+ module ProductFilters
+ # Example: filtering by price
+ # The named scope just maps incoming labels onto their conditions, and builds the conjunction
+ # 'price' is in the base scope's context (ie, "select foo from products where ...") so
+ # we can access the field right away
+ # The filter identifies which scope to use, then sets the conditions for each price range
+ #
+ # If user checks off three different price ranges then the argument passed to
+ # below scope would be something like ["$10 - $15", "$15 - $18", "$18 - $20"]
+ #
+ Spree::Product.add_search_scope :price_range_any do |*opts|
+ conds = opts.map { |o| Spree::Core::ProductFilters.price_filter[:conds][o] }.reject(&:nil?)
+ scope = conds.shift
+ conds.each do |new_scope|
+ scope = scope.or(new_scope)
+ end
+ Spree::Product.joins(master: :default_price).where(scope)
+ end
+
+ def self.format_price(amount)
+ Spree::Money.new(amount)
+ end
+
+ def self.price_filter
+ v = Spree::Price.arel_table
+ conds = [[Spree.t(:under_price, price: format_price(10)), v[:amount].lteq(10)],
+ ["#{format_price(10)} - #{format_price(15)}", v[:amount].in(10..15)],
+ ["#{format_price(15)} - #{format_price(18)}", v[:amount].in(15..18)],
+ ["#{format_price(18)} - #{format_price(20)}", v[:amount].in(18..20)],
+ [Spree.t(:or_over_price, price: format_price(20)), v[:amount].gteq(20)]]
+ {
+ name: Spree.t(:price_range),
+ scope: :price_range_any,
+ conds: Hash[*conds.flatten],
+ labels: conds.map { |k, _v| [k, k] }
+ }
+ end
+
+ # Example: filtering by possible brands
+ #
+ # First, we define the scope. Two interesting points here: (a) we run our conditions
+ # in the scope where the info for the 'brand' property has been loaded; and (b)
+ # because we may want to filter by other properties too, we give this part of the
+ # query a unique name (which must be used in the associated conditions too).
+ #
+ # Secondly, the filter. Instead of a static list of values, we pull out all existing
+ # brands from the db, and then build conditions which test for string equality on
+ # the (uniquely named) field "p_brand.value". There's also a test for brand info
+ # being blank: note that this relies on with_property doing a left outer join
+ # rather than an inner join.
+ Spree::Product.add_search_scope :brand_any do |*opts|
+ conds = opts.map { |o| ProductFilters.brand_filter[:conds][o] }.reject(&:nil?)
+ scope = conds.shift
+ conds.each do |new_scope|
+ scope = scope.or(new_scope)
+ end
+ Spree::Product.with_property('brand').where(scope)
+ end
+
+ def self.brand_filter
+ brand_property = Spree::Property.find_by(name: 'brand')
+ brands = brand_property ? Spree::ProductProperty.where(property_id: brand_property.id).pluck(:value).uniq.map(&:to_s) : []
+ pp = Spree::ProductProperty.arel_table
+ conds = Hash[*brands.map { |b| [b, pp[:value].eq(b)] }.flatten]
+ {
+ name: I18n.t('spree.taxonomy_brands_name'),
+ scope: :brand_any,
+ conds: conds,
+ labels: brands.sort.map { |k| [k, k] }
+ }
+ end
+
+ # Example: a parameterized filter
+ # The filter above may show brands which aren't applicable to the current taxon,
+ # so this one only shows the brands that are relevant to a particular taxon and
+ # its descendants.
+ #
+ # We don't have to give a new scope since the conditions here are a subset of the
+ # more general filter, so decoding will still work - as long as the filters on a
+ # page all have unique names (ie, you can't use the two brand filters together
+ # if they use the same scope). To be safe, the code uses a copy of the scope.
+ #
+ # HOWEVER: what happens if we want a more precise scope? we can't pass
+ # parametrized scope names to Ransack, only atomic names, so couldn't ask
+ # for taxon T's customized filter to be used. BUT: we can arrange for the form
+ # to pass back a hash instead of an array, where the key acts as the (taxon)
+ # parameter and value is its label array, and then get a modified named scope
+ # to get its conditions from a particular filter.
+ #
+ # The brand-finding code can be simplified if a few more named scopes were added to
+ # the product properties model.
+ Spree::Product.add_search_scope :selective_brand_any do |*opts|
+ Spree::Product.brand_any(*opts)
+ end
+
+ def self.selective_brand_filter(taxon = nil)
+ taxon ||= Spree::Taxonomy.first.root
+ brand_property = Spree::Property.find_by(name: 'brand')
+ scope = Spree::ProductProperty.where(property: brand_property).
+ joins(product: :taxons).
+ where("#{Spree::Taxon.table_name}.id" => [taxon] + taxon.descendants)
+ brands = scope.pluck(:value).uniq
+ {
+ name: 'Applicable Brands',
+ scope: :selective_brand_any,
+ labels: brands.sort.map { |k| [k, k] }
+ }
+ end
+
+ # Provide filtering on the immediate children of a taxon
+ #
+ # This doesn't fit the pattern of the examples above, so there's a few changes.
+ # Firstly, it uses an existing scope which was not built for filtering - and so
+ # has no need of a conditions mapping, and secondly, it has a mapping of name
+ # to the argument type expected by the other scope.
+ #
+ # This technique is useful for filtering on objects (by passing ids) or with a
+ # scope that can be used directly (eg. testing only ever on a single property).
+ #
+ # This scope selects products in any of the active taxons or their children.
+ #
+ def self.taxons_below(taxon)
+ return Spree::Core::ProductFilters.all_taxons if taxon.nil?
+
+ {
+ name: 'Taxons under ' + taxon.name,
+ scope: :taxons_id_in_tree_any,
+ labels: taxon.children.sort_by(&:position).map { |t| [t.name, t.id] },
+ conds: nil
+ }
+ end
+
+ # Filtering by the list of all taxons
+ #
+ # Similar idea as above, but we don't want the descendants' products, hence
+ # it uses one of the auto-generated scopes from Ransack.
+ #
+ # idea: expand the format to allow nesting of labels?
+ def self.all_taxons
+ taxons = Spree::Taxonomy.all.map { |t| [t.root] + t.root.descendants }.flatten
+ {
+ name: 'All taxons',
+ scope: :taxons_id_equals_any,
+ labels: taxons.sort_by(&:name).map { |t| [t.name, t.id] },
+ conds: nil # not needed
+ }
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/query_filters.rb b/core/lib/spree/core/query_filters.rb
new file mode 100644
index 00000000000..cce65fc4554
--- /dev/null
+++ b/core/lib/spree/core/query_filters.rb
@@ -0,0 +1,11 @@
+module Spree
+ module Core
+ module QueryFilters
+ end
+ end
+end
+
+require 'spree/core/query_filters/comparable'
+require 'spree/core/query_filters/number'
+require 'spree/core/query_filters/date'
+require 'spree/core/query_filters/text'
diff --git a/core/lib/spree/core/query_filters/comparable.rb b/core/lib/spree/core/query_filters/comparable.rb
new file mode 100644
index 00000000000..887ceb28ac8
--- /dev/null
+++ b/core/lib/spree/core/query_filters/comparable.rb
@@ -0,0 +1,46 @@
+module Spree
+ module Core
+ module QueryFilters
+ class Comparable
+ def initialize(attribute:)
+ @attribute = attribute
+ end
+
+ def call(scope:, filter:)
+ scope = gt(scope, filter[:gt])
+ scope = gteq(scope, filter[:gteq])
+ scope = lt(scope, filter[:lt])
+ lteq(scope, filter[:lteq])
+ end
+
+ private
+
+ attr_reader :attribute
+
+ def gt(scope, value)
+ return scope unless value
+
+ scope.where(attribute.gt(value))
+ end
+
+ def gteq(scope, value)
+ return scope unless value
+
+ scope.where(attribute.gteq(value))
+ end
+
+ def lt(scope, value)
+ return scope unless value
+
+ scope.where(attribute.lt(value))
+ end
+
+ def lteq(scope, value)
+ return scope unless value
+
+ scope.where(attribute.lteq(value))
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/query_filters/date.rb b/core/lib/spree/core/query_filters/date.rb
new file mode 100644
index 00000000000..88a15ec76ae
--- /dev/null
+++ b/core/lib/spree/core/query_filters/date.rb
@@ -0,0 +1,8 @@
+module Spree
+ module Core
+ module QueryFilters
+ class Date < Comparable
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/query_filters/number.rb b/core/lib/spree/core/query_filters/number.rb
new file mode 100644
index 00000000000..b1c6e5a141f
--- /dev/null
+++ b/core/lib/spree/core/query_filters/number.rb
@@ -0,0 +1,8 @@
+module Spree
+ module Core
+ module QueryFilters
+ class Number < Comparable
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/query_filters/text.rb b/core/lib/spree/core/query_filters/text.rb
new file mode 100644
index 00000000000..e55727a91a4
--- /dev/null
+++ b/core/lib/spree/core/query_filters/text.rb
@@ -0,0 +1,32 @@
+module Spree
+ module Core
+ module QueryFilters
+ class Text
+ def initialize(attribute:)
+ @attribute = attribute
+ end
+
+ def call(scope:, filter:)
+ scope = eq(scope, filter[:eq])
+ contains(scope, filter[:contains])
+ end
+
+ private
+
+ attr_reader :attribute
+
+ def eq(scope, value)
+ return scope unless value
+
+ scope.where(attribute.eq(value))
+ end
+
+ def contains(scope, value)
+ return scope unless value
+
+ scope.where(attribute.matches("%#{value}%"))
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/routes.rb b/core/lib/spree/core/routes.rb
new file mode 100644
index 00000000000..bffae42d668
--- /dev/null
+++ b/core/lib/spree/core/routes.rb
@@ -0,0 +1,42 @@
+module Spree
+ module Core
+ class Engine < ::Rails::Engine
+ def self.add_routes(&block)
+ @spree_routes ||= []
+
+ # Anything that causes the application's routes to be reloaded,
+ # will cause this method to be called more than once
+ # i.e. https://github.com/plataformatec/devise/blob/31971e69e6a1bcf6c7f01eaaa44f227c4af5d4d2/lib/devise/rails.rb#L14
+ # In the case of Devise, this *only* happens in the production env
+ # This coupled with Rails 4's insistence that routes are not drawn twice,
+ # poses quite a serious problem.
+ #
+ # This is mainly why this whole file exists in the first place.
+ #
+ # Thus we need to make sure that the routes aren't drawn twice.
+ @spree_routes << block unless @spree_routes.include?(block)
+ end
+
+ def self.append_routes(&block)
+ @append_routes ||= []
+ # See comment in add_routes.
+ @append_routes << block unless @append_routes.include?(block)
+ end
+
+ def self.draw_routes(&block)
+ @spree_routes ||= []
+ @append_routes ||= []
+ eval_block(block) if block_given?
+ @spree_routes.each { |r| eval_block(&r) }
+ @append_routes.each { |r| eval_block(&r) }
+ # # Clear out routes so that they aren't drawn twice.
+ @spree_routes = []
+ @append_routes = []
+ end
+
+ def eval_block(&block)
+ Spree::Core::Engine.routes.send :eval_block, block
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/search/base.rb b/core/lib/spree/core/search/base.rb
new file mode 100644
index 00000000000..f03849dca66
--- /dev/null
+++ b/core/lib/spree/core/search/base.rb
@@ -0,0 +1,106 @@
+module Spree
+ module Core
+ module Search
+ class Base
+ attr_accessor :properties
+ attr_accessor :current_user
+ attr_accessor :current_currency
+
+ def initialize(params)
+ self.current_currency = Spree::Config[:currency]
+ @properties = {}
+ prepare(params)
+ end
+
+ def retrieve_products
+ @products = get_base_scope
+ curr_page = page || 1
+
+ unless Spree::Config.show_products_without_price
+ @products = @products.where('spree_prices.amount IS NOT NULL').
+ where('spree_prices.currency' => current_currency)
+ end
+ @products = @products.page(curr_page).per(per_page)
+ end
+
+ def method_missing(name)
+ if @properties.key? name
+ @properties[name]
+ else
+ super
+ end
+ end
+
+ protected
+
+ def get_base_scope
+ base_scope = Spree::Product.spree_base_scopes.active
+ base_scope = base_scope.in_taxon(taxon) unless taxon.blank?
+ base_scope = get_products_conditions_for(base_scope, keywords)
+ base_scope = add_search_scopes(base_scope)
+ base_scope = add_eagerload_scopes(base_scope)
+ base_scope
+ end
+
+ def add_eagerload_scopes(scope)
+ # TL;DR Switch from `preload` to `includes` as soon as Rails starts honoring
+ # `order` clauses on `has_many` associations when a `where` constraint
+ # affecting a joined table is present (see
+ # https://github.com/rails/rails/issues/6769).
+ #
+ # Ideally this would use `includes` instead of `preload` calls, leaving it
+ # up to Rails whether associated objects should be fetched in one big join
+ # or multiple independent queries. However as of Rails 4.1.8 any `order`
+ # defined on `has_many` associations are ignored when Rails builds a join
+ # query.
+ #
+ # Would we use `includes` in this particular case, Rails would do
+ # separate queries most of the time but opt for a join as soon as any
+ # `where` constraints affecting joined tables are added to the search;
+ # which is the case as soon as a taxon is added to the base scope.
+ scope = scope.preload(:tax_category)
+ scope = scope.preload(master: :prices)
+ scope = scope.preload(master: :images) if include_images
+ scope
+ end
+
+ def add_search_scopes(base_scope)
+ if search.is_a?(ActionController::Parameters)
+ search.each do |name, scope_attribute|
+ scope_name = name.to_sym
+ base_scope = if base_scope.respond_to?(:search_scopes) && base_scope.search_scopes.include?(scope_name.to_sym)
+ base_scope.send(scope_name, *scope_attribute)
+ else
+ base_scope.merge(Spree::Product.ransack(scope_name => scope_attribute).result)
+ end
+ end
+ end
+ base_scope
+ end
+
+ # method should return new scope based on base_scope
+ def get_products_conditions_for(base_scope, query)
+ unless query.blank?
+ base_scope = base_scope.like_any([:name, :description], query.split)
+ end
+ base_scope
+ end
+
+ def prepare(params)
+ @properties[:taxon] = params[:taxon].blank? ? nil : Spree::Taxon.find(params[:taxon])
+ @properties[:keywords] = params[:keywords]
+ @properties[:search] = params[:search]
+ @properties[:include_images] = params[:include_images]
+
+ per_page = params[:per_page].to_i
+ @properties[:per_page] = per_page > 0 ? per_page : Spree::Config[:products_per_page]
+ @properties[:page] = if params[:page].respond_to?(:to_i)
+ params[:page].to_i <= 0 ? 1 : params[:page].to_i
+ else
+ 1
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/token_generator.rb b/core/lib/spree/core/token_generator.rb
new file mode 100644
index 00000000000..0a393a9d0c9
--- /dev/null
+++ b/core/lib/spree/core/token_generator.rb
@@ -0,0 +1,22 @@
+module Spree
+ module Core
+ module TokenGenerator
+ def generate_token(model_class = Spree::Order)
+ loop do
+ token = "#{random_token}#{unique_ending}"
+ break token unless model_class.exists?(token: token)
+ end
+ end
+
+ private
+
+ def random_token
+ SecureRandom.urlsafe_base64(nil, false)
+ end
+
+ def unique_ending
+ (Time.now.to_f * 1000).to_i
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/core/version.rb b/core/lib/spree/core/version.rb
new file mode 100644
index 00000000000..9a8cf4303a1
--- /dev/null
+++ b/core/lib/spree/core/version.rb
@@ -0,0 +1,5 @@
+module Spree
+ def self.version
+ '4.0.0.alpha'
+ end
+end
diff --git a/core/lib/spree/dependencies_helper.rb b/core/lib/spree/dependencies_helper.rb
new file mode 100644
index 00000000000..de4ff6c1c7d
--- /dev/null
+++ b/core/lib/spree/dependencies_helper.rb
@@ -0,0 +1,11 @@
+module Spree
+ module DependenciesHelper
+ def current_values
+ values = []
+ self.class::INJECTION_POINTS.each do |injection_point|
+ values << { injection_point.to_s => instance_variable_get("@#{injection_point}") }
+ end
+ values
+ end
+ end
+end
diff --git a/core/lib/spree/i18n.rb b/core/lib/spree/i18n.rb
new file mode 100644
index 00000000000..55d9dcc620f
--- /dev/null
+++ b/core/lib/spree/i18n.rb
@@ -0,0 +1,35 @@
+require 'i18n'
+require 'active_support/core_ext/array/extract_options'
+require 'spree/i18n/base'
+
+module Spree
+ extend ActionView::Helpers::TranslationHelper
+ extend ActionView::Helpers::TagHelper
+
+ class << self
+ # Add spree namespace and delegate to Rails TranslationHelper for some nice
+ # extra functionality. e.g return reasonable strings for missing translations
+ def translate(*args)
+ @virtual_path = virtual_path
+
+ options = args.extract_options!
+ options[:scope] = [*options[:scope]].unshift(:spree)
+ args << options
+ super(*args)
+ end
+
+ alias t translate
+
+ def context
+ Spree::ViewContext.context
+ end
+
+ def virtual_path
+ if context
+ path = context.instance_variable_get('@virtual_path')
+
+ path&.gsub(/spree/, '')
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/i18n/base.rb b/core/lib/spree/i18n/base.rb
new file mode 100644
index 00000000000..765c8ad169b
--- /dev/null
+++ b/core/lib/spree/i18n/base.rb
@@ -0,0 +1,17 @@
+module Spree
+ module ViewContext
+ def self.context=(context)
+ @context = context
+ end
+
+ def self.context
+ @context
+ end
+
+ def view_context
+ super.tap do |context|
+ Spree::ViewContext.context = context
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/i18n/initializer.rb b/core/lib/spree/i18n/initializer.rb
new file mode 100644
index 00000000000..5019d567c82
--- /dev/null
+++ b/core/lib/spree/i18n/initializer.rb
@@ -0,0 +1 @@
+Spree::BaseController.include Spree::ViewContext
diff --git a/core/lib/spree/localized_number.rb b/core/lib/spree/localized_number.rb
new file mode 100644
index 00000000000..73348703b91
--- /dev/null
+++ b/core/lib/spree/localized_number.rb
@@ -0,0 +1,23 @@
+module Spree
+ class LocalizedNumber
+ # Strips all non-price-like characters from the number, taking into account locale settings.
+ def self.parse(number)
+ return number unless number.is_a?(String)
+
+ separator, delimiter = I18n.t([:'number.currency.format.separator', :'number.currency.format.delimiter'])
+ non_number_characters = /[^0-9\-#{separator}]/
+
+ # work on a copy, prevent original argument modification
+ number = number.dup
+ # strip everything else first, including thousands delimiter
+ number.gsub!(non_number_characters, '')
+ # then replace the locale-specific decimal separator with the standard separator if necessary
+ number.gsub!(separator, '.') unless separator == '.'
+
+ # Returns 0 to avoid ArgumentError: invalid value for BigDecimal(): "" for empty string
+ return 0 unless number.present?
+
+ number.to_d
+ end
+ end
+end
diff --git a/core/lib/spree/migrations.rb b/core/lib/spree/migrations.rb
new file mode 100644
index 00000000000..ed550cf7d7b
--- /dev/null
+++ b/core/lib/spree/migrations.rb
@@ -0,0 +1,80 @@
+module Spree
+ class Migrations
+ attr_reader :config, :engine_name
+
+ # Takes the engine config block and engine name
+ def initialize(config, engine_name)
+ @config = config
+ @engine_name = engine_name
+ end
+
+ # Puts warning when any engine migration is not present on the Rails app
+ # db/migrate dir
+ #
+ # First split:
+ #
+ # ["20131128203548", "update_name_fields_on_spree_credit_cards.spree.rb"]
+ #
+ # Second split should give the engine_name of the migration
+ #
+ # ["update_name_fields_on_spree_credit_cards", "spree.rb"]
+ #
+ # Shouldn't run on test mode because migrations inside engine don't have
+ # engine name on the file name
+ def check
+ if File.directory?(app_dir)
+ engine_in_app = app_migrations.map do |file_name|
+ name, engine = file_name.split('.', 2)
+ next unless match_engine?(engine)
+
+ name
+ end.compact
+
+ missing_migrations = engine_migrations.sort - engine_in_app.sort
+ unless missing_migrations.empty?
+ puts "[#{engine_name.capitalize} WARNING] Missing migrations."
+ missing_migrations.each do |migration|
+ puts "[#{engine_name.capitalize} WARNING] #{migration} from #{engine_name} is missing."
+ end
+ puts "[#{engine_name.capitalize} WARNING] Run `bundle exec rake railties:install:migrations` to get them.\n\n"
+ true
+ end
+ end
+ end
+
+ private
+
+ def engine_migrations
+ Dir.entries(engine_dir).map do |file_name|
+ name = file_name.split('_', 2).last.split('.', 2).first
+ name.empty? ? next : name
+ end.compact! || []
+ end
+
+ def app_migrations
+ Dir.entries(app_dir).map do |file_name|
+ next if ['.', '..'].include? file_name
+
+ name = file_name.split('_', 2).last
+ name.empty? ? next : name
+ end.compact! || []
+ end
+
+ def app_dir
+ "#{Rails.root}/db/migrate"
+ end
+
+ def engine_dir
+ "#{config.root}/db/migrate"
+ end
+
+ def match_engine?(engine)
+ if engine_name == 'spree'
+ # Avoid stores upgrading from 1.3 getting wrong warnings
+ ['spree.rb', 'spree_promo.rb'].include? engine
+ else
+ engine == "#{engine_name}.rb"
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/money.rb b/core/lib/spree/money.rb
new file mode 100644
index 00000000000..519d9f1c913
--- /dev/null
+++ b/core/lib/spree/money.rb
@@ -0,0 +1,69 @@
+require 'money'
+
+Money.locale_backend = :i18n
+
+module Spree
+ class Money
+ class <]*>/, '') # we don't want wrap every element in span
+ output = output.sub(' ', ' ').html_safe
+ end
+
+ output
+ end
+
+ def as_json(*)
+ to_s
+ end
+
+ def decimal_mark
+ options[:decimal_mark] || money.decimal_mark
+ end
+
+ def thousands_separator
+ options[:thousands_separator] || money.thousands_separator
+ end
+
+ def ==(obj)
+ money == obj.money
+ end
+
+ private
+
+ attr_reader :options
+ end
+end
diff --git a/core/lib/spree/permitted_attributes.rb b/core/lib/spree/permitted_attributes.rb
new file mode 100644
index 00000000000..b4745f41084
--- /dev/null
+++ b/core/lib/spree/permitted_attributes.rb
@@ -0,0 +1,120 @@
+module Spree
+ module PermittedAttributes
+ ATTRIBUTES = [
+ :address_attributes,
+ :checkout_attributes,
+ :customer_return_attributes,
+ :image_attributes,
+ :inventory_unit_attributes,
+ :line_item_attributes,
+ :option_type_attributes,
+ :option_value_attributes,
+ :payment_attributes,
+ :product_attributes,
+ :product_properties_attributes,
+ :property_attributes,
+ :return_authorization_attributes,
+ :shipment_attributes,
+ :source_attributes,
+ :stock_item_attributes,
+ :stock_location_attributes,
+ :stock_movement_attributes,
+ :store_attributes,
+ :store_credit_attributes,
+ :taxon_attributes,
+ :taxonomy_attributes,
+ :user_attributes,
+ :variant_attributes
+ ]
+
+ mattr_reader *ATTRIBUTES
+
+ @@address_attributes = [
+ :id, :firstname, :lastname, :first_name, :last_name,
+ :address1, :address2, :city, :country_iso, :country_id, :state_id,
+ :zipcode, :phone, :state_name, :alternative_phone, :company,
+ country: [:iso, :name, :iso3, :iso_name],
+ state: [:name, :abbr]
+ ]
+
+ @@checkout_attributes = [
+ :coupon_code, :email, :shipping_method_id, :special_instructions, :use_billing, :user_id
+ ]
+
+ @@customer_return_attributes = [:stock_location_id, return_items_attributes: [:id, :inventory_unit_id, :return_authorization_id, :returned, :pre_tax_amount, :acceptance_status, :exchange_variant_id, :resellable]]
+
+ @@image_attributes = [:alt, :attachment, :position, :viewable_type, :viewable_id]
+
+ @@inventory_unit_attributes = [:shipment, :shipment_id, :variant_id]
+
+ @@line_item_attributes = [:id, :variant_id, :quantity]
+
+ @@option_type_attributes = [:name, :presentation, :option_values_attributes]
+
+ @@option_value_attributes = [:name, :presentation]
+
+ @@payment_attributes = [:amount, :payment_method_id, :payment_method]
+
+ @@product_properties_attributes = [:property_name, :value, :position]
+
+ @@product_attributes = [
+ :name, :description, :available_on, :discontinue_on, :permalink, :meta_description,
+ :meta_keywords, :price, :sku, :deleted_at, :prototype_id,
+ :option_values_hash, :weight, :height, :width, :depth,
+ :shipping_category_id, :tax_category_id,
+ :cost_currency, :cost_price,
+ option_type_ids: [], taxon_ids: []
+ ]
+
+ @@property_attributes = [:name, :presentation]
+
+ @@return_authorization_attributes = [:amount, :memo, :stock_location_id, :inventory_units_attributes, :return_authorization_reason_id]
+
+ @@shipment_attributes = [
+ :order, :special_instructions, :stock_location_id, :id,
+ :tracking, :address, :inventory_units, :selected_shipping_rate_id
+ ]
+
+ # month / year may be provided by some sources, or others may elect to use one field
+ @@source_attributes = [
+ :number, :month, :year, :expiry, :verification_value,
+ :first_name, :last_name, :cc_type, :gateway_customer_profile_id,
+ :gateway_payment_profile_id, :last_digits, :name, :encrypted_data
+ ]
+
+ @@stock_item_attributes = [:variant, :stock_location, :backorderable, :variant_id]
+
+ @@stock_location_attributes = [
+ :name, :active, :address1, :address2, :city, :zipcode,
+ :backorderable_default, :state_name, :state_id, :country_id, :phone,
+ :propagate_all_variants
+ ]
+
+ @@stock_movement_attributes = [
+ :quantity, :stock_item, :stock_item_id, :originator, :action
+ ]
+
+ @@store_attributes = [:name, :url, :seo_title, :code, :meta_keywords,
+ :meta_description, :default_currency, :mail_from_address]
+
+ @@store_credit_attributes = %i[amount currency category_id memo]
+
+ @@taxonomy_attributes = [:name]
+
+ @@taxon_attributes = [
+ :name, :parent_id, :position, :icon, :description, :permalink, :taxonomy_id,
+ :meta_description, :meta_keywords, :meta_title, :child_index
+ ]
+
+ # TODO: Should probably use something like Spree.user_class.attributes
+ @@user_attributes = [:email, :password, :password_confirmation]
+
+ @@variant_attributes = [
+ :name, :presentation, :cost_price, :discontinue_on, :lock_version,
+ :position, :track_inventory,
+ :product_id, :product, :option_values_attributes, :price,
+ :weight, :height, :width, :depth, :sku, :cost_currency,
+ options: [:name, :value], option_value_ids: []
+ ]
+ end
+end
diff --git a/core/lib/spree/responder.rb b/core/lib/spree/responder.rb
new file mode 100644
index 00000000000..a5cde0fd244
--- /dev/null
+++ b/core/lib/spree/responder.rb
@@ -0,0 +1,44 @@
+module Spree
+ class Responder < ::ActionController::Responder #:nodoc:
+ attr_accessor :on_success, :on_failure
+
+ def initialize(controller, resources, options = {})
+ super
+
+ class_name = controller.class.name.to_sym
+ action_name = options.delete(:action_name)
+
+ if result = Spree::BaseController.spree_responders[class_name].try(:[], action_name).try(:[], format.to_sym)
+ self.on_success = handler(controller, result, :success)
+ self.on_failure = handler(controller, result, :failure)
+ end
+ end
+
+ def to_html
+ super && return unless on_success || on_failure
+ has_errors? ? controller.instance_exec(&on_failure) : controller.instance_exec(&on_success)
+ end
+
+ def to_format
+ super && return unless on_success || on_failure
+ has_errors? ? controller.instance_exec(&on_failure) : controller.instance_exec(&on_success)
+ end
+
+ private
+
+ def handler(controller, result, status)
+ return result if result.respond_to? :call
+
+ case result
+ when Hash
+ if result[status].is_a? Symbol
+ controller.method(result[status])
+ else
+ result[status]
+ end
+ when Symbol
+ controller.method(result)
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/service_module.rb b/core/lib/spree/service_module.rb
new file mode 100644
index 00000000000..189e7459a14
--- /dev/null
+++ b/core/lib/spree/service_module.rb
@@ -0,0 +1,98 @@
+module Spree
+ module ServiceModule
+ module Callable
+ def call(*args)
+ new.call(*args).tap do |result|
+ return yield(result) if block_given?
+ end
+ end
+ end
+
+ class MethodNotImplemented < StandardError; end
+ class WrongDataPassed < StandardError; end
+ class NonCallablePassedToRun < StandardError; end
+ class IncompatibleParamsPassed < StandardError; end
+
+ Result = Struct.new(:success, :value, :error) do
+ def success?
+ success
+ end
+
+ def failure?
+ !success
+ end
+ end
+
+ ResultError = Struct.new(:value) do
+ def to_s
+ return value.full_messages.join(', ') if value&.respond_to?(:full_messages)
+
+ value.to_s
+ end
+
+ def to_h
+ return value.messages if value&.respond_to?(:messages)
+
+ {}
+ end
+ end
+
+ module Base
+ def self.prepended(base)
+ class << base
+ prepend Callable
+ end
+ end
+
+ def call(input = nil)
+ input ||= {}
+ @_passed_input = Result.new(true, input)
+ result = super
+ @_passed_input = result if result.is_a? Result
+ enforce_data_format
+ @_passed_input
+ end
+
+ private
+
+ def run(callable)
+ return unless @_passed_input.success?
+
+ if callable.instance_of? Symbol
+ unless respond_to?(callable, true)
+ raise MethodNotImplemented, "You didn't implement #{callable} method. Implement it before calling this class"
+ end
+
+ callable = method(callable)
+ end
+
+ unless callable.respond_to?(:call)
+ raise NonCallablePassedToRun, 'You can pass only symbol with method name or instance of callable class to run method'
+ end
+
+ begin
+ @_passed_input = callable.call(@_passed_input.value)
+ rescue ArgumentError => e
+ if e.message.include? 'missing'
+ raise IncompatibleParamsPassed, "You didn't pass #{e.message} to callable '#{callable.name}'"
+ else
+ raise IncompatibleParamsPassed, "You passed #{e.message} to callable '#{callable.name}'"
+ end
+ end
+ end
+
+ def success(value)
+ Result.new(true, value, nil)
+ end
+
+ def failure(value, error = nil)
+ error = value.errors if error.nil? && value.respond_to?(:errors)
+ Result.new(false, value, ResultError.new(error))
+ end
+
+ def enforce_data_format
+ raise WrongDataPassed, "You didn't use `success` or `failure` method to return value from method." unless @_passed_input.instance_of? Result
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/ability_helpers.rb b/core/lib/spree/testing_support/ability_helpers.rb
new file mode 100644
index 00000000000..89c7a122b05
--- /dev/null
+++ b/core/lib/spree/testing_support/ability_helpers.rb
@@ -0,0 +1,105 @@
+shared_examples_for 'access granted' do
+ it 'should allow read' do
+ expect(ability).to be_able_to(:read, resource, token) if token
+ expect(ability).to be_able_to(:read, resource) unless token
+ end
+
+ it 'should allow create' do
+ expect(ability).to be_able_to(:create, resource, token) if token
+ expect(ability).to be_able_to(:create, resource) unless token
+ end
+
+ it 'should allow update' do
+ expect(ability).to be_able_to(:update, resource, token) if token
+ expect(ability).to be_able_to(:update, resource) unless token
+ end
+end
+
+shared_examples_for 'access denied' do
+ it 'should not allow read' do
+ expect(ability).to_not be_able_to(:read, resource)
+ end
+
+ it 'should not allow create' do
+ expect(ability).to_not be_able_to(:create, resource)
+ end
+
+ it 'should not allow update' do
+ expect(ability).to_not be_able_to(:update, resource)
+ end
+end
+
+shared_examples_for 'admin granted' do
+ it 'should allow admin' do
+ expect(ability).to be_able_to(:admin, resource, token) if token
+ expect(ability).to be_able_to(:admin, resource) unless token
+ end
+end
+
+shared_examples_for 'admin denied' do
+ it 'should not allow admin' do
+ expect(ability).to_not be_able_to(:admin, resource)
+ end
+end
+
+shared_examples_for 'index allowed' do
+ it 'should allow index' do
+ expect(ability).to be_able_to(:index, resource)
+ end
+end
+
+shared_examples_for 'no index allowed' do
+ it 'should not allow index' do
+ expect(ability).to_not be_able_to(:index, resource)
+ end
+end
+
+shared_examples_for 'create only' do
+ it 'should allow create' do
+ expect(ability).to be_able_to(:create, resource)
+ end
+
+ it 'should not allow read' do
+ expect(ability).to_not be_able_to(:read, resource)
+ end
+
+ it 'should not allow update' do
+ expect(ability).to_not be_able_to(:update, resource)
+ end
+
+ it 'should not allow index' do
+ expect(ability).to_not be_able_to(:index, resource)
+ end
+end
+
+shared_examples_for 'read only' do
+ it 'should not allow create' do
+ expect(ability).to_not be_able_to(:create, resource)
+ end
+
+ it 'should not allow update' do
+ expect(ability).to_not be_able_to(:update, resource)
+ end
+
+ it 'should allow index' do
+ expect(ability).to be_able_to(:index, resource)
+ end
+end
+
+shared_examples_for 'update only' do
+ it 'should not allow create' do
+ expect(ability).to_not be_able_to(:create, resource)
+ end
+
+ it 'should not allow read' do
+ expect(ability).to_not be_able_to(:read, resource)
+ end
+
+ it 'should allow update' do
+ expect(ability).to be_able_to(:update, resource)
+ end
+
+ it 'should not allow index' do
+ expect(ability).to_not be_able_to(:index, resource)
+ end
+end
diff --git a/core/lib/spree/testing_support/authorization_helpers.rb b/core/lib/spree/testing_support/authorization_helpers.rb
new file mode 100644
index 00000000000..5880d752c46
--- /dev/null
+++ b/core/lib/spree/testing_support/authorization_helpers.rb
@@ -0,0 +1,63 @@
+module Spree
+ module TestingSupport
+ module AuthorizationHelpers
+ module CustomAbility
+ def build_ability(&block)
+ block ||= proc { |_u| can :manage, :all }
+ Class.new do
+ include CanCan::Ability
+ define_method(:initialize, block)
+ end
+ end
+ end
+
+ module Controller
+ include CustomAbility
+
+ def stub_authorization!(&block)
+ ability_class = build_ability(&block)
+ before do
+ allow(controller).to receive(:current_ability).and_return(ability_class.new(nil))
+ end
+ end
+ end
+
+ module Request
+ include CustomAbility
+
+ def stub_authorization!
+ ability = build_ability
+
+ after(:all) do
+ Spree::Ability.remove_ability(ability)
+ end
+
+ before(:all) do
+ Spree::Ability.register_ability(ability)
+ end
+
+ before do
+ allow(Spree.user_class).to receive(:find_by).
+ with(hash_including(:spree_api_key)).
+ and_return(Spree.user_class.new)
+ end
+ end
+
+ def custom_authorization!(&block)
+ ability = build_ability(&block)
+ after(:all) do
+ Spree::Ability.remove_ability(ability)
+ end
+ before(:all) do
+ Spree::Ability.register_ability(ability)
+ end
+ end
+ end
+ end
+ end
+end
+
+RSpec.configure do |config|
+ config.extend Spree::TestingSupport::AuthorizationHelpers::Controller, type: :controller
+ config.extend Spree::TestingSupport::AuthorizationHelpers::Request, type: :feature
+end
diff --git a/core/lib/spree/testing_support/bar_ability.rb b/core/lib/spree/testing_support/bar_ability.rb
new file mode 100644
index 00000000000..ad23f1e4360
--- /dev/null
+++ b/core/lib/spree/testing_support/bar_ability.rb
@@ -0,0 +1,14 @@
+# Fake ability for testing administration
+class BarAbility
+ include CanCan::Ability
+
+ def initialize(user)
+ user ||= Spree::User.new
+ if user.has_spree_role? 'bar'
+ # allow dispatch to :admin, :index, and :show on Spree::Order
+ can [:admin, :index, :show], Spree::Order
+ # allow dispatch to :index, :show, :create and :update shipments on the admin
+ can [:admin, :manage], Spree::Shipment
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/caching.rb b/core/lib/spree/testing_support/caching.rb
new file mode 100644
index 00000000000..96a0297952a
--- /dev/null
+++ b/core/lib/spree/testing_support/caching.rb
@@ -0,0 +1,47 @@
+# module Spree
+# module TestingSupport
+# module Caching
+ # def cache_writes
+ # @cache_write_events
+ # end
+
+ # def assert_written_to_cache(key)
+ # unless @cache_write_events.detect { |event| event[:key].starts_with?(key) }
+ # raise %Q{Expected to find #{key} in the cache, but didn't.
+
+ # Cache writes:
+ # #{@cache_write_events.join("\n")}
+ # }
+ # end
+ # end
+
+ # def clear_cache_events
+ # @cache_read_events = []
+ # @cache_write_events = []
+ # end
+ # end
+ # end
+# end
+
+# RSpec.configure do |config|
+ # config.include Spree::TestingSupport::Caching, caching: true
+
+ # config.before(:each, caching: true) do
+ # ActionController::Base.perform_caching = true
+
+ # ActiveSupport::Notifications.subscribe('read_fragment.action_controller') do |_event, _start_time, _finish_time, _, details|
+ # @cache_read_events ||= []
+ # @cache_read_events << details
+ # end
+
+ # ActiveSupport::Notifications.subscribe('write_fragment.action_controller') do |_event, _start_time, _finish_time, _, details|
+ # @cache_write_events ||= []
+ # @cache_write_events << details
+ # end
+ # end
+
+ # config.after(:each, caching: true) do
+ # ActionController::Base.perform_caching = false
+ # Rails.cache.clear
+ # end
+# end
diff --git a/core/lib/spree/testing_support/capybara_config.rb b/core/lib/spree/testing_support/capybara_config.rb
new file mode 100644
index 00000000000..288af7deecb
--- /dev/null
+++ b/core/lib/spree/testing_support/capybara_config.rb
@@ -0,0 +1,20 @@
+require 'capybara-screenshot/rspec'
+
+Capybara.save_path = ENV['CIRCLE_ARTIFACTS'] if ENV['CIRCLE_ARTIFACTS']
+
+if ENV['WEBDRIVER'] == 'accessible'
+ require 'capybara/accessible'
+ Capybara.javascript_driver = :accessible
+else
+ Capybara.register_driver :chrome do |app|
+ Capybara::Selenium::Driver.new app,
+ browser: :chrome,
+ options: Selenium::WebDriver::Chrome::Options.new(args: %w[disable-popup-blocking headless disable-gpu window-size=1920,1080])
+ end
+ Capybara.javascript_driver = :chrome
+
+ Capybara::Screenshot.register_driver(:chrome) do |driver, path|
+ driver.browser.save_screenshot(path)
+ end
+end
+Capybara.default_max_wait_time = 45
diff --git a/core/lib/spree/testing_support/capybara_ext.rb b/core/lib/spree/testing_support/capybara_ext.rb
new file mode 100644
index 00000000000..af00f7ecca0
--- /dev/null
+++ b/core/lib/spree/testing_support/capybara_ext.rb
@@ -0,0 +1,185 @@
+module CapybaraExt
+ # https://bugs.chromium.org/p/chromedriver/issues/detail?id=1771
+ def native_fill_in(selector, text)
+ text.to_s.split('').each { |char| find_field(selector).native.send_keys(char) }
+ end
+
+ def page!
+ save_and_open_page
+ end
+
+ def click_icon(type)
+ find(".icon-#{type}").click
+ end
+
+ def eventually_fill_in(field, options = {})
+ expect(page).to have_css('#' + field)
+ fill_in field, options
+ end
+
+ def within_row(num, &block)
+ if RSpec.current_example.metadata[:js]
+ within("table.table tbody tr:nth-child(#{num})", &block)
+ else
+ within(:xpath, all('table.table tbody tr')[num - 1].path, &block)
+ end
+ end
+
+ def column_text(num)
+ if RSpec.current_example.metadata[:js]
+ find("td:nth-child(#{num})").text
+ else
+ all('td')[num - 1].text
+ end
+ end
+
+ def set_select2_field(field, value)
+ page.execute_script %Q{$('#{field}').select2('val', '#{value}')}
+ end
+
+ def select2_search(value, options)
+ label = find_label_by_text(options[:from])
+ within label.first(:xpath, './/..') do
+ options[:from] = "##{find('.select2-container')['id']}"
+ end
+ targetted_select2_search(value, options)
+ end
+
+ def targetted_select2_search(value, options)
+ page.execute_script %Q{$('#{options[:from]}').select2('open')}
+ page.execute_script "$('#{options[:dropdown_css]} input.select2-input').val('#{value}').trigger('keyup-change');"
+ select_select2_result(value)
+ end
+
+ def select2(value, options)
+ label = find_label_by_text(options[:from])
+
+ within label.first(:xpath, './/..') do
+ options[:from] = "##{find('.select2-container')['id']}"
+ end
+ targetted_select2(value, options)
+ end
+
+ def select2_no_label(value, options = {})
+ raise "Must pass a hash containing 'from'" if !options.is_a?(Hash) || !options.key?(:from)
+
+ placeholder = options[:from]
+ click_link placeholder
+
+ select_select2_result(value)
+ end
+
+ def targetted_select2(value, options)
+ # find select2 element and click it
+ find(options[:from]).find('a').click
+ select_select2_result(value)
+ end
+
+ def select_select2_result(value)
+ # results are in a div appended to the end of the document
+ within(:xpath, '//body') do
+ page.find('div.select2-result-label', text: %r{#{Regexp.escape(value)}}i).click
+ end
+ end
+
+ def find_label_by_text(text)
+ label = find_label(text)
+ counter = 0
+
+ # Because JavaScript testing is prone to errors...
+ while label.nil? && counter < 10
+ sleep(1)
+ counter += 1
+ label = find_label(text)
+ end
+
+ raise "Could not find label by text #{text}" if label.nil?
+
+ label
+ end
+
+ def find_label(text)
+ first(:xpath, "//label[text()[contains(.,'#{text}')]]")
+ end
+
+ # arg delay in seconds
+ def wait_for_ajax(delay = Capybara.default_max_wait_time)
+ Timeout.timeout(delay) do
+ active = page.evaluate_script('typeof jQuery !== "undefined" && jQuery.active')
+ active = page.evaluate_script('typeof jQuery !== "undefined" && jQuery.active') until active.nil? || active.zero?
+ end
+ end
+
+ # "Intelligiently" wait on condition
+ #
+ # Much better than a random sleep "here and there"
+ # it will not cause any delay in case the condition is fullfilled on first cycle.
+
+ def wait_for_condition(delay = Capybara.default_max_wait_time)
+ counter = 0
+ delay_threshold = delay * 10
+ until yield
+ counter += 1
+ sleep(0.1)
+ raise "Could not achieve condition within #{delay} seconds." if counter >= delay_threshold
+ end
+ end
+
+ def dismiss_alert
+ page.evaluate_script('window.confirm = function() { return false; }')
+ yield
+ # Restore existing default
+ page.evaluate_script('window.confirm = function() { return true; }')
+ end
+
+ def spree_accept_alert
+ yield
+ rescue Selenium::WebDriver::Error::UnhandledAlertError
+ Selenium::WebDriver::Wait.new(timeout: 5)
+ .until { page.driver.browser.switch_to.alert }
+ .accept
+ end
+
+ def disable_html5_validation
+ page.execute_script('for(var f=document.forms,i=f.length;i--;)f[i].setAttribute("novalidate",i)')
+ end
+end
+
+Capybara.configure do |config|
+ config.match = :prefer_exact
+ config.ignore_hidden_elements = true
+end
+
+RSpec::Matchers.define :have_meta do |name, expected|
+ match do |_actual|
+ has_css?("meta[name='#{name}'][content='#{expected}']", visible: false)
+ end
+
+ failure_message do |actual|
+ actual = first("meta[name='#{name}']")
+ if actual
+ "expected that meta #{name} would have content='#{expected}' but was '#{actual[:content]}'"
+ else
+ "expected that meta #{name} would exist with content='#{expected}'"
+ end
+ end
+end
+
+RSpec::Matchers.define :have_title do |expected|
+ match do |_actual|
+ has_css?('title', text: expected, visible: false)
+ end
+
+ failure_message do |actual|
+ actual = first('title')
+ if actual
+ "expected that title would have been '#{expected}' but was '#{actual.text}'"
+ else
+ "expected that title would exist with '#{expected}'"
+ end
+ end
+end
+
+RSpec.configure do |c|
+ c.include CapybaraExt
+end
diff --git a/core/lib/spree/testing_support/common_rake.rb b/core/lib/spree/testing_support/common_rake.rb
new file mode 100644
index 00000000000..278f07282fe
--- /dev/null
+++ b/core/lib/spree/testing_support/common_rake.rb
@@ -0,0 +1,41 @@
+unless defined?(Spree::InstallGenerator)
+ require 'generators/spree/install/install_generator'
+end
+
+require 'generators/spree/dummy/dummy_generator'
+require 'generators/spree/dummy_model/dummy_model_generator'
+
+desc 'Generates a dummy app for testing'
+namespace :common do
+ task :test_app, :user_class do |_t, args|
+ args.with_defaults(user_class: 'Spree::LegacyUser')
+ require ENV['LIB_NAME'].to_s
+
+ ENV['RAILS_ENV'] = 'test'
+ Rails.env = 'test'
+
+ Spree::DummyGenerator.start ["--lib_name=#{ENV['LIB_NAME']}", '--quiet']
+ Spree::InstallGenerator.start ["--lib_name=#{ENV['LIB_NAME']}", '--auto-accept', '--migrate=false', '--seed=false', '--sample=false', '--quiet', '--copy_views=false', "--user_class=#{args[:user_class]}"]
+
+ puts 'Setting up dummy database...'
+ system("bundle exec rake db:drop db:create > #{File::NULL}")
+ Spree::DummyModelGenerator.start
+ system("bundle exec rake db:migrate > #{File::NULL}")
+
+ begin
+ require "generators/#{ENV['LIB_NAME']}/install/install_generator"
+ puts 'Running extension installation generator...'
+ "#{ENV['LIB_NAME'].camelize}::Generators::InstallGenerator".constantize.start(['--auto-run-migrations'])
+ rescue LoadError
+ puts 'Skipping installation no generator to run...'
+ end
+
+ puts 'Precompiling assets...'
+ system("bundle exec rake assets:precompile > #{File::NULL}")
+ end
+
+ task :seed do |_t|
+ puts 'Seeding ...'
+ system("bundle exec rake db:seed RAILS_ENV=test > #{File::NULL}")
+ end
+end
diff --git a/core/lib/spree/testing_support/controller_requests.rb b/core/lib/spree/testing_support/controller_requests.rb
new file mode 100644
index 00000000000..71539d1b82f
--- /dev/null
+++ b/core/lib/spree/testing_support/controller_requests.rb
@@ -0,0 +1,93 @@
+# Use this module to easily test Spree actions within Spree components
+# or inside your application to test routes for the mounted Spree engine.
+#
+# Inside your spec_helper.rb, include this module inside the RSpec.configure
+# block by doing this:
+#
+# require 'spree/testing_support/controller_requests'
+# RSpec.configure do |c|
+# c.include Spree::TestingSupport::ControllerRequests, type: :controller
+# end
+#
+# Then, in your controller tests, you can access spree routes like this:
+#
+# require 'spec_helper'
+#
+# describe Spree::ProductsController do
+# it "can see all the products" do
+# spree_get :index
+# end
+# end
+#
+# Use spree_get, spree_post, spree_put or spree_delete to make requests
+# to the Spree engine, and use regular get, post, put or delete to make
+# requests to your application.
+#
+module Spree
+ module TestingSupport
+ module ControllerRequests
+ extend ActiveSupport::Concern
+
+ included do
+ routes { Spree::Core::Engine.routes }
+ end
+
+ def spree_get(action, parameters = nil, session = nil, flash = nil)
+ process_spree_action(action, parameters, session, flash, 'GET')
+ end
+
+ # Executes a request simulating POST HTTP method and set/volley the response
+ def spree_post(action, parameters = nil, session = nil, flash = nil)
+ process_spree_action(action, parameters, session, flash, 'POST')
+ end
+
+ # Executes a request simulating PUT HTTP method and set/volley the response
+ def spree_put(action, parameters = nil, session = nil, flash = nil)
+ process_spree_action(action, parameters, session, flash, 'PUT')
+ end
+
+ # Executes a request simulating PATCH HTTP method and set/volley the response
+ def spree_patch(action, parameters = nil, session = nil, flash = nil)
+ process_spree_action(action, parameters, session, flash, 'PATCH')
+ end
+
+ # Executes a request simulating DELETE HTTP method and set/volley the response
+ def spree_delete(action, parameters = nil, session = nil, flash = nil)
+ process_spree_action(action, parameters, session, flash, 'DELETE')
+ end
+
+ def spree_xhr_get(action, parameters = nil, session = nil, flash = nil)
+ process_spree_xhr_action(action, parameters, session, flash, :get)
+ end
+
+ def spree_xhr_post(action, parameters = nil, session = nil, flash = nil)
+ process_spree_xhr_action(action, parameters, session, flash, :post)
+ end
+
+ def spree_xhr_put(action, parameters = nil, session = nil, flash = nil)
+ process_spree_xhr_action(action, parameters, session, flash, :put)
+ end
+
+ def spree_xhr_patch(action, parameters = nil, session = nil, flash = nil)
+ process_spree_xhr_action(action, parameters, session, flash, :patch)
+ end
+
+ def spree_xhr_delete(action, parameters = nil, session = nil, flash = nil)
+ process_spree_xhr_action(action, parameters, session, flash, :delete)
+ end
+
+ private
+
+ def process_spree_action(action, parameters = nil, session = nil, flash = nil, method = 'GET')
+ parameters ||= {}
+ process(action, method: method, params: parameters, session: session, flash: flash)
+ end
+
+ def process_spree_xhr_action(action, parameters = nil, session = nil, flash = nil, method = :get)
+ parameters ||= {}
+ parameters.reverse_merge!(format: :json)
+ process(action, method: method, params: parameters, session: session, flash: flash, xhr: true)
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/extension_rake.rb b/core/lib/spree/testing_support/extension_rake.rb
new file mode 100644
index 00000000000..f4d5dbaffd6
--- /dev/null
+++ b/core/lib/spree/testing_support/extension_rake.rb
@@ -0,0 +1,9 @@
+require 'spree/testing_support/common_rake'
+
+desc 'Generates a dummy app for testing an extension'
+namespace :extension do
+ task :test_app, [:user_class] do |_t, _args|
+ Spree::DummyGeneratorHelper.inject_extension_requirements = true
+ Rake::Task['common:test_app'].invoke
+ end
+end
diff --git a/core/lib/spree/testing_support/factories.rb b/core/lib/spree/testing_support/factories.rb
new file mode 100644
index 00000000000..4faa4931718
--- /dev/null
+++ b/core/lib/spree/testing_support/factories.rb
@@ -0,0 +1,19 @@
+require 'factory_bot'
+
+Spree::Zone.class_eval do
+ def self.global
+ find_by(name: 'GlobalZone') || FactoryBot.create(:global_zone)
+ end
+end
+
+Dir["#{File.dirname(__FILE__)}/factories/**"].each do |f|
+ load File.expand_path(f)
+end
+
+FactoryBot.define do
+ sequence(:random_string) { FFaker::Lorem.sentence }
+ sequence(:random_description) { FFaker::Lorem.paragraphs(Kernel.rand(1..5)).join("\n") }
+ sequence(:random_email) { FFaker::Internet.email }
+
+ sequence(:sku) { |n| "SKU-#{n}" }
+end
diff --git a/core/lib/spree/testing_support/factories/address_factory.rb b/core/lib/spree/testing_support/factories/address_factory.rb
new file mode 100644
index 00000000000..89e7a4ba7b3
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/address_factory.rb
@@ -0,0 +1,23 @@
+FactoryBot.define do
+ factory :address, aliases: [:bill_address, :ship_address], class: Spree::Address do
+ firstname { 'John' }
+ lastname { 'Doe' }
+ company { 'Company' }
+ address1 { '10 Lovely Street' }
+ address2 { 'Northwest' }
+ city { 'Herndon' }
+ zipcode { '35005' }
+ phone { '555-555-0199' }
+ alternative_phone { '555-555-0199' }
+
+ state { |address| address.association(:state) || Spree::State.last }
+
+ country do |address|
+ if address.state
+ address.state.country
+ else
+ address.association(:country)
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/adjustment_factory.rb b/core/lib/spree/testing_support/factories/adjustment_factory.rb
new file mode 100644
index 00000000000..0c5014cea1e
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/adjustment_factory.rb
@@ -0,0 +1,28 @@
+FactoryBot.define do
+ factory :adjustment, class: Spree::Adjustment do
+ association(:adjustable, factory: :order)
+ association(:source, factory: :tax_rate)
+
+ amount { 100.0 }
+ label { 'Shipping' }
+ eligible { true }
+ end
+
+ factory :tax_adjustment, class: Spree::Adjustment do
+ association(:adjustable, factory: :line_item)
+ association(:source, factory: :tax_rate)
+
+ amount { 10.0 }
+ label { 'VAT 5%' }
+ eligible { true }
+
+ after(:create) do |adjustment|
+ # Set correct tax category, so that adjustment amount is not 0
+ if adjustment.adjustable.is_a?(Spree::LineItem)
+ adjustment.source.tax_category = adjustment.adjustable.tax_category
+ adjustment.source.save
+ adjustment.update!
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/calculator_factory.rb b/core/lib/spree/testing_support/factories/calculator_factory.rb
new file mode 100644
index 00000000000..4c74c6b7c25
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/calculator_factory.rb
@@ -0,0 +1,20 @@
+FactoryBot.define do
+ factory :calculator, class: Spree::Calculator::FlatRate do
+ after(:create) { |c| c.set_preference(:amount, 10.0) }
+ end
+
+ factory :no_amount_calculator, class: Spree::Calculator::FlatRate do
+ after(:create) { |c| c.set_preference(:amount, 0) }
+ end
+
+ factory :default_tax_calculator, class: Spree::Calculator::DefaultTax do
+ end
+
+ factory :shipping_calculator, class: Spree::Calculator::Shipping::FlatRate do
+ after(:create) { |c| c.set_preference(:amount, 10.0) }
+ end
+
+ factory :shipping_no_amount_calculator, class: Spree::Calculator::Shipping::FlatRate do
+ after(:create) { |c| c.set_preference(:amount, 0) }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/country_factory.rb b/core/lib/spree/testing_support/factories/country_factory.rb
new file mode 100644
index 00000000000..437b3f9358a
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/country_factory.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :country, class: Spree::Country do
+ sequence(:iso_name) { |n| "ISO_NAME_#{n}" }
+ sequence(:name) { |n| "NAME_#{n}" }
+ iso { 'US' }
+ iso3 { 'USA' }
+ numcode { 840 }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/credit_card_factory.rb b/core/lib/spree/testing_support/factories/credit_card_factory.rb
new file mode 100644
index 00000000000..1d6fc66c5de
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/credit_card_factory.rb
@@ -0,0 +1,12 @@
+FactoryBot.define do
+ factory :credit_card, class: Spree::CreditCard do
+ verification_value { 123 }
+ month { 12 }
+ year { 1.year.from_now.year }
+ number { '4111111111111111' }
+ name { 'Spree Commerce' }
+ cc_type { 'visa' }
+
+ association(:payment_method, factory: :credit_card_payment_method)
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/customer_return_factory.rb b/core/lib/spree/testing_support/factories/customer_return_factory.rb
new file mode 100644
index 00000000000..0472ad08c9f
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/customer_return_factory.rb
@@ -0,0 +1,29 @@
+FactoryBot.define do
+ factory :customer_return, class: Spree::CustomerReturn do
+ association(:stock_location, factory: :stock_location)
+
+ transient do
+ line_items_count { 1 }
+ return_items_count { line_items_count }
+ end
+
+ before(:create) do |customer_return, evaluator|
+ shipped_order = create(:shipped_order, line_items_count: evaluator.line_items_count)
+
+ shipped_order.inventory_units.take(evaluator.return_items_count).each do |inventory_unit|
+ customer_return.return_items << build(:return_item, inventory_unit: inventory_unit)
+ end
+ end
+
+ factory :customer_return_with_accepted_items do
+ after(:create) do |customer_return|
+ customer_return.return_items.each(&:accept!)
+ end
+ end
+ end
+
+ # for the case when you want to supply existing return items instead of generating some
+ factory :customer_return_without_return_items, class: Spree::CustomerReturn do
+ association(:stock_location, factory: :stock_location)
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/image_factory.rb b/core/lib/spree/testing_support/factories/image_factory.rb
new file mode 100644
index 00000000000..342670409aa
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/image_factory.rb
@@ -0,0 +1,11 @@
+FactoryBot.define do
+ factory :image, class: Spree::Image do
+ if Rails.application.config.use_paperclip
+ attachment { File.new(Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg') }
+ else
+ before(:create) do |image|
+ image.attachment.attach(io: File.new(Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg'), filename: 'thinking-cat.jpg')
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/inventory_unit_factory.rb b/core/lib/spree/testing_support/factories/inventory_unit_factory.rb
new file mode 100644
index 00000000000..c6676c9ed69
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/inventory_unit_factory.rb
@@ -0,0 +1,18 @@
+FactoryBot.define do
+ factory :inventory_unit, class: Spree::InventoryUnit do
+ variant
+ order
+ line_item
+ state { 'on_hand' }
+
+ association(:shipment, factory: :shipment, state: 'pending')
+ # return_authorization
+
+ # this trait usage increases build speed ~ 2x
+ trait :without_assoc do
+ shipment { nil }
+ order { nil }
+ line_item { nil }
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/line_item_factory.rb b/core/lib/spree/testing_support/factories/line_item_factory.rb
new file mode 100644
index 00000000000..1ea33ae6df4
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/line_item_factory.rb
@@ -0,0 +1,12 @@
+FactoryBot.define do
+ factory :line_item, class: Spree::LineItem do
+ order
+ quantity { 1 }
+ price { BigDecimal('10.00') }
+ currency { order.currency }
+ transient do
+ association :product
+ end
+ variant { product.master }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/options_factory.rb b/core/lib/spree/testing_support/factories/options_factory.rb
new file mode 100644
index 00000000000..aa775901e9a
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/options_factory.rb
@@ -0,0 +1,12 @@
+FactoryBot.define do
+ factory :option_value, class: Spree::OptionValue do
+ sequence(:name) { |n| "Size-#{n}" }
+ presentation { 'S' }
+ option_type
+ end
+
+ factory :option_type, class: Spree::OptionType do
+ sequence(:name) { |n| "foo-size-#{n}" }
+ presentation { 'Size' }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/order_factory.rb b/core/lib/spree/testing_support/factories/order_factory.rb
new file mode 100644
index 00000000000..b565368d9e5
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/order_factory.rb
@@ -0,0 +1,102 @@
+FactoryBot.define do
+ factory :order, class: Spree::Order do
+ user
+ bill_address
+ store
+ completed_at { nil }
+ email { user.email }
+ currency { 'USD' }
+
+ transient do
+ line_items_price { BigDecimal(10) }
+ end
+
+ factory :order_with_totals do
+ after(:create) do |order, evaluator|
+ create(:line_item, order: order, price: evaluator.line_items_price)
+ order.line_items.reload # to ensure order.line_items is accessible after
+ end
+ end
+
+ factory :order_with_line_item_quantity do
+ transient do
+ line_items_quantity { 1 }
+ end
+
+ after(:create) do |order, evaluator|
+ create(:line_item, order: order, price: evaluator.line_items_price, quantity: evaluator.line_items_quantity)
+ order.line_items.reload # to ensure order.line_items is accessible after
+ end
+ end
+
+ factory :order_with_line_items do
+ bill_address
+ ship_address
+
+ transient do
+ line_items_count { 1 }
+ without_line_items { false }
+ shipment_cost { 100 }
+ shipping_method_filter { Spree::ShippingMethod::DISPLAY_ON_FRONT_END }
+ end
+
+ after(:create) do |order, evaluator|
+ unless evaluator.without_line_items
+ create_list(:line_item, evaluator.line_items_count, order: order, price: evaluator.line_items_price)
+ order.line_items.reload
+ end
+
+ create(:shipment, order: order, cost: evaluator.shipment_cost)
+ order.shipments.reload
+
+ order.update_with_updater!
+ end
+
+ factory :completed_order_with_totals do
+ state { 'complete' }
+
+ after(:create) do |order, evaluator|
+ order.refresh_shipment_rates(evaluator.shipping_method_filter)
+ order.update_column(:completed_at, Time.current)
+ end
+
+ factory :completed_order_with_pending_payment do
+ after(:create) do |order|
+ create(:payment, amount: order.total, order: order)
+ end
+ end
+
+ factory :completed_order_with_store_credit_payment do
+ after(:create) do |order|
+ create(:store_credit_payment, amount: order.total, order: order)
+ end
+ end
+
+ factory :order_ready_to_ship do
+ payment_state { 'paid' }
+ shipment_state { 'ready' }
+
+ after(:create) do |order|
+ create(:payment, amount: order.total, order: order, state: 'completed')
+ order.shipments.each do |shipment|
+ shipment.inventory_units.update_all state: 'on_hand'
+ shipment.update_column('state', 'ready')
+ end
+ order.reload
+ end
+
+ factory :shipped_order do
+ after(:create) do |order|
+ order.shipments.each do |shipment|
+ shipment.inventory_units.update_all state: 'shipped'
+ shipment.update_column('state', 'shipped')
+ end
+ order.update_column('shipment_state', 'shipped')
+ order.reload
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/order_promotion_factory.rb b/core/lib/spree/testing_support/factories/order_promotion_factory.rb
new file mode 100644
index 00000000000..b782e95d9bd
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/order_promotion_factory.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :order_promotion, class: Spree::OrderPromotion do
+ order
+ promotion
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/payment_factory.rb b/core/lib/spree/testing_support/factories/payment_factory.rb
new file mode 100644
index 00000000000..d46f7991edf
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/payment_factory.rb
@@ -0,0 +1,30 @@
+FactoryBot.define do
+ factory :payment, class: Spree::Payment do
+ order
+ amount { 45.75 }
+ state { 'checkout' }
+ response_code { '12345' }
+
+ association(:payment_method, factory: :credit_card_payment_method)
+ association(:source, factory: :credit_card)
+
+ factory :payment_with_refund do
+ state { 'completed' }
+ after :create do |payment|
+ create(:refund, amount: 5, payment: payment)
+ end
+ end
+ end
+
+ factory :check_payment, class: Spree::Payment do
+ amount { 45.75 }
+ order
+
+ association(:payment_method, factory: :check_payment_method)
+ end
+
+ factory :store_credit_payment, class: Spree::Payment, parent: :payment do
+ association(:payment_method, factory: :store_credit_payment_method)
+ association(:source, factory: :store_credit)
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/payment_method_factory.rb b/core/lib/spree/testing_support/factories/payment_method_factory.rb
new file mode 100644
index 00000000000..2415d49c130
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/payment_method_factory.rb
@@ -0,0 +1,23 @@
+FactoryBot.define do
+ factory :check_payment_method, class: Spree::PaymentMethod::Check do
+ name { 'Check' }
+ end
+
+ factory :credit_card_payment_method, class: Spree::Gateway::Bogus do
+ name { 'Credit Card' }
+ end
+
+ # authorize.net was moved to spree_gateway.
+ # Leaving this factory in place with bogus in case anyone is using it.
+ factory :simple_credit_card_payment_method, class: Spree::Gateway::BogusSimple do
+ name { 'Credit Card' }
+ end
+
+ factory :store_credit_payment_method, class: Spree::PaymentMethod::StoreCredit do
+ type { 'Spree::PaymentMethod::StoreCredit' }
+ name { 'Store Credit' }
+ description { 'Store Credit' }
+ active { true }
+ auto_capture { true }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/price_factory.rb b/core/lib/spree/testing_support/factories/price_factory.rb
new file mode 100644
index 00000000000..c0668978083
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/price_factory.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :price, class: Spree::Price do
+ variant
+ amount { 19.99 }
+ currency { 'USD' }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/product_factory.rb b/core/lib/spree/testing_support/factories/product_factory.rb
new file mode 100644
index 00000000000..ffafe2a9f82
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/product_factory.rb
@@ -0,0 +1,36 @@
+FactoryBot.define do
+ factory :base_product, class: Spree::Product do
+ sequence(:name) { |n| "Product ##{n} - #{Kernel.rand(9999)}" }
+ description { generate(:random_description) }
+ price { 19.99 }
+ cost_price { 17.00 }
+ sku { generate(:sku) }
+ available_on { 1.year.ago }
+ deleted_at { nil }
+ shipping_category { |r| Spree::ShippingCategory.first || r.association(:shipping_category) }
+
+ # ensure stock item will be created for this products master
+ before(:create) { create(:stock_location) unless Spree::StockLocation.any? }
+
+ factory :custom_product do
+ name { 'Custom Product' }
+ price { 17.99 }
+
+ tax_category { |r| Spree::TaxCategory.first || r.association(:tax_category) }
+ end
+
+ factory :product do
+ tax_category { |r| Spree::TaxCategory.first || r.association(:tax_category) }
+
+ factory :product_in_stock do
+ after :create do |product|
+ product.master.stock_items.first.adjust_count_on_hand(10)
+ end
+ end
+
+ factory :product_with_option_types do
+ after(:create) { |product| create(:product_option_type, product: product) }
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/product_option_type_factory.rb b/core/lib/spree/testing_support/factories/product_option_type_factory.rb
new file mode 100644
index 00000000000..2b6e52b6443
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/product_option_type_factory.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :product_option_type, class: Spree::ProductOptionType do
+ product
+ option_type
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/product_property_factory.rb b/core/lib/spree/testing_support/factories/product_property_factory.rb
new file mode 100644
index 00000000000..9844faea47d
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/product_property_factory.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :product_property, class: Spree::ProductProperty do
+ product
+ property
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/promotion_category_factory.rb b/core/lib/spree/testing_support/factories/promotion_category_factory.rb
new file mode 100644
index 00000000000..b8d067d1f12
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/promotion_category_factory.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :promotion_category, class: Spree::PromotionCategory do
+ name { 'Promotion Category' }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/promotion_factory.rb b/core/lib/spree/testing_support/factories/promotion_factory.rb
new file mode 100644
index 00000000000..01d418a5e30
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/promotion_factory.rb
@@ -0,0 +1,61 @@
+FactoryBot.define do
+ factory :promotion, class: Spree::Promotion do
+ name { 'Promo' }
+
+ trait :with_line_item_adjustment do
+ transient do
+ adjustment_rate { 10 }
+ end
+
+ after(:create) do |promotion, evaluator|
+ calculator = Spree::Calculator::FlatRate.new
+ calculator.preferred_amount = evaluator.adjustment_rate
+ Spree::Promotion::Actions::CreateItemAdjustments.create!(calculator: calculator, promotion: promotion)
+ end
+ end
+ factory :promotion_with_item_adjustment, traits: [:with_line_item_adjustment]
+
+ trait :with_order_adjustment do
+ transient do
+ weighted_order_adjustment_amount { 10 }
+ end
+
+ after(:create) do |promotion, evaluator|
+ calculator = Spree::Calculator::FlatRate.new
+ calculator.preferred_amount = evaluator.weighted_order_adjustment_amount
+ action = Spree::Promotion::Actions::CreateAdjustment.create!(calculator: calculator)
+ promotion.actions << action
+ promotion.save!
+ end
+ end
+ factory :promotion_with_order_adjustment, traits: [:with_order_adjustment]
+
+ trait :with_item_total_rule do
+ transient do
+ item_total_threshold_amount { 10 }
+ end
+
+ after(:create) do |promotion, evaluator|
+ rule = Spree::Promotion::Rules::ItemTotal.create!(
+ preferred_operator_min: 'gte',
+ preferred_operator_max: 'lte',
+ preferred_amount_min: evaluator.item_total_threshold_amount,
+ preferred_amount_max: evaluator.item_total_threshold_amount + 100
+ )
+ promotion.rules << rule
+ promotion.save!
+ end
+ end
+ factory :promotion_with_item_total_rule, traits: [:with_item_total_rule]
+ end
+
+ factory :free_shipping_promotion, class: Spree::Promotion do
+ name { 'Free Shipping Promotion' }
+
+ after(:create) do |promotion|
+ action = Spree::Promotion::Actions::FreeShipping.new
+ action.promotion = promotion
+ action.save
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/promotion_rule_factory.rb b/core/lib/spree/testing_support/factories/promotion_rule_factory.rb
new file mode 100644
index 00000000000..7d0315bb025
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/promotion_rule_factory.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :promotion_rule, class: Spree::PromotionRule do
+ association :promotion
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/property_factory.rb b/core/lib/spree/testing_support/factories/property_factory.rb
new file mode 100644
index 00000000000..592fba7f519
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/property_factory.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :property, class: Spree::Property do
+ name { 'baseball_cap_color' }
+ presentation { 'cap color' }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/prototype_factory.rb b/core/lib/spree/testing_support/factories/prototype_factory.rb
new file mode 100644
index 00000000000..88f6452d83e
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/prototype_factory.rb
@@ -0,0 +1,11 @@
+FactoryBot.define do
+ factory :prototype, class: Spree::Prototype do
+ name { 'Baseball Cap' }
+ properties { [create(:property)] }
+ end
+ factory :prototype_with_option_types, class: Spree::Prototype do
+ name { 'Baseball Cap' }
+ properties { [create(:property)] }
+ option_types { [create(:option_type)] }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/refund_factory.rb b/core/lib/spree/testing_support/factories/refund_factory.rb
new file mode 100644
index 00000000000..37e91aed1fc
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/refund_factory.rb
@@ -0,0 +1,22 @@
+FactoryBot.define do
+ sequence(:refund_transaction_id) { |n| "fake-refund-transaction-#{n}" }
+
+ factory :refund, class: Spree::Refund do
+ amount { 100.00 }
+ transaction_id { generate(:refund_transaction_id) }
+ association(:payment, state: 'completed')
+ association(:reason, factory: :refund_reason)
+ end
+
+ factory :default_refund_reason, class: Spree::RefundReason do
+ name { 'Return processing' }
+ active { true }
+ mutable { false }
+ end
+
+ factory :refund_reason, class: Spree::RefundReason do
+ sequence(:name) { |n| "Refund for return ##{n}" }
+ active { true }
+ mutable { false }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/reimbursement_factory.rb b/core/lib/spree/testing_support/factories/reimbursement_factory.rb
new file mode 100644
index 00000000000..aeec6d325f2
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/reimbursement_factory.rb
@@ -0,0 +1,16 @@
+FactoryBot.define do
+ factory :reimbursement, class: Spree::Reimbursement do
+ transient do
+ return_items_count { 1 }
+ end
+
+ customer_return { create(:customer_return_with_accepted_items, line_items_count: return_items_count) }
+
+ before(:create) do |reimbursement, _evaluator|
+ reimbursement.order ||= reimbursement.customer_return.order
+ if reimbursement.return_items.empty?
+ reimbursement.return_items = reimbursement.customer_return.return_items
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/reimbursement_type_factory.rb b/core/lib/spree/testing_support/factories/reimbursement_type_factory.rb
new file mode 100644
index 00000000000..1ba226a6cf3
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/reimbursement_type_factory.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :reimbursement_type, class: Spree::ReimbursementType do
+ sequence(:name) { |n| "Reimbursement Type #{n}" }
+ active { true }
+ mutable { true }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/return_authorization_factory.rb b/core/lib/spree/testing_support/factories/return_authorization_factory.rb
new file mode 100644
index 00000000000..3f3b6851c71
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/return_authorization_factory.rb
@@ -0,0 +1,21 @@
+FactoryBot.define do
+ factory :return_authorization, class: Spree::ReturnAuthorization do
+ association(:order, factory: :shipped_order)
+ association(:stock_location, factory: :stock_location)
+ association(:reason, factory: :return_authorization_reason)
+
+ memo { 'Items were broken' }
+ end
+
+ factory :new_return_authorization, class: Spree::ReturnAuthorization do
+ association(:order, factory: :shipped_order)
+ association(:stock_location, factory: :stock_location)
+ association(:reason, factory: :return_authorization_reason)
+ end
+
+ factory :return_authorization_reason, class: Spree::ReturnAuthorizationReason do
+ sequence(:name) { |n| "Defect ##{n}" }
+ active { true }
+ mutable { false }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/return_item_factory.rb b/core/lib/spree/testing_support/factories/return_item_factory.rb
new file mode 100644
index 00000000000..8f93cbc1fb3
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/return_item_factory.rb
@@ -0,0 +1,14 @@
+FactoryBot.define do
+ factory :return_item, class: Spree::ReturnItem do
+ association(:inventory_unit, factory: :inventory_unit, state: :shipped)
+ association(:return_authorization, factory: :return_authorization)
+
+ factory :exchange_return_item do
+ after(:build) do |return_item|
+ # set track_inventory to false to ensure it passes the in_stock check
+ return_item.inventory_unit.variant.update_column(:track_inventory, false)
+ return_item.exchange_variant = return_item.inventory_unit.variant
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/role_factory.rb b/core/lib/spree/testing_support/factories/role_factory.rb
new file mode 100644
index 00000000000..0742c5948f1
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/role_factory.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :role, class: Spree::Role do
+ sequence(:name) { |n| "Role ##{n}" }
+
+ factory :admin_role do
+ name { 'admin' }
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/shipment_factory.rb b/core/lib/spree/testing_support/factories/shipment_factory.rb
new file mode 100644
index 00000000000..8279d58a209
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/shipment_factory.rb
@@ -0,0 +1,23 @@
+FactoryBot.define do
+ factory :shipment, class: Spree::Shipment do
+ tracking { 'U10000' }
+ cost { 100.00 }
+ state { 'pending' }
+ order
+ stock_location
+
+ after(:create) do |shipment, _evalulator|
+ shipment.add_shipping_method(create(:shipping_method), true)
+
+ shipment.order.line_items.each do |line_item|
+ line_item.quantity.times do
+ shipment.inventory_units.create(
+ order_id: shipment.order_id,
+ variant_id: line_item.variant_id,
+ line_item_id: line_item.id
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/shipping_category_factory.rb b/core/lib/spree/testing_support/factories/shipping_category_factory.rb
new file mode 100644
index 00000000000..beec7a76955
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/shipping_category_factory.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :shipping_category, class: Spree::ShippingCategory do
+ sequence(:name) { |n| "ShippingCategory ##{n}" }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/shipping_method_factory.rb b/core/lib/spree/testing_support/factories/shipping_method_factory.rb
new file mode 100644
index 00000000000..e0db7feb5b8
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/shipping_method_factory.rb
@@ -0,0 +1,22 @@
+FactoryBot.define do
+ factory :base_shipping_method, class: Spree::ShippingMethod do
+ zones { |_a| [Spree::Zone.global] }
+ name { 'UPS Ground' }
+ code { 'UPS_GROUND' }
+ display_on { 'both' }
+
+ before(:create) do |shipping_method, _evaluator|
+ if shipping_method.shipping_categories.empty?
+ shipping_method.shipping_categories << (Spree::ShippingCategory.first || create(:shipping_category))
+ end
+ end
+
+ factory :shipping_method, class: Spree::ShippingMethod do
+ association(:calculator, factory: :shipping_calculator, strategy: :build)
+ end
+
+ factory :free_shipping_method, class: Spree::ShippingMethod do
+ association(:calculator, factory: :shipping_no_amount_calculator, strategy: :build)
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/state_factory.rb b/core/lib/spree/testing_support/factories/state_factory.rb
new file mode 100644
index 00000000000..78945d675c9
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/state_factory.rb
@@ -0,0 +1,10 @@
+FactoryBot.define do
+ factory :state, class: Spree::State do
+ sequence(:name) { |n| "STATE_NAME_#{n}" }
+ sequence(:abbr) { |n| "STATE_ABBR_#{n}" }
+ country do |country|
+ usa = Spree::Country.find_by(numcode: 840)
+ usa.present? ? usa : country.association(:country)
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/stock_factory.rb b/core/lib/spree/testing_support/factories/stock_factory.rb
new file mode 100644
index 00000000000..88fb5091672
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/stock_factory.rb
@@ -0,0 +1,31 @@
+FactoryBot.define do
+ # must use build()
+ factory :stock_packer, class: Spree::Stock::Packer do
+ transient do
+ stock_location { build(:stock_location) }
+ contents { [] }
+ end
+
+ initialize_with { new(stock_location, contents) }
+ end
+
+ factory :stock_package, class: Spree::Stock::Package do
+ transient do
+ stock_location { build(:stock_location) }
+ contents { [] }
+ variants_contents { {} }
+ end
+
+ initialize_with { new(stock_location, contents) }
+
+ after(:build) do |package, evaluator|
+ evaluator.variants_contents.each do |variant, count|
+ package.add_multiple build_list(:inventory_unit, count, variant: variant)
+ end
+ end
+
+ factory :stock_package_fulfilled do
+ transient { variants_contents { { build(:variant) => 2 } } }
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/stock_item_factory.rb b/core/lib/spree/testing_support/factories/stock_item_factory.rb
new file mode 100644
index 00000000000..1494aa319d1
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/stock_item_factory.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :stock_item, class: Spree::StockItem do
+ backorderable { true }
+ stock_location
+ variant
+
+ after(:create) { |object| object.adjust_count_on_hand(10) }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/stock_location_factory.rb b/core/lib/spree/testing_support/factories/stock_location_factory.rb
new file mode 100644
index 00000000000..8384ae47518
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/stock_location_factory.rb
@@ -0,0 +1,28 @@
+FactoryBot.define do
+ factory :stock_location, class: Spree::StockLocation do
+ name { FFaker::Name.unique.name }
+ address1 { '1600 Pennsylvania Ave NW' }
+ city { 'Washington' }
+ zipcode { '20500' }
+ phone { '(202) 456-1111' }
+ active { true }
+ backorderable_default { true }
+
+ country { |stock_location| Spree::Country.first || stock_location.association(:country) }
+ state do |stock_location|
+ stock_location.country.states.first || stock_location.association(:state, country: stock_location.country)
+ end
+
+ factory :stock_location_with_items do
+ after(:create) do |stock_location, _evaluator|
+ # variant will add itself to all stock_locations in an after_create
+ # creating a product will automatically create a master variant
+ product_1 = create(:product)
+ product_2 = create(:product)
+
+ stock_location.stock_items.where(variant_id: product_1.master.id).first.adjust_count_on_hand(10)
+ stock_location.stock_items.where(variant_id: product_2.master.id).first.adjust_count_on_hand(20)
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/stock_movement_factory.rb b/core/lib/spree/testing_support/factories/stock_movement_factory.rb
new file mode 100644
index 00000000000..decf7533eb5
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/stock_movement_factory.rb
@@ -0,0 +1,11 @@
+FactoryBot.define do
+ factory :stock_movement, class: Spree::StockMovement do
+ quantity { 1 }
+ action { 'sold' }
+ stock_item
+ end
+
+ trait :received do
+ action { 'received' }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/store_credit_category_factory.rb b/core/lib/spree/testing_support/factories/store_credit_category_factory.rb
new file mode 100644
index 00000000000..4eb29dcf99a
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/store_credit_category_factory.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :store_credit_category, class: Spree::StoreCreditCategory do
+ name { 'Exchange' }
+ end
+
+ factory :store_credit_gift_card_category, class: Spree::StoreCreditCategory do
+ name { Spree::StoreCreditCategory::GIFT_CARD_CATEGORY_NAME }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/store_credit_event_factory.rb b/core/lib/spree/testing_support/factories/store_credit_event_factory.rb
new file mode 100644
index 00000000000..76a02d36051
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/store_credit_event_factory.rb
@@ -0,0 +1,8 @@
+FactoryBot.define do
+ factory :store_credit_auth_event, class: Spree::StoreCreditEvent do
+ store_credit { create(:store_credit) }
+ action { Spree::StoreCredit::AUTHORIZE_ACTION }
+ amount { 100.00 }
+ authorization_code { "#{store_credit.id}-SC-20140602164814476128" }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/store_credit_factory.rb b/core/lib/spree/testing_support/factories/store_credit_factory.rb
new file mode 100644
index 00000000000..e6a782c0163
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/store_credit_factory.rb
@@ -0,0 +1,17 @@
+FactoryBot.define do
+ sequence(:store_credits_order_number) { |n| "R1000#{n}" }
+
+ factory :store_credit, class: Spree::StoreCredit do
+ user
+ created_by { create(:user) }
+ category { create(:store_credit_category) }
+ amount { 150.00 }
+ currency { 'USD' }
+ credit_type { create(:primary_credit_type) }
+ end
+
+ factory :store_credits_order_without_user, class: Spree::Order do
+ number { generate(:store_credits_order_number) }
+ bill_address
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/store_credit_type_factory.rb b/core/lib/spree/testing_support/factories/store_credit_type_factory.rb
new file mode 100644
index 00000000000..41deedef1d2
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/store_credit_type_factory.rb
@@ -0,0 +1,11 @@
+FactoryBot.define do
+ factory :primary_credit_type, class: Spree::StoreCreditType do
+ name { Spree::StoreCreditType::DEFAULT_TYPE_NAME }
+ priority { '1' }
+ end
+
+ factory :secondary_credit_type, class: Spree::StoreCreditType do
+ name { 'Non-expiring' }
+ priority { '2' }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/store_factory.rb b/core/lib/spree/testing_support/factories/store_factory.rb
new file mode 100644
index 00000000000..239b2b0610e
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/store_factory.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :store, class: Spree::Store do
+ sequence(:code) { |i| "spree_#{i}" }
+ name { 'Spree Test Store' }
+ url { 'www.example.com' }
+ mail_from_address { 'spree@example.org' }
+ default_currency { 'USD' }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/tag_factory.rb b/core/lib/spree/testing_support/factories/tag_factory.rb
new file mode 100644
index 00000000000..27f9be83f35
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/tag_factory.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :tag, class: Spree::Tag do
+ sequence(:name) { |n| "Tag ##{n} - #{Kernel.rand(9999)}" }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/tax_category_factory.rb b/core/lib/spree/testing_support/factories/tax_category_factory.rb
new file mode 100644
index 00000000000..f3240142860
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/tax_category_factory.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :tax_category, class: Spree::TaxCategory do
+ name { "TaxCategory - #{rand(999_999)}" }
+ description { generate(:random_string) }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/tax_rate_factory.rb b/core/lib/spree/testing_support/factories/tax_rate_factory.rb
new file mode 100644
index 00000000000..bcd9ace4686
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/tax_rate_factory.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+ factory :tax_rate, class: Spree::TaxRate do
+ zone
+ tax_category
+ amount { 0.1 }
+
+ association(:calculator, factory: :default_tax_calculator)
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/taxon_factory.rb b/core/lib/spree/testing_support/factories/taxon_factory.rb
new file mode 100644
index 00000000000..527368442a5
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/taxon_factory.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :taxon, class: Spree::Taxon do
+ sequence(:name) { |n| "taxon_#{n}" }
+ taxonomy
+ parent_id { taxonomy.root.id }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/taxonomy_factory.rb b/core/lib/spree/testing_support/factories/taxonomy_factory.rb
new file mode 100644
index 00000000000..26f528e3170
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/taxonomy_factory.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :taxonomy, class: Spree::Taxonomy do
+ sequence(:name) { |n| "taxonomy_#{n}" }
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/user_factory.rb b/core/lib/spree/testing_support/factories/user_factory.rb
new file mode 100644
index 00000000000..ad70459e665
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/user_factory.rb
@@ -0,0 +1,22 @@
+FactoryBot.define do
+ sequence :user_authentication_token do |n|
+ "xxxx#{Time.current.to_i}#{rand(1000)}#{n}xxxxxxxxxxxxx"
+ end
+
+ factory :user, class: Spree.user_class do
+ email { generate(:random_email) }
+ login { email }
+ password { 'secret' }
+ password_confirmation { password }
+ authentication_token { generate(:user_authentication_token) } if Spree.user_class.attribute_method? :authentication_token
+
+ factory :admin_user do
+ spree_roles { [Spree::Role.find_by(name: 'admin') || create(:role, name: 'admin')] }
+ end
+
+ factory :user_with_addresses, aliases: [:user_with_addreses] do
+ ship_address
+ bill_address
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/variant_factory.rb b/core/lib/spree/testing_support/factories/variant_factory.rb
new file mode 100644
index 00000000000..cf2e794ec03
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/variant_factory.rb
@@ -0,0 +1,38 @@
+FactoryBot.define do
+ sequence(:random_float) { BigDecimal("#{rand(200)}.#{rand(99)}") }
+
+ factory :base_variant, class: Spree::Variant do
+ price { 19.99 }
+ cost_price { 17.00 }
+ sku { generate(:sku) }
+ weight { generate(:random_float) }
+ height { generate(:random_float) }
+ width { generate(:random_float) }
+ depth { generate(:random_float) }
+ is_master { 0 }
+ track_inventory { true }
+
+ product { |p| p.association(:base_product) }
+ option_values { [create(:option_value)] }
+
+ # ensure stock item will be created for this variant
+ before(:create) { create(:stock_location) unless Spree::StockLocation.any? }
+
+ factory :variant do
+ # on_hand 5
+ product { |p| p.association(:product) }
+ end
+
+ factory :master_variant do
+ is_master { 1 }
+ end
+
+ factory :on_demand_variant do
+ track_inventory { false }
+
+ factory :on_demand_master_variant do
+ is_master { 1 }
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/zone_factory.rb b/core/lib/spree/testing_support/factories/zone_factory.rb
new file mode 100644
index 00000000000..fd8400bbc67
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/zone_factory.rb
@@ -0,0 +1,25 @@
+FactoryBot.define do
+ factory :global_zone, class: Spree::Zone do
+ name { 'GlobalZone' }
+ description { generate(:random_string) }
+ zone_members do |proxy|
+ zone = proxy.instance_eval { @instance }
+ Spree::Country.all.map do |c|
+ Spree::ZoneMember.create(zoneable: c, zone: zone)
+ end
+ end
+ end
+
+ factory :zone, class: Spree::Zone do
+ name { generate(:random_string) }
+ description { generate(:random_string) }
+
+ factory :zone_with_country do
+ zone_members do |proxy|
+ zone = proxy.instance_eval { @instance }
+ country = create(:country)
+ [Spree::ZoneMember.create(zoneable: country, zone: zone)]
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/factories/zone_member_factory.rb b/core/lib/spree/testing_support/factories/zone_member_factory.rb
new file mode 100644
index 00000000000..fa9a2ba84e5
--- /dev/null
+++ b/core/lib/spree/testing_support/factories/zone_member_factory.rb
@@ -0,0 +1,6 @@
+FactoryBot.define do
+ factory :zone_member, class: Spree::ZoneMember do
+ zone { |member| member.association(:zone) }
+ zoneable { |member| member.association(:zoneable) }
+ end
+end
diff --git a/core/lib/spree/testing_support/flash.rb b/core/lib/spree/testing_support/flash.rb
new file mode 100644
index 00000000000..9f6c9ca8944
--- /dev/null
+++ b/core/lib/spree/testing_support/flash.rb
@@ -0,0 +1,25 @@
+module Spree
+ module TestingSupport
+ module Flash
+ def assert_flash_success(flash)
+ flash = convert_flash(flash)
+
+ within("[class='flash success']") do
+ expect(page).to have_content(flash)
+ end
+ end
+
+ def assert_successful_update_message(resource)
+ flash = Spree.t(:successfully_updated, resource: Spree.t(resource))
+ assert_flash_success(flash)
+ end
+
+ private
+
+ def convert_flash(flash)
+ flash = Spree.t(flash) if flash.is_a?(Symbol)
+ flash
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/i18n.rb b/core/lib/spree/testing_support/i18n.rb
new file mode 100644
index 00000000000..d9643885af0
--- /dev/null
+++ b/core/lib/spree/testing_support/i18n.rb
@@ -0,0 +1,96 @@
+# This file exists solely to test whether or not there are missing translations
+# within the code that Spree's test suite covers.
+#
+# If there is a translation referenced which has no corresponding key within the
+# .yml file, then there will be a message output at the end of the suite showing
+# that.
+#
+# If there is a translation within the locale file which *isn't* used in the
+# test, this will also be shown at the end of the suite run.
+module Spree
+ class << self
+ attr_accessor :used_translations, :missing_translation_messages,
+ :unused_translations, :unused_translation_messages
+ alias normal_t t
+ end
+
+ def self.t(*args)
+ original_args = args.dup
+ options = args.extract_options!
+ self.used_translations ||= []
+ [*args.first].each do |translation_key|
+ key = ([*options[:scope]] << translation_key).join('.')
+ self.used_translations << key
+ end
+ normal_t(*original_args)
+ end
+
+ def self.check_missing_translations
+ self.missing_translation_messages = []
+ self.used_translations ||= []
+ used_translations.map { |a| a.split('.') }.each do |translation_keys|
+ root = translations
+ processed_keys = []
+ translation_keys.each do |key|
+ begin
+ root = root.fetch(key.to_sym)
+ processed_keys << key.to_sym
+ rescue KeyError
+ error = "#{(processed_keys << key).join('.')} (#{I18n.locale})"
+ unless Spree.missing_translation_messages.include?(error)
+ Spree.missing_translation_messages << error
+ end
+ end
+ end
+ end
+ end
+
+ def self.check_unused_translations
+ self.used_translations ||= []
+ self.unused_translation_messages = []
+ self.unused_translations = []
+ load_translations(translations)
+ translation_diff = unused_translations - used_translations
+ translation_diff.each do |translation|
+ Spree.unused_translation_messages << "#{translation} (#{I18n.locale})"
+ end
+ end
+
+ private
+
+ def self.load_translations(hash, root = [])
+ hash.each do |k, v|
+ if v.is_a?(Hash)
+ load_translations(v, root.dup << k)
+ else
+ key = (root + [k]).join('.')
+ unused_translations << key
+ end
+ end
+ end
+
+ def self.translations
+ @translations ||= I18n.backend.send(:translations)[I18n.locale][:spree]
+ end
+end
+
+RSpec.configure do |config|
+ # Need to check here again because this is used in i18n_spec too.
+ if ENV['CHECK_TRANSLATIONS']
+ config.after :suite do
+ Spree.check_missing_translations
+ if Spree.missing_translation_messages.any?
+ puts "\nThere are missing translations within Spree:"
+ puts Spree.missing_translation_messages.sort
+ exit(1)
+ end
+
+ Spree.check_unused_translations
+ if false && Spree.unused_translation_messages.any?
+ puts "\nThere are unused translations within Spree:"
+ puts Spree.unused_translation_messages.sort
+ exit(1)
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/image_helpers.rb b/core/lib/spree/testing_support/image_helpers.rb
new file mode 100644
index 00000000000..de35723b309
--- /dev/null
+++ b/core/lib/spree/testing_support/image_helpers.rb
@@ -0,0 +1,19 @@
+module Spree
+ module TestingSupport
+ module ImageHelpers
+ def create_image(attachable, file)
+ # user paperclip to attach an image
+ if Rails.application.config.use_paperclip
+ attachable.images.create!(attachment: file)
+ # use ActiveStorage (default)
+ else
+ image = attachable.images.new
+ image.attachment.attach(io: file, filename: File.basename(file))
+ image.save!
+ file.rewind
+ image
+ end
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/kernel.rb b/core/lib/spree/testing_support/kernel.rb
new file mode 100644
index 00000000000..923c7f8172a
--- /dev/null
+++ b/core/lib/spree/testing_support/kernel.rb
@@ -0,0 +1,17 @@
+module Spree
+ module TestingSupport
+ module Kernel
+ private
+
+ def silence_stream(stream)
+ old_stream = stream.dup
+ stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
+ stream.sync = true
+ yield
+ ensure
+ stream.reopen(old_stream)
+ old_stream.close
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/order_walkthrough.rb b/core/lib/spree/testing_support/order_walkthrough.rb
new file mode 100644
index 00000000000..375677a3caa
--- /dev/null
+++ b/core/lib/spree/testing_support/order_walkthrough.rb
@@ -0,0 +1,69 @@
+class OrderWalkthrough
+ def self.up_to(state)
+ # A default store must exist to provide store settings
+ FactoryBot.create(:store) unless Spree::Store.exists?
+
+ # A payment method must exist for an order to proceed through the Address state
+ unless Spree::PaymentMethod.exists?
+ FactoryBot.create(:check_payment_method)
+ end
+
+ # Need to create a valid zone too...
+ zone = FactoryBot.create(:zone)
+ country = FactoryBot.create(:country)
+ zone.members << Spree::ZoneMember.create(zoneable: country)
+ country.states << FactoryBot.create(:state, country: country)
+
+ # A shipping method must exist for rates to be displayed on checkout page
+ unless Spree::ShippingMethod.exists?
+ FactoryBot.create(:shipping_method).tap do |sm|
+ sm.calculator.preferred_amount = 10
+ sm.calculator.preferred_currency = Spree::Config[:currency]
+ sm.calculator.save
+ end
+ end
+
+ order = Spree::Order.create!(email: 'spree@example.com')
+ add_line_item!(order)
+ order.next!
+
+ end_state_position = states.index(state.to_sym)
+ states[0...end_state_position].each do |state|
+ send(state, order)
+ end
+
+ order
+ end
+
+ private
+
+ def self.add_line_item!(order)
+ FactoryBot.create(:line_item, order: order)
+ order.reload
+ end
+
+ def self.address(order)
+ order.bill_address = FactoryBot.create(:address, country_id: Spree::Zone.global.members.first.zoneable.id)
+ order.ship_address = FactoryBot.create(:address, country_id: Spree::Zone.global.members.first.zoneable.id)
+ order.next!
+ end
+
+ def self.delivery(order)
+ order.next!
+ end
+
+ def self.payment(order)
+ order.payments.create!(payment_method: Spree::PaymentMethod.first, amount: order.total)
+ # TODO: maybe look at some way of making this payment_state change automatic
+ order.payment_state = 'paid'
+ order.next!
+ end
+
+ def self.complete(_order)
+ # noop?
+ end
+
+ def self.states
+ [:address, :delivery, :payment, :complete]
+ end
+end
diff --git a/core/lib/spree/testing_support/preferences.rb b/core/lib/spree/testing_support/preferences.rb
new file mode 100644
index 00000000000..bf639bc56b1
--- /dev/null
+++ b/core/lib/spree/testing_support/preferences.rb
@@ -0,0 +1,30 @@
+module Spree
+ module TestingSupport
+ module Preferences
+ # Resets all preferences to default values, you can
+ # pass a block to override the defaults with a block
+ #
+ # reset_spree_preferences do |config|
+ # config.track_inventory_levels = false
+ # end
+ #
+ def reset_spree_preferences(&config_block)
+ Spree::Preferences::Store.instance.persistence = false
+ Spree::Preferences::Store.instance.clear_cache
+
+ config = Rails.application.config.spree.preferences
+ configure_spree_preferences &config_block if block_given?
+ end
+
+ def configure_spree_preferences
+ config = Rails.application.config.spree.preferences
+ yield(config) if block_given?
+ end
+
+ def assert_preference_unset(preference)
+ find("#preferences_#{preference}")['checked'].should be false
+ Spree::Config[preference].should be false
+ end
+ end
+ end
+end
diff --git a/core/lib/spree/testing_support/url_helpers.rb b/core/lib/spree/testing_support/url_helpers.rb
new file mode 100644
index 00000000000..d238624cc68
--- /dev/null
+++ b/core/lib/spree/testing_support/url_helpers.rb
@@ -0,0 +1,9 @@
+module Spree
+ module TestingSupport
+ module UrlHelpers
+ def spree
+ Spree::Core::Engine.routes.url_helpers
+ end
+ end
+ end
+end
diff --git a/core/lib/spree_core.rb b/core/lib/spree_core.rb
new file mode 100644
index 00000000000..e0b0a6176cd
--- /dev/null
+++ b/core/lib/spree_core.rb
@@ -0,0 +1,2 @@
+require 'friendly_id/slug_rails5_patch'
+require 'spree/core'
diff --git a/core/lib/tasks/core.rake b/core/lib/tasks/core.rake
new file mode 100644
index 00000000000..dcf36b89999
--- /dev/null
+++ b/core/lib/tasks/core.rake
@@ -0,0 +1,187 @@
+require 'active_record'
+
+namespace :db do
+ desc %q{Loads a specified fixture file:
+use rake db:load_file[/absolute/path/to/sample/filename.rb]}
+
+ task :load_file, [:file, :dir] => :environment do |_t, args|
+ file = Pathname.new(args.file)
+
+ puts "loading ruby #{file}"
+ require file
+ end
+
+ desc 'Loads fixtures from the the dir you specify using rake db:load_dir[loadfrom]'
+ task :load_dir, [:dir] => :environment do |_t, args|
+ dir = args.dir
+ dir = File.join(Rails.root, 'db', dir) if Pathname.new(dir).relative?
+
+ ruby_files = {}
+ Dir.glob(File.join(dir, '**/*.{rb}')).each do |fixture_file|
+ ext = File.extname fixture_file
+ ruby_files[File.basename(fixture_file, '.*')] = fixture_file
+ end
+ ruby_files.sort.each do |fixture, ruby_file|
+ # If file exists within application it takes precendence.
+ if File.exist?(File.join(Rails.root, 'db/default/spree', "#{fixture}.rb"))
+ ruby_file = File.expand_path(File.join(Rails.root, 'db/default/spree', "#{fixture}.rb"))
+ end
+ # an invoke will only execute the task once
+ Rake::Task['db:load_file'].execute(Rake::TaskArguments.new([:file], [ruby_file]))
+ end
+ end
+
+ desc 'Migrate schema to version 0 and back up again. WARNING: Destroys all data in tables!!'
+ task remigrate: :environment do
+ require 'highline/import'
+
+ if ENV['SKIP_NAG'] || ENV['OVERWRITE'].to_s.casecmp('true').zero? || agree("This task will destroy any data in the database. Are you sure you want to \ncontinue? [y/n] ")
+
+ # Drop all tables
+ ActiveRecord::Base.connection.tables.each { |t| ActiveRecord::Base.connection.drop_table t }
+
+ # Migrate upward
+ Rake::Task['db:migrate'].invoke
+
+ # Dump the schema
+ Rake::Task['db:schema:dump'].invoke
+ else
+ say 'Task cancelled.'
+ exit
+ end
+ end
+
+ desc 'Bootstrap is: migrating, loading defaults, sample data and seeding (for all extensions) and load_products tasks'
+ task :bootstrap do
+ require 'highline/import'
+
+ # remigrate unless production mode (as saftey check)
+ if %w[demo development test].include? Rails.env
+ if ENV['AUTO_ACCEPT'] || agree("This task will destroy any data in the database. Are you sure you want to \ncontinue? [y/n] ")
+ ENV['SKIP_NAG'] = 'yes'
+ Rake::Task['db:create'].invoke
+ Rake::Task['db:remigrate'].invoke
+ else
+ say 'Task cancelled, exiting.'
+ exit
+ end
+ else
+ say 'NOTE: Bootstrap in production mode will not drop database before migration'
+ Rake::Task['db:migrate'].invoke
+ end
+
+ ActiveRecord::Base.send(:subclasses).each(&:reset_column_information)
+
+ load_defaults = Spree::Country.count == 0
+ load_defaults ||= agree('Countries present, load sample data anyways? [y/n]: ')
+ Rake::Task['db:seed'].invoke if load_defaults
+
+ if Rails.env.production? && Spree::Product.count > 0
+ load_sample = agree('WARNING: In Production and products exist in database, load sample data anyways? [y/n]:')
+ else
+ load_sample = true if ENV['AUTO_ACCEPT']
+ load_sample ||= agree('Load Sample Data? [y/n]: ')
+ end
+
+ if load_sample
+ # Reload models' attributes in case they were loaded in old migrations with wrong attributes
+ ActiveRecord::Base.descendants.each(&:reset_column_information)
+ Rake::Task['spree_sample:load'].invoke
+ end
+
+ puts "Bootstrap Complete.\n\n"
+ end
+
+ desc 'Fix orphan line items after upgrading to Spree 3.1: only needed if you have line items attached to deleted records with Slug (product) and SKU (variant) duplicates of non-deleted records.'
+ task fix_orphan_line_items: :environment do |_t, _args|
+ def get_input
+ STDOUT.flush
+ input = STDIN.gets.chomp
+ case input.upcase
+ when 'Y'
+ return true
+
+ when 'N'
+ puts 'aborting .....'
+ return false
+ else
+ return true
+ end
+ end
+
+ puts 'WARNING: This task will re-associate any line_items associated with deleted variants to non-deleted variants with matching SKUs. Because other attributes and product associations may switch during the re-association, this may have unintended side-effects. If this task finishes successfully, line items for old order should no longer be orphaned from their varaints. You should run this task after you have already run the db migratoin AddDiscontinuedToProductsAndVariants. If the db migration did not warn you that it was leaving deleted records in place because of duplicate SKUs, then you do not need to run this rake task.'
+ puts 'Are you sure you want to continue? (Y/n):'
+
+ if get_input
+ puts 'looping through all your deleted variants ...'
+
+ # first verify that I can really fix all of your line items
+
+ no_live_variants_found = []
+ variants_to_fix = []
+
+ Spree::Variant.deleted.each do |variant|
+ # check if this variant has any line items at all
+ next if variant.line_items.none?
+
+ variants_to_fix << variant
+ dup_variant = Spree::Variant.find_by(sku: variant.sku)
+ if dup_variant
+ # this variant is OK
+ else
+ no_live_variants_found << variant
+ end
+ end
+
+ if variants_to_fix.none?
+ abort('ABORT: You have no deleted variants that are associated to line items. You do not need to run this raks task.')
+ end
+
+ if no_live_variants_found.any?
+ puts "ABORT: Unfortunately, I found some deleted variants in your database that do not have matching non-deleted variants to replace them with. This script can only be used to cleanup deleted variants that have SKUs that match non-deleted variants. To continue, you must either (1) un-delete these variants (hint: mark them 'discontinued' instead) or (2) create new variants with a matching SKU for each variant in the list below."
+ no_live_variants_found.each do |deleted_variant|
+ puts "variant id #{deleted_variant.id} (sku is '#{deleted_variant.sku}') ... no match found"
+ end
+ abort
+ end
+
+ puts 'Ready to fix...'
+ variants_to_fix.each do |variant|
+ dup_variant = Spree::Variant.find_by(sku: variant.sku)
+ puts "Changing all line items for #{variant.sku} variant id #{variant.id} (deleted) to variant id #{dup_variant.id} (not deleted) ..."
+ Spree::LineItem.unscoped.where(variant_id: variant.id).update_all(variant_id: dup_variant.id)
+ end
+
+ puts 'DONE ! Your database should no longer have line items that are associated with deleted variants.'
+ end
+ end
+
+ desc 'Migrates taxon icons to spree assets after upgrading to Spree 3.4: only needed if you used taxons icons.'
+ task migrate_taxon_icons: :environment do |_t, _args|
+ Spree::Taxon.where.not(icon_file_name: nil).find_each do |taxon|
+ taxon.create_icon(attachment_file_name: taxon.icon_file_name,
+ attachment_content_type: taxon.icon_content_type,
+ attachment_file_size: taxon.icon_file_size,
+ attachment_updated_at: taxon.icon_updated_at)
+ end
+ end
+
+ desc 'Migrates taxon icons to taxon images after upgrading to Spree 3.7: only needed if you used taxons icons.'
+ task migrate_taxon_icons_to_images: :environment do |_t, _args|
+ Spree::Asset.where(type: 'Spree::TaxonIcon').update_all(type: 'Spree::TaxonImage')
+ end
+
+ desc 'Ensure all Order associated with Store after upgrading to Spree 3.7'
+ task associate_orders_with_store: :environment do |_t, _args|
+ Spree::Order.where(store_id: nil).update_all(store_id: Spree::Store.default.id)
+ end
+
+ desc 'Ensure all Order has currency present after upgrading to Spree 3.7'
+ task ensure_order_currency_presence: :environment do |_t, _args|
+ Spree::Order.where(currency: nil).find_in_batches do |orders|
+ orders.each do |order|
+ order.update!(currency: order.store.default_currency || Spree::Config[:currency])
+ end
+ end
+ end
+end
diff --git a/core/lib/tasks/email.rake b/core/lib/tasks/email.rake
new file mode 100644
index 00000000000..87f1fc8dcd2
--- /dev/null
+++ b/core/lib/tasks/email.rake
@@ -0,0 +1,10 @@
+namespace :email do
+ desc 'Sends test email to specified address - Example: EMAIL=spree@example.com bundle exec rake email:test'
+ task test: :environment do
+ unless ENV['EMAIL'].present?
+ raise ArgumentError, 'Must pass EMAIL environment variable. ' \
+ 'Example: EMAIL=spree@example.com bundle exec rake email:test'
+ end
+ Spree::TestMailer.test_email(ENV['EMAIL']).deliver_now
+ end
+end
diff --git a/core/lib/tasks/exchanges.rake b/core/lib/tasks/exchanges.rake
new file mode 100644
index 00000000000..645908e4943
--- /dev/null
+++ b/core/lib/tasks/exchanges.rake
@@ -0,0 +1,68 @@
+namespace :exchanges do
+ desc %q{Takes unreturned exchanged items and creates a new order to charge
+ the customer for not returning them}
+ task charge_unreturned_items: :environment do
+ unreturned_return_items_scope = Spree::ReturnItem.awaiting_return.exchange_processed
+ unreturned_return_items = unreturned_return_items_scope.joins(:exchange_inventory_units).where([
+ 'spree_inventory_units.created_at < :days_ago AND spree_inventory_units.state = :iu_state',
+ days_ago: Spree::Config[:expedited_exchanges_days_window].days.ago, iu_state: 'shipped'
+ ]).distinct.to_a
+
+ # Determine that a return item has already been deemed unreturned and therefore charged
+ # by the fact that its exchange inventory unit has popped off to a different order
+ unreturned_return_items.select! { |ri| ri.exchange_inventory_units.exists?(order_id: ri.inventory_unit.order_id) }
+
+ failed_orders = []
+
+ unreturned_return_items.group_by(&:exchange_shipments).each do |shipments, return_items|
+ begin
+ original_order = shipments.first.order
+ order_attributes = {
+ bill_address: original_order.bill_address,
+ ship_address: original_order.ship_address,
+ email: original_order.email
+ }
+ order_attributes[:store_id] = original_order.store_id
+ order = Spree::Order.create!(order_attributes)
+
+ order.associate_user!(original_order.user) if original_order.user
+
+ return_items.group_by(&:exchange_variant).map do |variant, variant_return_items|
+ variant_inventory_units = variant_return_items.map(&:exchange_inventory_units).flatten
+ line_item = Spree::LineItem.create!(variant: variant, quantity: variant_return_items.count, order: order)
+ variant_inventory_units.each { |i| i.update_attributes!(line_item_id: line_item.id, order_id: order.id) }
+ end
+
+ order.reload.update_with_updater!
+ while order.state != order.checkout_steps[-2] && order.next; end
+
+ unless order.payments.present?
+ card_to_reuse = original_order.valid_credit_cards.first
+ card_to_reuse = original_order.user.credit_cards.default.first if !card_to_reuse && original_order.user
+ Spree::Payment.create!(order: order,
+ payment_method_id: card_to_reuse.try(:payment_method_id),
+ source: card_to_reuse,
+ amount: order.total)
+ end
+
+ # the order builds a shipment on its own on transition to delivery, but we want
+ # the original exchange shipment, not the built one
+ order.shipments.destroy_all
+ shipments.each { |shipment| shipment.update_attributes!(order_id: order.id) }
+ order.update_attributes!(state: 'confirm')
+
+ order.reload.next!
+ order.update_with_updater!
+ order.finalize!
+
+ failed_orders << order unless order.completed? && order.valid?
+ rescue StandardError
+ failed_orders << order
+ end
+ end
+ failure_message = failed_orders.map { |o| "#{o.number} - #{o.errors.full_messages}" }.join(', ')
+ raise UnableToChargeForUnreturnedItems, failure_message if failed_orders.present?
+ end
+end
+
+class UnableToChargeForUnreturnedItems < StandardError; end
diff --git a/core/script/rails b/core/script/rails
new file mode 100755
index 00000000000..fa8af90971a
--- /dev/null
+++ b/core/script/rails
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
+
+ENGINE_ROOT = File.expand_path('../..', __FILE__)
+ENGINE_PATH = File.expand_path('../../lib/spree/core/engine', __FILE__)
+
+require 'rails/all'
+require 'rails/engine/commands'
+
diff --git a/core/spec/fixtures/text-file.txt b/core/spec/fixtures/text-file.txt
new file mode 100644
index 00000000000..807e643bd81
--- /dev/null
+++ b/core/spec/fixtures/text-file.txt
@@ -0,0 +1 @@
+Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
diff --git a/core/spec/fixtures/thinking-cat.jpg b/core/spec/fixtures/thinking-cat.jpg
new file mode 100644
index 00000000000..7e8524d367b
Binary files /dev/null and b/core/spec/fixtures/thinking-cat.jpg differ
diff --git a/core/spec/helpers/base_helper_spec.rb b/core/spec/helpers/base_helper_spec.rb
new file mode 100644
index 00000000000..f901657fa71
--- /dev/null
+++ b/core/spec/helpers/base_helper_spec.rb
@@ -0,0 +1,194 @@
+require 'spec_helper'
+
+describe Spree::BaseHelper, type: :helper do
+ include Spree::BaseHelper
+
+ let(:current_store) { create :store }
+
+ context 'available_countries' do
+ let(:country) { create(:country) }
+
+ before do
+ create_list(:country, 3)
+ end
+
+ context 'with no checkout zone defined' do
+ before do
+ Spree::Config[:checkout_zone] = nil
+ end
+
+ it 'return complete list of countries' do
+ expect(available_countries.count).to eq(Spree::Country.count)
+ end
+ end
+
+ context 'with a checkout zone defined' do
+ context 'checkout zone is of type country' do
+ before do
+ @country_zone = create(:zone, name: 'CountryZone')
+ @country_zone.members.create(zoneable: country)
+ Spree::Config[:checkout_zone] = @country_zone.name
+ end
+
+ it 'return only the countries defined by the checkout zone' do
+ expect(available_countries).to eq([country])
+ end
+ end
+
+ context 'checkout zone is of type state' do
+ before do
+ state_zone = create(:zone, name: 'StateZone')
+ state = create(:state, country: country)
+ state_zone.members.create(zoneable: state)
+ Spree::Config[:checkout_zone] = state_zone.name
+ end
+
+ it 'return complete list of countries' do
+ expect(available_countries.count).to eq(Spree::Country.count)
+ end
+ end
+ end
+ end
+
+ # Regression test for #1436
+ context 'defining custom image helpers' do
+ let(:product) { mock_model(Spree::Product, images: [], variant_images: []) }
+
+ before do
+ Spree::Image.class_eval do
+ styles[:very_strange] = '1x1'
+ styles.merge!(foobar: '2x2')
+ end
+ end
+
+ it 'does not raise errors when style exists' do
+ expect { very_strange_image(product) }.not_to raise_error
+ end
+
+ it 'raises NoMethodError when style is not exists' do
+ expect { another_strange_image(product) }.to raise_error(NoMethodError)
+ end
+
+ it 'does not raise errors when helper method called' do
+ expect { foobar_image(product) }.not_to raise_error
+ end
+
+ it 'raises NoMethodError when statement with name equal to style name called' do
+ expect { foobar(product) }.to raise_error(NoMethodError)
+ end
+ end
+
+ context 'link_to_tracking' do
+ it 'returns tracking link if available' do
+ a = link_to_tracking_html(shipping_method: true, tracking: '123', tracking_url: 'http://g.c/?t=123').css('a')
+
+ expect(a.text).to eq '123'
+ expect(a.attr('href').value).to eq 'http://g.c/?t=123'
+ end
+
+ it 'returns tracking without link if link unavailable' do
+ html = link_to_tracking_html(shipping_method: true, tracking: '123', tracking_url: nil)
+ expect(html.css('span').text).to eq '123'
+ end
+
+ it 'returns nothing when no shipping method' do
+ html = link_to_tracking_html(shipping_method: nil, tracking: '123')
+ expect(html.css('span').text).to eq ''
+ end
+
+ it 'returns nothing when no tracking' do
+ html = link_to_tracking_html(tracking: nil)
+ expect(html.css('span').text).to eq ''
+ end
+
+ def link_to_tracking_html(options = {})
+ node = link_to_tracking(double(:shipment, options))
+ Nokogiri::HTML(node.to_s)
+ end
+ end
+
+ # Regression test for #2396
+ context 'meta_data_tags' do
+ it 'truncates a product description to 160 characters' do
+ # Because the controller_name method returns "test"
+ # controller_name is used by this method to infer what it is supposed
+ # to be generating meta_data_tags for
+ text = FFaker::Lorem.paragraphs(2).join(' ')
+ @test = Spree::Product.new(description: text)
+ tags = Nokogiri::HTML.parse(meta_data_tags)
+ content = tags.css('meta[name=description]').first['content']
+ assert content.length <= 160, 'content length is not truncated to 160 characters'
+ end
+ end
+
+ # Regression test for #5384
+
+ context 'pretty_time' do
+ it 'prints in a format' do
+ expect(pretty_time(Time.new(2012, 5, 6, 13, 33))).to eq 'May 06, 2012 1:33 PM'
+ end
+ end
+
+ describe '#display_price' do
+ let!(:product) { create(:product) }
+ let(:current_currency) { 'USD' }
+ let(:current_price_options) { { tax_zone: current_tax_zone } }
+
+ context 'when there is no current order' do
+ let (:current_tax_zone) { nil }
+
+ it 'returns the price including default vat' do
+ expect(display_price(product)).to eq('$19.99')
+ end
+
+ context 'with a default VAT' do
+ let(:current_tax_zone) { create(:zone_with_country, default_tax: true) }
+ let!(:tax_rate) do
+ create :tax_rate,
+ included_in_price: true,
+ zone: current_tax_zone,
+ tax_category: product.tax_category,
+ amount: 0.2
+ end
+
+ it 'returns the price adding the VAT' do
+ expect(display_price(product)).to eq('$19.99')
+ end
+ end
+ end
+
+ context 'with an order that has a tax zone' do
+ let(:current_tax_zone) { create(:zone_with_country) }
+ let(:current_order) { Spree::Order.new }
+ let(:default_zone) { create(:zone_with_country, default_tax: true) }
+
+ let!(:default_vat) do
+ create :tax_rate,
+ included_in_price: true,
+ zone: default_zone,
+ tax_category: product.tax_category,
+ amount: 0.2
+ end
+
+ context 'that matches no VAT' do
+ it 'returns the price excluding VAT' do
+ expect(display_price(product)).to eq('$16.66')
+ end
+ end
+
+ context 'that matches a VAT' do
+ let!(:other_vat) do
+ create :tax_rate,
+ included_in_price: true,
+ zone: current_tax_zone,
+ tax_category: product.tax_category,
+ amount: 0.4
+ end
+
+ it 'returns the price adding the VAT' do
+ expect(display_price(product)).to eq('$23.32')
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/helpers/products_helper_spec.rb b/core/spec/helpers/products_helper_spec.rb
new file mode 100644
index 00000000000..ca5be45219b
--- /dev/null
+++ b/core/spec/helpers/products_helper_spec.rb
@@ -0,0 +1,297 @@
+require 'spec_helper'
+
+module Spree
+ describe ProductsHelper, type: :helper do
+ include ProductsHelper
+
+ let(:product) { create(:product) }
+ let(:currency) { 'USD' }
+
+ before do
+ allow(helper).to receive(:current_currency) { currency }
+ end
+
+ context '#variant_price_diff' do
+ subject { helper.variant_price(@variant) }
+
+ let(:product_price) { 10 }
+ let(:variant_price) { 10 }
+
+ before do
+ @variant = create(:variant, product: product)
+ product.price = 15
+ @variant.price = 10
+ allow(product).to receive(:amount_in) { product_price }
+ allow(@variant).to receive(:amount_in) { variant_price }
+ end
+
+ context 'when variant is same as master' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when the master has no price' do
+ let(:product_price) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when currency is default' do
+ context 'when variant is more than master' do
+ let(:variant_price) { 15 }
+
+ it { is_expected.to eq('(Add: $5.00)') }
+ # Regression test for #2737
+ it { is_expected.to be_html_safe }
+ end
+
+ context 'when variant is less than master' do
+ let(:product_price) { 15 }
+
+ it { is_expected.to eq('(Subtract: $5.00)') }
+ end
+ end
+
+ context 'when currency is JPY' do
+ let(:variant_price) { 100 }
+ let(:product_price) { 100 }
+ let(:currency) { 'JPY' }
+
+ context 'when variant is more than master' do
+ let(:variant_price) { 150 }
+
+ it { is_expected.to eq('(Add: ¥50)') }
+ end
+
+ context 'when variant is less than master' do
+ let(:product_price) { 150 }
+
+ it { is_expected.to eq('(Subtract: ¥50)') }
+ end
+ end
+ end
+
+ context '#variant_price_full' do
+ before do
+ Spree::Config[:show_variant_full_price] = true
+ @variant1 = create(:variant, product: product)
+ @variant2 = create(:variant, product: product)
+ end
+
+ context 'when currency is default' do
+ it 'returns the variant price if the price is different than master' do
+ product.price = 10
+ @variant1.price = 15
+ @variant2.price = 20
+ expect(helper.variant_price(@variant1)).to eq('$15.00')
+ expect(helper.variant_price(@variant2)).to eq('$20.00')
+ end
+ end
+
+ context 'when currency is JPY' do
+ let(:currency) { 'JPY' }
+
+ before do
+ product.variants.active.each do |variant|
+ variant.prices.each do |price|
+ price.currency = currency
+ price.save!
+ end
+ end
+ end
+
+ it 'returns the variant price if the price is different than master' do
+ product.price = 100
+ @variant1.price = 150
+ expect(helper.variant_price(@variant1)).to eq('¥150')
+ end
+ end
+
+ it 'is nil when all variant prices are equal' do
+ product.price = 10
+ @variant1.default_price.update_column(:amount, 10)
+ @variant2.default_price.update_column(:amount, 10)
+ expect(helper.variant_price(@variant1)).to be_nil
+ expect(helper.variant_price(@variant2)).to be_nil
+ end
+ end
+
+ context '#product_description' do
+ # Regression test for #1607
+ it 'renders a product description without excessive paragraph breaks' do
+ product.description = %Q{
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a ligula leo. Proin eu arcu at ipsum dapibus ullamcorper. Pellentesque egestas orci nec magna condimentum luctus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut ac ante et mauris bibendum ultricies non sed massa. Fusce facilisis dui eget lacus scelerisque eget aliquam urna ultricies. Duis et rhoncus quam. Praesent tellus nisi, ultrices sed iaculis quis, euismod interdum ipsum.
+
+
Lorem ipsum dolor sit amet
+
Lorem ipsum dolor sit amet
+
+ }
+ description = product_description(product)
+ expect(description.strip).to eq(product.description.strip)
+ end
+
+ it 'renders a product description with automatic paragraph breaks' do
+ product.description = %Q{
+THIS IS THE BEST PRODUCT EVER!
+
+"IT CHANGED MY LIFE" - Sue, MD}
+
+ description = product_description(product)
+ expect(description.strip).to eq(%Q{
\nTHIS IS THE BEST PRODUCT EVER!
"IT CHANGED MY LIFE" - Sue, MD})
+ end
+
+ it 'renders a product description without any formatting based on configuration' do
+ initial_description = %Q{
+
hello world
+
+
tihs is completely awesome and it works
+
+
why so many spaces in the code. and why some more formatting afterwards?
+ }
+
+ product.description = initial_description
+
+ Spree::Config[:show_raw_product_description] = true
+ description = product_description(product)
+ expect(description).to eq(initial_description)
+ end
+
+ context 'renders a product description default description incase description is blank' do
+ before { product.description = '' }
+
+ it { expect(product_description(product)).to eq(Spree.t(:product_has_no_description)) }
+ end
+ end
+
+ shared_examples_for 'line item descriptions' do
+ context 'variant has a blank description' do
+ let(:description) { nil }
+
+ it { is_expected.to eq(Spree.t(:product_has_no_description)) }
+ end
+
+ context 'variant has a description' do
+ let(:description) { 'test_desc' }
+
+ it { is_expected.to eq(description) }
+ end
+
+ context 'description has nonbreaking spaces' do
+ let(:description) { 'test desc' }
+
+ it { is_expected.to eq('test desc') }
+ end
+
+ context 'description has line endings' do
+ let(:description) { "test\n\r\ndesc" }
+
+ it { is_expected.to eq('test desc') }
+ end
+ end
+
+ context '#line_item_description_text' do
+ subject { line_item_description_text description }
+
+ it_behaves_like 'line item descriptions'
+ end
+
+ context '#cache_key_for_products' do
+ subject { helper.cache_key_for_products }
+
+ let(:zone) { Spree::Zone.new }
+ let(:price_options) { { tax_zone: zone } }
+
+ before do
+ @products = double('products collection')
+ allow(helper).to receive(:params).and_return(page: 10)
+ allow(helper).to receive(:current_price_options) { price_options }
+ end
+
+ context 'when there is a maximum updated date' do
+ let(:updated_at) { Date.new(2011, 12, 13) }
+
+ before do
+ allow(@products).to receive(:count).and_return(5)
+ allow(@products).to receive(:maximum).with(:updated_at) { updated_at }
+ end
+
+ it { is_expected.to eq('en/USD/spree/zones/new/spree/products/all-10-20111213-5') }
+ end
+
+ context 'when there is no considered maximum updated date' do
+ let(:today) { Date.new(2013, 12, 11) }
+
+ before do
+ allow(@products).to receive(:count).and_return(1_234_567)
+ allow(@products).to receive(:maximum).with(:updated_at).and_return(nil)
+ allow(Date).to receive(:today) { today }
+ end
+
+ it { is_expected.to eq('en/USD/spree/zones/new/spree/products/all-10-20131211-1234567') }
+ end
+ end
+
+ context '#cache_key_for_product' do
+ subject(:cache_key) { helper.cache_key_for_product(product) }
+
+ let(:product) { Spree::Product.new }
+ let(:price_options) { { tax_zone: zone } }
+
+ before do
+ allow(helper).to receive(:current_price_options) { price_options }
+ end
+
+ context 'when there is a current tax zone' do
+ let(:zone) { Spree::Zone.new }
+
+ it 'includes the current_tax_zone' do
+ expect(subject).to eq('en/USD/spree/zones/new/spree/products/new/')
+ end
+ end
+
+ context 'when there is no current tax zone' do
+ let(:zone) { nil }
+
+ it { is_expected.to eq('en/USD/spree/products/new/') }
+ end
+
+ context 'when current_price_options includes nil values' do
+ let(:price_options) do
+ {
+ a: nil,
+ b: Spree::Zone.new
+ }
+ end
+
+ it 'does not include nil values' do
+ expect(cache_key).to eq('en/USD/spree/zones/new/spree/products/new/')
+ end
+ end
+
+ context 'when current_price_options includes values that do not implement cache_key' do
+ let(:price_options) do
+ {
+ a: true,
+ b: Spree::Zone.new
+ }
+ end
+
+ it 'includes string representations of these values' do
+ expect(cache_key).to eq('en/USD/true/spree/zones/new/spree/products/new/')
+ end
+ end
+
+ context 'when keys in the options hash are inserted in non-alphabetical order' do
+ let(:price_options) do
+ {
+ b: Spree::Zone.new,
+ a: true
+ }
+ end
+
+ it 'the values are nevertheless returned in alphabetical order of their keys' do
+ expect(cache_key).to eq('en/USD/true/spree/zones/new/spree/products/new/')
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/calculated_adjustments_spec.rb b/core/spec/lib/calculated_adjustments_spec.rb
new file mode 100644
index 00000000000..2cbc8e2b2d1
--- /dev/null
+++ b/core/spec/lib/calculated_adjustments_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+
+describe Spree::CalculatedAdjustments do
+ it 'adds has_one :calculator relationship' do
+ assert Spree::ShippingMethod.reflect_on_all_associations(:has_one).map(&:name).include?(:calculator)
+ end
+end
diff --git a/core/spec/lib/i18n_spec.rb b/core/spec/lib/i18n_spec.rb
new file mode 100644
index 00000000000..59675a9fe9c
--- /dev/null
+++ b/core/spec/lib/i18n_spec.rb
@@ -0,0 +1,121 @@
+require 'rspec/expectations'
+require 'spree/i18n'
+require 'spree/testing_support/i18n'
+
+describe 'i18n' do
+ before do
+ I18n.backend.store_translations(:en,
+ spree: {
+ foo: 'bar',
+ bar: {
+ foo: 'bar within bar scope',
+ invalid: nil,
+ legacy_translation: 'back in the day...'
+ },
+ invalid: nil,
+ legacy_translation: 'back in the day...'
+ })
+ end
+
+ it 'translates within the spree scope' do
+ expect(Spree.normal_t(:foo)).to eql('bar')
+ expect(Spree.translate(:foo)).to eql('bar')
+ end
+
+ it 'translates within the spree scope using a path' do
+ allow(Spree).to receive(:virtual_path).and_return('bar')
+
+ expect(Spree.normal_t('.legacy_translation')).to eql('back in the day...')
+ expect(Spree.translate('.legacy_translation')).to eql('back in the day...')
+ end
+
+ it 'raise error without any context when using a path' do
+ expect do
+ Spree.normal_t('.legacy_translation')
+ end.to raise_error(StandardError)
+
+ expect do
+ Spree.translate('.legacy_translation')
+ end.to raise_error(StandardError)
+ end
+
+ it 'prepends a string scope' do
+ expect(Spree.normal_t(:foo, scope: 'bar')).to eql('bar within bar scope')
+ end
+
+ it 'prepends to an array scope' do
+ expect(Spree.normal_t(:foo, scope: ['bar'])).to eql('bar within bar scope')
+ end
+
+ it 'returns two translations' do
+ expect(Spree.normal_t([:foo, 'bar.foo'])).to eql(['bar', 'bar within bar scope'])
+ end
+
+ it 'returns reasonable string for missing translations' do
+ expect(Spree.t(:missing_entry)).to include(' ['Under $10.00'] } }
+ searcher = described_class.new(ActionController::Parameters.new(params))
+ expect(searcher.send(:get_base_scope).to_sql).to match(/<= 10/)
+ expect(searcher.retrieve_products.count).to eq(1)
+ end
+
+ it 'maps multiple price_range_any filters' do
+ params = { per_page: '', search: { 'price_range_any' => ['Under $10.00', '$10.00 - $15.00'] } }
+ searcher = described_class.new(ActionController::Parameters.new(params))
+ expect(searcher.send(:get_base_scope).to_sql).to match(/<= 10/)
+ expect(searcher.send(:get_base_scope).to_sql).to match(/between 10 and 15/i)
+ expect(searcher.retrieve_products.count).to eq(2)
+ end
+
+ it 'uses ransack if scope not found' do
+ params = { per_page: '', search: { 'name_not_cont' => 'Shirt' } }
+ searcher = described_class.new(ActionController::Parameters.new(params))
+ expect(searcher.retrieve_products.count).to eq(1)
+ end
+
+ it 'accepts a current user' do
+ user = double
+ searcher = described_class.new({})
+ searcher.current_user = user
+ expect(searcher.current_user).to eql(user)
+ end
+
+ it 'finds products in alternate currencies' do
+ create(:price, currency: 'EUR', variant: @product1.master)
+ searcher = described_class.new({})
+ searcher.current_currency = 'EUR'
+ expect(searcher.retrieve_products).to eq([@product1])
+ end
+end
diff --git a/core/spec/lib/spree/core/controller_helpers/auth_spec.rb b/core/spec/lib/spree/core/controller_helpers/auth_spec.rb
new file mode 100644
index 00000000000..6db87bf5884
--- /dev/null
+++ b/core/spec/lib/spree/core/controller_helpers/auth_spec.rb
@@ -0,0 +1,116 @@
+require 'spec_helper'
+require 'spree/testing_support/url_helpers'
+
+class FakesController < ApplicationController
+ include Spree::Core::ControllerHelpers::Auth
+ def index
+ render plain: 'index'
+ end
+end
+
+describe Spree::Core::ControllerHelpers::Auth, type: :controller do
+ controller(FakesController) {}
+ include Spree::TestingSupport::UrlHelpers
+
+ describe '#current_ability' do
+ it 'returns Spree::Ability instance' do
+ expect(controller.current_ability.class).to eq Spree::Ability
+ end
+ end
+
+ describe '#redirect_back_or_default' do
+ controller(FakesController) do
+ def index
+ redirect_back_or_default('/')
+ end
+ end
+ it 'redirects to session url' do
+ session[:spree_user_return_to] = '/redirect'
+ get :index
+ expect(response).to redirect_to('/redirect')
+ end
+ it 'redirects to HTTP_REFERER' do
+ request.env['HTTP_REFERER'] = '/dummy_redirect'
+ get :index
+ expect(response).to redirect_to('/dummy_redirect')
+ end
+ it 'redirects to default page' do
+ get :index
+ expect(response).to redirect_to('/')
+ end
+ end
+
+ describe '#set_token' do
+ controller(FakesController) do
+ def index
+ set_token
+ render plain: 'index'
+ end
+ end
+ it 'sends cookie header' do
+ get :index
+ expect(response.cookies['token']).not_to be_nil
+ end
+ it 'sets httponly flag' do
+ get :index
+ expect(response['Set-Cookie']).to include('HttpOnly')
+ end
+ end
+
+ describe '#store_location' do
+ it 'sets session return url' do
+ allow(controller).to receive_messages(request: double(fullpath: '/redirect'))
+ controller.store_location
+ expect(session[:spree_user_return_to]).to eq '/redirect'
+ end
+ end
+
+ describe '#try_spree_current_user' do
+ it 'calls spree_current_user when define spree_current_user method' do
+ expect(controller).to receive(:spree_current_user)
+ controller.try_spree_current_user
+ end
+ it 'calls current_spree_user when define current_spree_user method' do
+ expect(controller).to receive(:current_spree_user)
+ controller.try_spree_current_user
+ end
+ it 'returns nil' do
+ expect(controller.try_spree_current_user).to eq nil
+ end
+ end
+
+ describe '#redirect_unauthorized_access' do
+ controller(FakesController) do
+ def index
+ redirect_unauthorized_access
+ end
+ end
+ context 'when logged in' do
+ before do
+ allow(controller).to receive_messages(try_spree_current_user: double('User', id: 1, last_incomplete_spree_order: nil))
+ end
+
+ it 'redirects forbidden path' do
+ get :index
+ expect(response).to redirect_to(spree.forbidden_path)
+ end
+ end
+
+ context 'when guest user' do
+ before do
+ allow(controller).to receive_messages(try_spree_current_user: nil)
+ end
+
+ it 'redirects login path' do
+ allow(controller).to receive_messages(spree_login_path: '/login')
+ get :index
+ expect(response).to redirect_to('/login')
+ end
+ it 'redirects root path' do
+ allow(controller).to receive_message_chain(:spree, :root_path).and_return('/root_path')
+ get :index
+ expect(response).to redirect_to('/root_path')
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core/controller_helpers/order_spec.rb b/core/spec/lib/spree/core/controller_helpers/order_spec.rb
new file mode 100644
index 00000000000..12419878e1e
--- /dev/null
+++ b/core/spec/lib/spree/core/controller_helpers/order_spec.rb
@@ -0,0 +1,118 @@
+require 'spec_helper'
+
+class FakesController < ApplicationController
+ include Spree::Core::ControllerHelpers::Order
+end
+
+describe Spree::Core::ControllerHelpers::Order, type: :controller do
+ controller(FakesController) {}
+
+ let(:user) { create(:user) }
+ let(:order) { create(:order, user: user) }
+ let(:store) { create(:store) }
+
+ describe '#simple_current_order' do
+ before { allow(controller).to receive_messages(try_spree_current_user: user) }
+
+ it 'returns an empty order' do
+ expect(controller.simple_current_order.item_count).to eq 0
+ end
+ it 'returns Spree::Order instance' do
+ allow(controller).to receive_messages(cookies: double(signed: { token: order.token }))
+ expect(controller.simple_current_order).to eq order
+ end
+ end
+
+ describe '#current_order' do
+ before do
+ allow(controller).to receive_messages(current_store: store)
+ allow(controller).to receive_messages(try_spree_current_user: user)
+ end
+
+ context 'create_order_if_necessary option is false' do
+ let!(:order) { create :order, user: user, store: store }
+
+ it 'returns current order' do
+ expect(controller.current_order).to eq order
+ end
+ end
+
+ context 'create_order_if_necessary option is true' do
+ it 'creates new order' do
+ expect do
+ controller.current_order(create_order_if_necessary: true)
+ end.to change(Spree::Order, :count).to(1)
+ end
+
+ it 'assigns the current_store id' do
+ controller.current_order(create_order_if_necessary: true)
+ expect(Spree::Order.last.store_id).to eq store.id
+ end
+ end
+
+ context 'gets using the token' do
+ let!(:order) { create :order, user: user }
+ let!(:guest_order) { create :order, user: nil, email: nil, token: 'token' }
+
+ before do
+ expect(controller).to receive(:current_order_params).and_return(
+ currency: Spree::Config[:currency], token: 'token', store_id: guest_order.store_id, user_id: user.id
+ )
+ end
+
+ specify 'without the guest token being bound to any user yet' do
+ expect(controller.current_order).to eq guest_order
+ end
+ end
+ end
+
+ describe '#associate_user' do
+ before do
+ allow(controller).to receive_messages(current_order: order, try_spree_current_user: user)
+ end
+
+ context "user's email is blank" do
+ let(:user) { create(:user, email: '') }
+
+ it 'calls Spree::Order#associate_user! method' do
+ expect_any_instance_of(Spree::Order).to receive(:associate_user!)
+ controller.associate_user
+ end
+ end
+
+ context "user isn't blank" do
+ it 'does not calls Spree::Order#associate_user! method' do
+ expect_any_instance_of(Spree::Order).not_to receive(:associate_user!)
+ controller.associate_user
+ end
+ end
+ end
+
+ describe '#set_current_order' do
+ let(:incomplete_order) { create(:order, user: user) }
+
+ before { allow(controller).to receive_messages(try_spree_current_user: user) }
+
+ context 'when current order not equal to users incomplete orders' do
+ before { allow(controller).to receive_messages(current_order: order, last_incomplete_order: incomplete_order, cookies: double(signed: { token: 'token' })) }
+
+ it 'calls Spree::Order#merge! method' do
+ expect(order).to receive(:merge!).with(incomplete_order, user)
+ controller.set_current_order
+ end
+ end
+ end
+
+ describe '#current_currency' do
+ it 'returns current currency' do
+ Spree::Config[:currency] = 'USD'
+ expect(controller.current_currency).to eq 'USD'
+ end
+ end
+
+ describe '#ip_address' do
+ it 'returns remote ip' do
+ expect(controller.ip_address).to eq request.remote_ip
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core/controller_helpers/search_spec.rb b/core/spec/lib/spree/core/controller_helpers/search_spec.rb
new file mode 100644
index 00000000000..89747a4c6ff
--- /dev/null
+++ b/core/spec/lib/spree/core/controller_helpers/search_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+class FakesController < ApplicationController
+ include Spree::Core::ControllerHelpers::Search
+end
+
+describe Spree::Core::ControllerHelpers::Search, type: :controller do
+ controller(FakesController) {}
+
+ describe '#build_searcher' do
+ it 'returns Spree::Core::Search::Base instance' do
+ allow(controller).to receive_messages(try_spree_current_user: create(:user),
+ current_currency: 'USD')
+ expect(controller.build_searcher({}).class).to eq Spree::Core::Search::Base
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core/controller_helpers/store_spec.rb b/core/spec/lib/spree/core/controller_helpers/store_spec.rb
new file mode 100644
index 00000000000..41fee44a165
--- /dev/null
+++ b/core/spec/lib/spree/core/controller_helpers/store_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+class FakesController < ApplicationController
+ include Spree::Core::ControllerHelpers::Auth
+ include Spree::Core::ControllerHelpers::Order
+ include Spree::Core::ControllerHelpers::Store
+end
+
+describe Spree::Core::ControllerHelpers::Store, type: :controller do
+ controller(FakesController) {}
+
+ describe '#current_store' do
+ let!(:store) { create :store, default: true }
+
+ it 'returns current store' do
+ expect(controller.current_store).to eq store
+ end
+ end
+
+ describe '#current_price_options' do
+ subject(:current_price_options) { controller.current_price_options }
+
+ context 'when there is a default tax zone' do
+ let(:default_zone) { Spree::Zone.new }
+
+ before do
+ allow(Spree::Zone).to receive(:default_tax).and_return(default_zone)
+ end
+
+ context 'when there is no current order' do
+ it 'returns the default tax zone' do
+ expect(subject).to include(tax_zone: default_zone)
+ end
+ end
+
+ context 'when there is a current order' do
+ let(:other_zone) { Spree::Zone.new }
+ let(:current_order) { Spree::Order.new }
+
+ before do
+ allow(current_order).to receive(:tax_zone).and_return(other_zone)
+ allow(controller).to receive(:current_order).and_return(current_order)
+ end
+
+ it { is_expected.to include(tax_zone: other_zone) }
+ end
+ end
+
+ context 'when there is no default tax zone' do
+ before do
+ allow(Spree::Zone).to receive(:default_tax).and_return(nil)
+ end
+
+ context 'when there is no current order' do
+ it 'return nil when asked for the current tax zone' do
+ expect(current_price_options[:tax_zone]).to be_nil
+ end
+ end
+
+ context 'when there is a current order' do
+ let(:other_zone) { Spree::Zone.new }
+ let(:current_order) { Spree::Order.new }
+
+ before do
+ allow(current_order).to receive(:tax_zone).and_return(other_zone)
+ allow(controller).to receive(:current_order).and_return(current_order)
+ end
+
+ it { is_expected.to include(tax_zone: other_zone) }
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core/controller_helpers/strong_parameters_spec.rb b/core/spec/lib/spree/core/controller_helpers/strong_parameters_spec.rb
new file mode 100644
index 00000000000..f72f5fe733f
--- /dev/null
+++ b/core/spec/lib/spree/core/controller_helpers/strong_parameters_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+class FakesController < ApplicationController
+ include Spree::Core::ControllerHelpers::StrongParameters
+end
+
+describe Spree::Core::ControllerHelpers::StrongParameters, type: :controller do
+ controller(FakesController) {}
+
+ describe '#permitted_attributes' do
+ it 'returns Spree::PermittedAttributes module' do
+ expect(controller.permitted_attributes).to eq Spree::PermittedAttributes
+ end
+ end
+
+ describe '#permitted_payment_attributes' do
+ it 'returns Array class' do
+ expect(controller.permitted_payment_attributes.class).to eq Array
+ end
+ end
+
+ describe '#permitted_checkout_attributes' do
+ it 'returns Array class' do
+ expect(controller.permitted_checkout_attributes.class).to eq Array
+ end
+ end
+
+ describe '#permitted_order_attributes' do
+ it 'returns Array class' do
+ expect(controller.permitted_order_attributes.class).to eq Array
+ end
+ end
+
+ describe '#permitted_product_attributes' do
+ it 'returns Array class' do
+ expect(controller.permitted_product_attributes.class).to eq Array
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core/importer/order_spec.rb b/core/spec/lib/spree/core/importer/order_spec.rb
new file mode 100644
index 00000000000..a574737e762
--- /dev/null
+++ b/core/spec/lib/spree/core/importer/order_spec.rb
@@ -0,0 +1,608 @@
+require 'spec_helper'
+
+module Spree
+ module Core
+ describe Importer::Order do
+ let!(:country) { create(:country) }
+ let!(:state) { country.states.first || create(:state, country: country) }
+ let!(:stock_location) { create(:stock_location, admin_name: 'Admin Name') }
+
+ let(:user) { stub_model(LegacyUser, email: 'fox@mudler.com') }
+ let(:shipping_method) { create(:shipping_method) }
+ let(:payment_method) { create(:check_payment_method) }
+
+ let(:product) do
+ product = Spree::Product.create(name: 'Test',
+ sku: 'TEST-1',
+ price: 33.22, available_on: Time.current - 1.day)
+ product.shipping_category = create(:shipping_category)
+ product.save
+ product
+ end
+
+ let(:variant) do
+ variant = product.master
+ variant.stock_items.each { |si| si.update_attribute(:count_on_hand, 10) }
+ variant
+ end
+
+ let(:sku) { variant.sku }
+ let(:variant_id) { variant.id }
+
+ let(:line_items) { [{ variant_id: variant.id, quantity: 5 }] }
+ let(:ship_address) do
+ {
+ address1: '123 Testable Way',
+ firstname: 'Fox',
+ lastname: 'Mulder',
+ city: 'Washington',
+ country_id: country.id,
+ state_id: state.id,
+ zipcode: '66666',
+ phone: '666-666-6666'
+ }
+ end
+
+ it 'can import an order number' do
+ params = { number: '123-456-789' }
+ order = Importer::Order.import(user, params)
+ expect(order.number).to eq '123-456-789'
+ end
+
+ it 'optionally add completed at' do
+ params = {
+ email: 'test@test.com',
+ completed_at: Time.current,
+ line_items_attributes: line_items
+ }
+
+ order = Importer::Order.import(user, params)
+ expect(order).to be_completed
+ expect(order.state).to eq 'complete'
+ end
+
+ it 'assigns order[email] over user email to order' do
+ params = { email: 'wooowww@test.com' }
+ order = Importer::Order.import(user, params)
+ expect(order.email).to eq params[:email]
+ end
+
+ context 'assigning a user to an order' do
+ let(:other_user) { stub_model(LegacyUser, email: 'dana@scully.com') }
+
+ context 'as an admin' do
+ before { allow(user).to receive_messages has_spree_role?: true }
+
+ context "a user's id is not provided" do
+ # this is a regression spec for an issue we ran into at Bonobos
+ it "doesn't unassociate the admin from the order" do
+ params = {}
+ order = Importer::Order.import(user, params)
+ expect(order.user_id).to eq(user.id)
+ end
+ end
+ end
+
+ context 'as a user' do
+ before { allow(user).to receive_messages has_spree_role?: false }
+
+ it 'does not assign the order to the other user' do
+ params = { user_id: other_user.id }
+ order = Importer::Order.import(user, params)
+ expect(order.user_id).to eq(user.id)
+ end
+ end
+ end
+
+ it 'can build an order from API with just line items' do
+ params = { line_items_attributes: line_items }
+
+ expect(Importer::Order).to receive(:ensure_variant_id_from_params).and_return(variant_id: variant.id,
+ quantity: 5)
+ order = Importer::Order.import(user, params)
+ expect(order.user).to eq(nil)
+ line_item = order.line_items.first
+ expect(line_item.quantity).to eq(5)
+ expect(line_item.variant_id).to eq(variant_id)
+ end
+
+ it 'handles line_item building exceptions' do
+ line_items.first[:variant_id] = 'XXX'
+ params = { line_items_attributes: line_items }
+
+ expect { Importer::Order.import(user, params) }.to raise_error(/XXX/)
+ end
+
+ it 'handles line_item updating exceptions' do
+ line_items.first[:currency] = 'GBP'
+ params = { line_items_attributes: line_items }
+
+ expect { Importer::Order.import(user, params) }.to raise_error(/Validation failed/)
+ end
+
+ it 'can build an order from API with variant sku' do
+ params = { line_items_attributes: [{ sku: sku, quantity: 5 }] }
+
+ order = Importer::Order.import(user, params)
+
+ line_item = order.line_items.first
+ expect(line_item.variant_id).to eq(variant_id)
+ expect(line_item.quantity).to eq(5)
+ end
+
+ it 'handles exceptions when sku is not found' do
+ params = { line_items_attributes: [{ sku: 'XXX', quantity: 5 }] }
+ expect { Importer::Order.import(user, params) }.to raise_error(/XXX/)
+ end
+
+ it 'can build an order from API shipping address' do
+ params = {
+ ship_address_attributes: ship_address,
+ line_items_attributes: line_items
+ }
+
+ order = Importer::Order.import(user, params)
+ expect(order.ship_address.address1).to eq '123 Testable Way'
+ end
+
+ it 'can build an order from API with country attributes' do
+ ship_address.delete(:country_id)
+ ship_address[:country] = { 'iso' => 'US' }
+ params = {
+ ship_address_attributes: ship_address,
+ line_items_attributes: line_items
+ }
+
+ order = Importer::Order.import(user, params)
+ expect(order.ship_address.country.iso).to eq 'US'
+ end
+
+ it 'handles country lookup exceptions' do
+ ship_address.delete(:country_id)
+ ship_address[:country] = { 'iso' => 'XXX' }
+ params = {
+ ship_address_attributes: ship_address,
+ line_items_attributes: line_items
+ }
+
+ expect { Importer::Order.import(user, params) }.to raise_error(/XXX/)
+ end
+
+ it 'can build an order from API with state attributes' do
+ ship_address.delete(:state_id)
+ ship_address[:state] = { 'name' => state.name }
+ params = {
+ ship_address_attributes: ship_address,
+ line_items_attributes: line_items
+ }
+
+ order = Importer::Order.import(user, params)
+ expect(order.ship_address.state.name).to eq state.name
+ end
+
+ context 'with a different currency' do
+ before { variant.price_in('GBP').update_attribute(:price, 18.99) }
+
+ it 'sets the order currency' do
+ params = { currency: 'GBP' }
+ order = Importer::Order.import(user, params)
+ expect(order.currency).to eq 'GBP'
+ end
+
+ it 'can handle it when a line order price is specified' do
+ params = {
+ currency: 'GBP',
+ line_items_attributes: line_items
+ }
+ line_items.first.merge! currency: 'GBP', price: 1.99
+ order = Importer::Order.import(user, params)
+ expect(order.currency).to eq 'GBP'
+ expect(order.line_items.first.price).to eq 1.99
+ expect(order.line_items.first.currency).to eq 'GBP'
+ end
+ end
+
+ context 'state passed is not associated with country' do
+ let(:params) do
+ {
+ ship_address_attributes: ship_address,
+ line_items_attributes: line_items
+ }
+ end
+
+ let(:other_state) { create(:state, name: 'Uhuhuh', country: create(:country)) }
+
+ before do
+ ship_address.delete(:state_id)
+ ship_address[:state] = { 'name' => other_state.name }
+ end
+
+ it 'sets states name instead of state id' do
+ order = Importer::Order.import(user, params)
+ expect(order.ship_address.state_name).to eq other_state.name
+ end
+ end
+
+ it 'sets state name if state record not found' do
+ ship_address.delete(:state_id)
+ ship_address[:state] = { 'name' => 'XXX' }
+ params = {
+ ship_address_attributes: ship_address,
+ line_items_attributes: line_items
+ }
+
+ order = Importer::Order.import(user, params)
+ expect(order.ship_address.state_name).to eq 'XXX'
+ end
+
+ context 'variant not deleted' do
+ it 'ensures variant id from api' do
+ hash = { sku: variant.sku }
+ Importer::Order.ensure_variant_id_from_params(hash)
+ expect(hash[:variant_id]).to eq variant.id
+ end
+ end
+
+ context 'variant was deleted' do
+ it 'raise error as variant shouldnt be found' do
+ variant.product.destroy
+ hash = { sku: variant.sku }
+ expect { Importer::Order.ensure_variant_id_from_params(hash) }.to raise_error("Ensure order import variant: Variant w/SKU #{hash[:sku]} not found.")
+ end
+ end
+
+ it 'ensures_country_id for country fields' do
+ [:name, :iso, :iso_name, :iso3].each do |field|
+ address = { country: { field => country.send(field) } }
+ Importer::Order.ensure_country_id_from_params(address)
+ expect(address[:country_id]).to eq country.id
+ end
+ end
+
+ it 'raises with proper message when cant find country' do
+ address = { country: { 'name' => 'NoNoCountry' } }
+ expect { Importer::Order.ensure_country_id_from_params(address) }.to raise_error(/NoNoCountry/)
+ end
+
+ it 'ensures_state_id for state fields' do
+ [:name, :abbr].each do |field|
+ address = { country_id: country.id, state: { field => state.send(field) } }
+ Importer::Order.ensure_state_id_from_params(address)
+ expect(address[:state_id]).to eq state.id
+ end
+ end
+
+ context 'shipments' do
+ let(:params) do
+ {
+ line_items_attributes: line_items,
+ shipments_attributes: [
+ {
+ tracking: '123456789',
+ cost: '14.99',
+ shipping_method: shipping_method.name,
+ stock_location: stock_location.name,
+ inventory_units: Array.new(3) { { sku: sku, variant_id: variant.id } }
+ },
+ {
+ tracking: '123456789',
+ cost: '14.99',
+ shipping_method: shipping_method.name,
+ stock_location: stock_location.name,
+ inventory_units: Array.new(2) { { sku: sku, variant_id: variant.id } }
+ }
+ ]
+ }
+ end
+
+ it 'ensures variant exists and is not deleted' do
+ expect(Importer::Order).to receive(:ensure_variant_id_from_params).exactly(6).times { line_items.first }
+ Importer::Order.import(user, params)
+ end
+
+ it 'builds them properly' do
+ order = Importer::Order.import(user, params)
+ shipment = order.shipments.first
+
+ expect(shipment.cost.to_f).to eq 14.99
+ expect(shipment.inventory_units.first.variant_id).to eq product.master.id
+ expect(shipment.tracking).to eq '123456789'
+ expect(shipment.shipping_rates.first.cost).to eq 14.99
+ expect(shipment.selected_shipping_rate).to eq(shipment.shipping_rates.first)
+ expect(shipment.stock_location).to eq stock_location
+ expect(order.shipment_total.to_f).to eq 29.98
+ end
+
+ it 'allocates inventory units to the correct shipments' do
+ order = Importer::Order.import(user, params)
+
+ expect(order.inventory_units.count).to eq 2
+ expect(order.shipments.first.inventory_units.count).to eq 1
+ expect(order.shipments.first.inventory_units.first.quantity).to eq 3
+ expect(order.shipments.last.inventory_units.count).to eq 1
+ expect(order.shipments.last.inventory_units.first.quantity).to eq 2
+ end
+
+ it 'accepts admin name for stock location' do
+ params[:shipments_attributes][0][:stock_location] = stock_location.admin_name
+ order = Importer::Order.import(user, params)
+ shipment = order.shipments.first
+
+ expect(shipment.stock_location).to eq stock_location
+ end
+
+ it 'raises if cant find stock location' do
+ params[:shipments_attributes][0][:stock_location] = 'doesnt exist'
+ expect { Importer::Order.import(user, params) }.to raise_error(StandardError)
+ end
+
+ context 'when a shipping adjustment is present' do
+ it 'creates the shipping adjustment' do
+ adjustment_attributes = [{ label: 'Shipping Discount', amount: -5.00 }]
+ params[:shipments_attributes][0][:adjustments_attributes] = adjustment_attributes
+ order = Importer::Order.import(user, params)
+ shipment = order.shipments.first
+
+ expect(shipment.adjustments.first.label).to eq('Shipping Discount')
+ expect(shipment.adjustments.first.amount).to eq(-5.00)
+ end
+ end
+
+ context 'when completed_at and shipped_at present' do
+ let(:params) do
+ {
+ completed_at: 2.days.ago,
+ line_items_attributes: line_items,
+ shipments_attributes: [
+ {
+ tracking: '123456789',
+ cost: '4.99',
+ shipped_at: 1.day.ago,
+ shipping_method: shipping_method.name,
+ stock_location: stock_location.name,
+ inventory_units: [{ sku: sku }]
+ }
+ ]
+ }
+ end
+
+ it 'builds them properly' do
+ order = Importer::Order.import(user, params)
+ shipment = order.shipments.first
+
+ expect(shipment.cost.to_f).to eq 4.99
+ expect(shipment.inventory_units.first.variant_id).to eq product.master.id
+ expect(shipment.tracking).to eq '123456789'
+ expect(shipment.shipped_at).to be_present
+ expect(shipment.shipping_rates.first.cost).to eq 4.99
+ expect(shipment.selected_shipping_rate).to eq(shipment.shipping_rates.first)
+ expect(shipment.stock_location).to eq stock_location
+ expect(shipment.state).to eq('shipped')
+ expect(shipment.inventory_units.all?(&:shipped?)).to be true
+ expect(order.shipment_state).to eq('shipped')
+ expect(order.shipment_total.to_f).to eq 4.99
+ end
+ end
+ end
+
+ it 'handles shipment building exceptions' do
+ params = {
+ shipments_attributes: [
+ {
+ tracking: '123456789',
+ cost: '4.99',
+ shipping_method: 'XXX',
+ inventory_units: [{ sku: sku }]
+ }
+ ]
+ }
+ expect { Importer::Order.import(user, params) }.to raise_error(/XXX/)
+ end
+
+ it 'adds adjustments' do
+ params = {
+ adjustments_attributes: [
+ {
+ label: 'Shipping Discount',
+ amount: -4.99
+ },
+ {
+ label: 'Promotion Discount',
+ amount: -3.00
+ }
+ ]
+ }
+
+ order = Importer::Order.import(user, params)
+ expect(order.adjustments.all?(&:closed?)).to be true
+ expect(order.adjustments.first.label).to eq 'Shipping Discount'
+ expect(order.adjustments.first.amount).to eq(-4.99)
+ end
+
+ it 'adds line item adjustments from promotion' do
+ line_items.first[:adjustments_attributes] = [
+ {
+ label: 'Line Item Discount',
+ amount: -4.99,
+ promotion: true
+ }
+ ]
+ params = {
+ line_items_attributes: line_items,
+ adjustments_attributes: [
+ { label: 'Order Discount', amount: -5.99 }
+ ]
+ }
+
+ order = Importer::Order.import(user, params)
+ line_item_adjustment = order.line_item_adjustments.first
+ expect(line_item_adjustment.closed?).to be true
+ expect(line_item_adjustment.label).to eq 'Line Item Discount'
+ expect(line_item_adjustment.amount).to eq(-4.99)
+ expect(order.line_items.first.adjustment_total).to eq(-4.99)
+ end
+
+ it 'adds line item adjustments from taxation' do
+ line_items.first[:adjustments_attributes] = [
+ { label: 'Line Item Tax', amount: -4.99, tax: true }
+ ]
+ params = {
+ line_items_attributes: line_items,
+ adjustments_attributes: [
+ { label: 'Order Discount', amount: -5.99 }
+ ]
+ }
+
+ order = Importer::Order.import(user, params)
+
+ line_item_adjustment = order.line_item_adjustments.first
+ expect(line_item_adjustment.closed?).to be true
+ expect(line_item_adjustment.label).to eq 'Line Item Tax'
+ expect(line_item_adjustment.amount).to eq(-4.99)
+ expect(order.line_items.first.adjustment_total).to eq(-4.99)
+ end
+
+ it 'calculates final order total correctly' do
+ params = {
+ adjustments_attributes: [
+ { label: 'Promotion Discount', amount: -3.00 }
+ ],
+ line_items_attributes: [
+ {
+ variant_id: variant.id,
+ quantity: 5
+ }
+ ]
+ }
+
+ order = Importer::Order.import(user, params)
+ expect(order.item_total).to eq(166.1)
+ expect(order.total).to eq(163.1) # = item_total (166.1) - adjustment_total (3.00)
+ end
+
+ it 'handles adjustment building exceptions' do
+ params = {
+ adjustments_attributes: [
+ {
+ amount: 'XXX'
+ },
+ {
+ label: 'Promotion Discount',
+ amount: '-3.00'
+ }
+ ]
+ }
+
+ expect { Importer::Order.import(user, params) }.to raise_error(/XXX/)
+ end
+
+ it 'builds a payment using state' do
+ params = {
+ payments_attributes: [
+ {
+ amount: '4.99',
+ payment_method: payment_method.name,
+ state: 'completed'
+ }
+ ]
+ }
+ order = Importer::Order.import(user, params)
+ expect(order.payments.first.amount).to eq 4.99
+ end
+
+ it 'builds a payment using status as fallback' do
+ params = {
+ payments_attributes: [
+ {
+ amount: '4.99',
+ payment_method: payment_method.name,
+ status: 'completed'
+ }
+ ]
+ }
+ order = Importer::Order.import(user, params)
+ expect(order.payments.first.amount).to eq 4.99
+ end
+
+ it 'handles payment building exceptions' do
+ params = {
+ payments_attributes: [
+ {
+ amount: '4.99',
+ payment_method: 'XXX'
+ }
+ ]
+ }
+ expect { Importer::Order.import(user, params) }.to raise_error(/XXX/)
+ end
+
+ it 'build a source payment using years and month' do
+ params = {
+ payments_attributes: [
+ {
+ amount: '4.99',
+ payment_method: payment_method.name,
+ status: 'completed',
+ source: {
+ name: 'Fox',
+ last_digits: '7424',
+ cc_type: 'visa',
+ year: '2022',
+ month: '5'
+ }
+ }
+ ]
+ }
+
+ order = Importer::Order.import(user, params)
+ expect(order.payments.first.source.last_digits).to eq '7424'
+ end
+
+ it 'handles source building exceptions when do not have years and month' do
+ params = {
+ payments_attributes: [
+ {
+ amount: '4.99',
+ payment_method: payment_method.name,
+ status: 'completed',
+ source: {
+ name: 'Fox',
+ last_digits: '7424',
+ cc_type: 'visa'
+ }
+ }
+ ]
+ }
+
+ expect { Importer::Order.import(user, params) }.
+ to raise_error(/Validation failed: Credit card Month is not a number, Credit card Year is not a number/)
+ end
+
+ it 'builds a payment with an optional created_at' do
+ created_at = 2.days.ago
+ params = {
+ payments_attributes: [
+ {
+ amount: '4.99',
+ payment_method: payment_method.name,
+ state: 'completed',
+ created_at: created_at
+ }
+ ]
+ }
+ order = Importer::Order.import(user, params)
+ expect(order.payments.first.created_at).to be_within(1).of created_at
+ end
+
+ context 'raises error' do
+ it 'clears out order from db' do
+ params = { payments_attributes: [{ payment_method: 'XXX' }] }
+ count = Order.count
+
+ expect { Importer::Order.import(user, params) }.to raise_error(StandardError)
+ expect(Order.count).to eq count
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core/number_generator_spec.rb b/core/spec/lib/spree/core/number_generator_spec.rb
new file mode 100644
index 00000000000..37bd07e516e
--- /dev/null
+++ b/core/spec/lib/spree/core/number_generator_spec.rb
@@ -0,0 +1,137 @@
+require 'spec_helper'
+
+describe Spree::Core::NumberGenerator do
+ let(:number_generator) { described_class.new(options) }
+
+ let(:model) do
+ mod = number_generator
+
+ Class.new(ApplicationRecord) do
+ self.table_name = 'spree_orders'
+ include mod
+ end
+ end
+
+ let(:options) { { prefix: 'R' } }
+
+ %i[prefix length].each do |name|
+ describe "##{name}" do
+ let(:value) { double('Generic Value') }
+
+ it 'returns attribute value from options' do
+ expect(described_class.new(options.merge(name => value)).public_send(name)).to be(value)
+ end
+ end
+
+ describe "##{name}=" do
+ let(:value_a) { double('Generic Value A') }
+ let(:value_b) { double('Generic Value B') }
+
+ it 'writes attribute value' do
+ object = described_class.new(options.merge(name => value_a))
+ expect { object.public_send(:"#{name}=", value_b) }.
+ to change { object.public_send(name) }.
+ from(value_a).
+ to(value_b)
+ end
+ end
+ end
+
+ shared_examples_for 'duplicate without length increment' do
+ it 'sets permalink field' do
+ expect { subject }.to change(resource, :number).from(nil).to(String)
+ expect(resource.number).to match(regex)
+ end
+ end
+
+ shared_examples_for 'generating permalink' do
+ let(:resource) { model.new }
+
+ context 'and generated candidate is unique' do
+ before do
+ expect(model).to receive(:exists?).and_return(false)
+ end
+
+ it 'sets permalink field' do
+ expect { subject }.to change(resource, :number).from(nil).to(String)
+ expect(resource.number).to match(regex)
+ end
+ end
+
+ context 'and generated candidate is NOT unique' do
+ before do
+ expect(model).to receive(:exists?).and_return(true).ordered
+ expect(model).to receive(:count).and_return(record_count).ordered
+ expect(model).to receive(:exists?).and_return(false)
+ end
+
+ context 'and less than half of the value space taken' do
+ let(:record_count) { 10**expected_length / 2 - 1 }
+
+ include_examples 'duplicate without length increment'
+ end
+
+ context 'and exactly half of the value space taken' do
+ let(:record_count) { 10**expected_length / 2 }
+
+ include_examples 'duplicate without length increment'
+ end
+
+ context 'and more than half of the value space is taken' do
+ let(:record_count) { 10**expected_length / 2 + 1 }
+
+ it 'sets permalink field' do
+ expect { subject }.to change(resource, :number).from(nil).to(String)
+ expect(resource.number).to match(regex_more_than_half)
+ end
+ end
+ end
+ end
+
+ describe '#included' do
+ context 'generates .number_generator on host' do
+ it 'returns number generator' do
+ expect(model.number_generator).to be(number_generator)
+ end
+ end
+
+ context 'generates validation hooks on host' do
+ subject { resource.valid? }
+
+ let(:expected_length) { 9 }
+ let(:regex) { /R[0-9]{9}$/ }
+ let(:regex_more_than_half) { /R[0-9]{10}$/ }
+
+ context 'when permalink field value is nil' do
+ context 'on defaults' do
+ include_examples 'generating permalink'
+ end
+
+ context 'with length: option' do
+ let(:options) { super().merge(length: 10) }
+ let(:expected_length) { 10 }
+ let(:regex) { /R[0-9]{10}$/ }
+ let(:regex_more_than_half) { /R[0-9]{11}$/ }
+
+ include_examples 'generating permalink'
+ end
+
+ context 'with letters option' do
+ let(:options) { super().merge(letters: true) }
+ let(:regex) { /R[0-9A-Z]{9}$/ }
+ let(:regex_more_than_half) { /R[0-9A-Z]{10}$/ }
+
+ include_examples 'generating permalink'
+ end
+ end
+
+ context 'when permalink field value is present' do
+ let(:resource) { model.new(number: 'Test') }
+
+ it 'does not touch field' do
+ expect { subject }.not_to change(resource, :number)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core/query_filters/comparable_spec.rb b/core/spec/lib/spree/core/query_filters/comparable_spec.rb
new file mode 100644
index 00000000000..79c2e138ab9
--- /dev/null
+++ b/core/spec/lib/spree/core/query_filters/comparable_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+module Spree
+ module Core
+ describe QueryFilters::Comparable do
+ let(:subject) do
+ described_class.new(attribute: Spree::DummyModel.arel_table[:position])
+ end
+
+ let!(:dummy1) { Spree::DummyModel.create(name: '1', position: 3) }
+ let!(:dummy2) { Spree::DummyModel.create(name: '2', position: 10) }
+ let!(:dummy3) { Spree::DummyModel.create(name: '3', position: 4) }
+ let(:scope) { Spree::DummyModel.all }
+
+ context 'with greater than matcher' do
+ let(:filter) do
+ {
+ position: { gt: 3 }
+ }
+ end
+
+ it 'returns correct dummies' do
+ result = subject.call(scope: scope, filter: filter[:position])
+ expect(result).to include(dummy2, dummy3)
+ expect(result).not_to include(dummy1)
+ end
+ end
+
+ context 'with greater than or equal matcher' do
+ let(:filter) do
+ {
+ position: { gteq: 4 }
+ }
+ end
+
+ it 'returns correct dummies' do
+ result = subject.call(scope: scope, filter: filter[:position])
+ expect(result).to include(dummy2, dummy3)
+ expect(result).not_to include(dummy1)
+ end
+ end
+
+ context 'with lower than matcher' do
+ let(:filter) do
+ {
+ position: { lt: 4 }
+ }
+ end
+
+ it 'returns correct dummies' do
+ result = subject.call(scope: scope, filter: filter[:position])
+ expect(result).to include(dummy1)
+ expect(result).not_to include(dummy2, dummy3)
+ end
+ end
+
+ context 'with lower than or equal matcher' do
+ let(:filter) do
+ {
+ position: { lteq: 4 }
+ }
+ end
+
+ it 'returns correct dummies' do
+ result = subject.call(scope: scope, filter: filter[:position])
+ expect(result).to include(dummy1, dummy3)
+ expect(result).not_to include(dummy2)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core/query_filters/text_spec.rb b/core/spec/lib/spree/core/query_filters/text_spec.rb
new file mode 100644
index 00000000000..698f4a0dc35
--- /dev/null
+++ b/core/spec/lib/spree/core/query_filters/text_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+module Spree
+ module Core
+ describe QueryFilters::Text do
+ let(:subject) do
+ described_class.new(attribute: Spree::DummyModel.arel_table[:name])
+ end
+
+ let!(:dummy1) { Spree::DummyModel.create(name: 'TestName', position: 3) }
+ let!(:dummy2) { Spree::DummyModel.create(name: 'Test', position: 10) }
+ let!(:dummy3) { Spree::DummyModel.create(name: 'Something', position: 4) }
+ let(:scope) { Spree::DummyModel.all }
+
+ context 'with eq matcher' do
+ let(:filter) do
+ {
+ position: { eq: 'TestName' }
+ }
+ end
+
+ it 'returns correct dummies' do
+ result = subject.call(scope: scope, filter: filter[:position])
+ expect(result).to include(dummy1)
+ expect(result).not_to include(dummy2, dummy3)
+ end
+ end
+
+ context 'with contains matcher' do
+ let(:filter) do
+ {
+ position: { contains: 'Test' }
+ }
+ end
+
+ it 'returns correct dummies' do
+ result = subject.call(scope: scope, filter: filter[:position])
+ expect(result).to include(dummy1, dummy2)
+ expect(result).not_to include(dummy3)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core/token_generator_spec.rb b/core/spec/lib/spree/core/token_generator_spec.rb
new file mode 100644
index 00000000000..5537fd1e78f
--- /dev/null
+++ b/core/spec/lib/spree/core/token_generator_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Spree::Core::TokenGenerator do
+ class DummyClass
+ include Spree::Core::TokenGenerator
+
+ attr_reader :created_at
+
+ def initialize
+ @created_at = Time.now.to_i
+ end
+ end
+
+ let(:dummy_class_instance) { DummyClass.new }
+
+ describe 'generate_token' do
+ let(:generated_token) { dummy_class_instance.generate_token }
+
+ it 'generates random token with timestamp' do
+ expect(generated_token.size).to eq 35
+ expect(generated_token).to include dummy_class_instance.created_at.to_s
+ end
+ end
+end
diff --git a/core/spec/lib/spree/core_spec.rb b/core/spec/lib/spree/core_spec.rb
new file mode 100644
index 00000000000..643a0f65da4
--- /dev/null
+++ b/core/spec/lib/spree/core_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Spree do
+ describe '.admin_path' do
+ it { expect(described_class.admin_path).to eq(Spree::Config[:admin_path]) }
+ end
+
+ describe '.admin_path=' do
+ let!(:original_admin_path) { described_class.admin_path }
+ let(:new_admin_path) { '/admin-secret-path' }
+
+ before do
+ described_class.admin_path = new_admin_path
+ end
+
+ after do
+ described_class.admin_path = original_admin_path
+ end
+
+ it { expect(described_class.admin_path).to eq(new_admin_path) }
+ it { expect(Spree::Config[:admin_path]).to eq(new_admin_path) }
+ end
+
+ describe '.user_class' do
+ after do
+ described_class.user_class = 'Spree::LegacyUser'
+ end
+
+ context 'when user_class is a Class instance' do
+ it 'raises an error' do
+ described_class.user_class = Spree::LegacyUser
+
+ expect { described_class.user_class }.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'when user_class is a Symbol instance' do
+ it 'returns the user_class constant' do
+ described_class.user_class = :'Spree::LegacyUser'
+
+ expect(described_class.user_class).to eq(Spree::LegacyUser)
+ end
+ end
+
+ context 'when user_class is a String instance' do
+ it 'returns the user_class constant' do
+ described_class.user_class = 'Spree::LegacyUser'
+
+ expect(described_class.user_class).to eq(Spree::LegacyUser)
+ end
+ end
+
+ context 'when constantize is false' do
+ it 'returns the user_class as a String' do
+ described_class.user_class = 'Spree::LegacyUser'
+
+ expect(described_class.user_class(constantize: false)).to eq('Spree::LegacyUser')
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/spree/localized_number_spec.rb b/core/spec/lib/spree/localized_number_spec.rb
new file mode 100644
index 00000000000..584132fbe12
--- /dev/null
+++ b/core/spec/lib/spree/localized_number_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Spree::LocalizedNumber do
+ context '.parse' do
+ before do
+ I18n.enforce_available_locales = false
+ I18n.locale = I18n.default_locale
+ I18n.backend.store_translations(:de, number: { currency: { format: { delimiter: '.', separator: ',' } } })
+ end
+
+ after do
+ I18n.locale = I18n.default_locale
+ I18n.enforce_available_locales = true
+ end
+
+ context 'with decimal point' do
+ it 'captures the proper amount for a formatted price' do
+ expect(subject.class.parse('1,599.99')).to eq 1599.99
+ end
+ end
+
+ context 'with decimal comma' do
+ it 'captures the proper amount for a formatted price' do
+ I18n.locale = :de
+ expect(subject.class.parse('1.599,99')).to eq 1599.99
+ end
+ end
+
+ context 'with a numeric price' do
+ it 'uses the price as is' do
+ I18n.locale = :de
+ expect(subject.class.parse(1599.99)).to eq 1599.99
+ end
+ end
+
+ context 'string argument' do
+ it 'is not modified' do
+ I18n.locale = :de
+ number = '1.599,99'
+ number_bak = number.dup
+ subject.class.parse(number)
+ expect(number).to eql(number_bak)
+ end
+ end
+
+ context 'with empty string' do
+ it 'returns 0' do
+ expect(subject.class.parse('')).to be 0
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/spree/migrations_spec.rb b/core/spec/lib/spree/migrations_spec.rb
new file mode 100644
index 00000000000..9ef268ca0c5
--- /dev/null
+++ b/core/spec/lib/spree/migrations_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+module Spree
+ describe Migrations do
+ subject { described_class.new(config, 'spree') }
+
+ let(:app_migrations) { ['.', '34_add_title.rb', '52_add_text.rb'] }
+ let(:engine_migrations) { ['.', '334_create_orders.spree.rb', '777_create_products.spree.rb'] }
+
+ let(:config) { double('Config', root: 'dir') }
+
+ let(:engine_dir) { 'dir/db/migrate' }
+ let(:app_dir) { "#{Rails.root}/db/migrate" }
+
+ before do
+ expect(File).to receive(:directory?).with(app_dir).and_return true
+ end
+
+ it 'warns about missing migrations' do
+ expect(Dir).to receive(:entries).with(app_dir).and_return app_migrations
+ expect(Dir).to receive(:entries).with(engine_dir).and_return engine_migrations
+
+ silence_stream(STDOUT) do
+ expect(subject.check).to eq true
+ end
+ end
+
+ context 'no missing migrations' do
+ it 'says nothing' do
+ expect(Dir).to receive(:entries).with(engine_dir).and_return engine_migrations
+ expect(Dir).to receive(:entries).with(app_dir).and_return(app_migrations + engine_migrations)
+ expect(subject.check).to eq nil
+ end
+ end
+ end
+end
diff --git a/core/spec/lib/spree/money_spec.rb b/core/spec/lib/spree/money_spec.rb
new file mode 100644
index 00000000000..2104595cd88
--- /dev/null
+++ b/core/spec/lib/spree/money_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+
+describe Spree::Money do
+ before do
+ configure_spree_preferences do |config|
+ config.currency = 'USD'
+ end
+ end
+
+ let(:money) { described_class.new(10) }
+ let(:currency) { Money::Currency.new('USD') }
+
+ it 'formats correctly' do
+ expect(money.to_s).to eq('$10.00')
+ end
+
+ it 'can get cents' do
+ expect(money.cents).to eq(1000)
+ end
+
+ it 'can get currency' do
+ expect(money.currency).to eq(currency)
+ end
+
+ context 'with currency' do
+ it 'passed in option' do
+ money = described_class.new(10, with_currency: true, html_wrap: false)
+ expect(money.to_s).to eq('$10.00 USD')
+ end
+ end
+
+ context 'hide cents' do
+ it 'hides cents suffix' do
+ money = described_class.new(10, no_cents: true)
+ expect(money.to_s).to eq('$10')
+ end
+
+ it 'shows cents suffix' do
+ money = described_class.new(10)
+ expect(money.to_s).to eq('$10.00')
+ end
+ end
+
+ context 'currency parameter' do
+ context 'when currency is specified in Canadian Dollars' do
+ it 'uses the currency param over the global configuration' do
+ money = described_class.new(10, currency: 'CAD', with_currency: true, html_wrap: false)
+ expect(money.to_s).to eq('$10.00 CAD')
+ end
+ end
+
+ context 'when currency is specified in Japanese Yen' do
+ it 'uses the currency param over the global configuration' do
+ money = described_class.new(100, currency: 'JPY', html_wrap: false)
+ expect(money.to_s).to eq('Â¥100')
+ end
+ end
+ end
+
+ context 'format' do
+ it 'passed in option' do
+ money = described_class.new(10, format: '%n %u', html_wrap: false)
+ expect(money.to_s).to eq('10.00 $')
+ end
+ end
+
+ context 'sign before symbol' do
+ it 'defaults to -$10.00' do
+ money = described_class.new(-10)
+ expect(money.to_s).to eq('-$10.00')
+ end
+
+ it 'passed in option' do
+ money = described_class.new(-10, sign_before_symbol: false)
+ expect(money.to_s).to eq('$-10.00')
+ end
+ end
+
+ context 'JPY' do
+ before do
+ configure_spree_preferences do |config|
+ config.currency = 'JPY'
+ end
+ end
+
+ it 'formats correctly' do
+ money = described_class.new(1000, html_wrap: false)
+ expect(money.to_s).to eq('Â¥1,000')
+ end
+ end
+
+ context 'EUR' do
+ before do
+ configure_spree_preferences do |config|
+ config.currency = 'EUR'
+ end
+ end
+
+ # Regression test for #2634
+ it 'formats as plain by default' do
+ money = described_class.new(10, format: '%n %u')
+ expect(money.to_s).to eq('10.00 €')
+ end
+
+ # rubocop:disable Style/AsciiComments
+ it 'formats as HTML if asked (nicely) to' do
+ money = described_class.new(10, format: '%n %u')
+ # The HTML'ified version of "10.00 €"
+ expect(money.to_html).to eq('10.00 €')
+ end
+
+ it 'formats as HTML with currency' do
+ money = described_class.new(10, format: '%n %u', with_currency: true)
+ # The HTML'ified version of "10.00 €"
+ expect(money.to_html).to eq('10.00 € EUR')
+ end
+ # rubocop:enable Style/AsciiComments
+ end
+
+ context 'Money formatting rules' do
+ before do
+ configure_spree_preferences do |config|
+ config.currency = 'EUR'
+ end
+ end
+
+ after do
+ described_class.default_formatting_rules.delete(:decimal_mark)
+ described_class.default_formatting_rules.delete(:thousands_separator)
+ end
+
+ let(:money) { described_class.new(10) }
+
+ describe '#decimal_mark' do
+ it 'uses decimal mark set in Monetize gem' do
+ expect(money.decimal_mark).to eq('.')
+ end
+
+ it 'favors decimal mark set in default_formatting_rules' do
+ described_class.default_formatting_rules[:decimal_mark] = ','
+ expect(money.decimal_mark).to eq(',')
+ end
+
+ it 'favors decimal mark passed in as a parameter on initialization' do
+ money = described_class.new(10, decimal_mark: ',')
+ expect(money.decimal_mark).to eq(',')
+ end
+ end
+
+ describe '#thousands_separator' do
+ it 'uses thousands separator set in Monetize gem' do
+ expect(money.thousands_separator).to eq(',')
+ end
+
+ it 'favors decimal mark set in default_formatting_rules' do
+ described_class.default_formatting_rules[:thousands_separator] = '.'
+ expect(money.thousands_separator).to eq('.')
+ end
+
+ it 'favors decimal mark passed in as a parameter on initialization' do
+ money = described_class.new(10, thousands_separator: '.')
+ expect(money.thousands_separator).to eq('.')
+ end
+ end
+ end
+
+ describe '#amount_in_cents' do
+ %w[USD JPY KRW].each do |currency_name|
+ context "when currency is #{currency_name}" do
+ let(:money) { described_class.new(100, currency: currency_name) }
+
+ it { expect(money.amount_in_cents).to eq(10000) }
+ end
+ end
+ end
+
+ describe '#as_json' do
+ let(:options) { double('options') }
+
+ it 'returns the expected string' do
+ money = described_class.new(10)
+ expect(money.as_json(options)).to eq('$10.00')
+ end
+ end
+end
diff --git a/core/spec/lib/spree/service_module_spec.rb b/core/spec/lib/spree/service_module_spec.rb
new file mode 100644
index 00000000000..bf87d32ed6c
--- /dev/null
+++ b/core/spec/lib/spree/service_module_spec.rb
@@ -0,0 +1,223 @@
+require 'spec_helper'
+
+describe Spree::ServiceModule do
+ context 'noncallable thing passed to run' do
+ class ServiceObjectWithUncallableThing
+ prepend ::Spree::ServiceModule::Base
+
+ def call(_params)
+ run 'something_crazy'
+ end
+ end
+
+ it 'raises NonCallablePassedToRun' do
+ expect { ServiceObjectWithUncallableThing.new.call }.to raise_error(Spree::ServiceModule::NonCallablePassedToRun)
+ end
+ end
+
+ context 'unimplemented method' do
+ class ServiceObjectWithMissingMethod
+ prepend ::Spree::ServiceModule::Base
+
+ def call(_params)
+ run :non_existing_method
+ end
+ end
+
+ it 'raises MethodNotImplemented' do
+ expect { ServiceObjectWithMissingMethod.new.call }.to raise_error(Spree::ServiceModule::MethodNotImplemented)
+ end
+
+ it 'returns message in exception' do
+ begin
+ ServiceObjectWithMissingMethod.new.call
+ rescue Spree::ServiceModule::MethodNotImplemented => e
+ expect(e.message).to eq("You didn't implement non_existing_method method. Implement it before calling this class")
+ end
+ end
+ end
+
+ context 'non wrapped value' do
+ class ServiceObjectWithNonWrappedReturn
+ prepend ::Spree::ServiceModule::Base
+
+ def call(_params)
+ run :first_method
+ run :second_method
+ end
+
+ private
+
+ def first_method(_params)
+ 'not wrapped return'
+ end
+
+ def second_method(params); end
+ end
+
+ it 'raises WrongDataPassed' do
+ expect { ServiceObjectWithNonWrappedReturn.new.call }.to raise_error(Spree::ServiceModule::WrongDataPassed)
+ end
+
+ it 'returns message in exception' do
+ begin
+ ServiceObjectWithNonWrappedReturn.new.call
+ rescue Spree::ServiceModule::WrongDataPassed => e
+ expect(e.message).to eq("You didn't use `success` or `failure` method to return value from method.")
+ end
+ end
+ end
+
+ context 'non wrapped value in last method' do
+ class ServiceObjectWithNonWrappedReturn
+ prepend ::Spree::ServiceModule::Base
+
+ def call(_params)
+ run :first_method
+ end
+
+ private
+
+ def first_method(_params)
+ 'not wrapped return'
+ end
+ end
+
+ it 'raises WrongDataPassed' do
+ expect { ServiceObjectWithNonWrappedReturn.new.call }.to raise_error(Spree::ServiceModule::WrongDataPassed)
+ end
+ end
+
+ context 'first method failed' do
+ class ServiceObjectWithFailure
+ prepend ::Spree::ServiceModule::Base
+
+ def call(_params)
+ run :first_method
+ run :second_method
+ end
+
+ private
+
+ def first_method(_params)
+ failure('Failed!')
+ end
+
+ def second_method(_params)
+ success('Success!')
+ end
+ end
+
+ it 'returns result with success? false' do
+ result = ServiceObjectWithFailure.new.call
+ expect(result.success?).to eq(false)
+ end
+
+ it 'returns result with failure? true' do
+ result = ServiceObjectWithFailure.new.call
+ expect(result.failure?).to eq(true)
+ end
+
+ it 'returns value from first failed method' do
+ result = ServiceObjectWithFailure.new.call
+ expect(result.value).to eq('Failed!')
+ end
+
+ it 'returns result which is instance of Result' do
+ result = ServiceObjectWithFailure.new.call
+ expect(result).to be_an_instance_of(Spree::ServiceModule::Result)
+ end
+
+ it "doesn't call second method" do
+ service = ServiceObjectWithFailure.new
+ expect(service).not_to receive(:second_method)
+ service.call
+ end
+
+ it 'returns Result instance' do
+ expect(ServiceObjectWithFailure.new.call).to be_an_instance_of(Spree::ServiceModule::Result)
+ end
+ end
+
+ context 'success' do
+ class ServiceObjectWithSuccess
+ prepend ::Spree::ServiceModule::Base
+
+ def call(_params)
+ run :first_method
+ run :second_method
+ end
+
+ private
+
+ def first_method(_params)
+ success('First Method Success!')
+ end
+
+ def second_method(params)
+ success(params + ' Second Method Success!')
+ end
+ end
+
+ it 'returns result with success? true' do
+ result = ServiceObjectWithSuccess.new.call
+ expect(result.success?).to eq(true)
+ end
+
+ it 'returns result with failure? false' do
+ result = ServiceObjectWithSuccess.new.call
+ expect(result.failure?).to eq(false)
+ end
+
+ it 'returns value from last method' do
+ result = ServiceObjectWithSuccess.new.call
+ expect(result.value).to include('Second Method Success!')
+ expect(result.value).to include('First Method Success!')
+ end
+
+ it 'calls second method' do
+ service = ServiceObjectWithSuccess.new
+ expect(service).to receive(:second_method).and_call_original
+ service.call
+ end
+
+ it 'passes input from call to first run method' do
+ param = 'param'
+ service = ServiceObjectWithSuccess.new
+ expect(service).to receive(:first_method).with(param).and_call_original
+ service.call(param)
+ end
+
+ it 'passes empty hash if input was not provided' do
+ service = ServiceObjectWithSuccess.new
+ expect(service).to receive(:first_method).with({}).and_call_original
+ service.call
+ end
+ end
+
+ context 'not compatible params passed as result' do
+ class ServiceObjectWithIncompatibleParams
+ prepend ::Spree::ServiceModule::Base
+
+ def call(_params)
+ run :first_method
+ run :second_method
+ end
+
+ private
+
+ def first_method(_params)
+ success(first_value: 'asd', second_value: 'qwe')
+ end
+
+ def second_method(first_value:)
+ success(first_value + ' Second Method Success!')
+ end
+ end
+
+ it 'raises exception' do
+ service = ServiceObjectWithIncompatibleParams.new
+ expect { service.call }.to raise_error(Spree::ServiceModule::IncompatibleParamsPassed)
+ end
+ end
+end
diff --git a/core/spec/lib/tasks/exchanges_spec.rb b/core/spec/lib/tasks/exchanges_spec.rb
new file mode 100644
index 00000000000..adee8afe6a4
--- /dev/null
+++ b/core/spec/lib/tasks/exchanges_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe 'exchanges:charge_unreturned_items' do
+ include_context 'rake'
+
+ describe '#prerequisites' do
+ it { expect(subject.prerequisites).to include('environment') }
+ end
+
+ context 'there are no unreturned items' do
+ it { expect { subject.invoke }.not_to change { Spree::Order.count } }
+ end
+
+ context 'there are unreturned items' do
+ let!(:order) { create(:shipped_order, line_items_count: 2) }
+ let(:return_item_1) { create(:exchange_return_item, inventory_unit: order.inventory_units.first) }
+ let(:return_item_2) { create(:exchange_return_item, inventory_unit: order.inventory_units.last) }
+ let!(:rma) { create(:return_authorization, order: order, return_items: [return_item_1, return_item_2]) }
+ let!(:tax_rate) { create(:tax_rate, zone: order.tax_zone, tax_category: return_item_2.exchange_variant.tax_category) }
+
+ before do
+ @original_expedited_exchanges_pref = Spree::Config[:expedited_exchanges]
+ Spree::Config[:expedited_exchanges] = true
+ Spree::StockItem.update_all(count_on_hand: 10)
+ rma.save!
+ Spree::Shipment.last.ship!
+ return_item_1.receive!
+ Timecop.travel travel_time
+ end
+
+ after do
+ Timecop.return
+ Spree::Config[:expedited_exchanges] = @original_expedited_exchanges_pref
+ end
+
+ context 'fewer than the config allowed days have passed' do
+ let(:travel_time) { (Spree::Config[:expedited_exchanges_days_window] - 1).days }
+
+ it 'does not create a new order' do
+ expect { subject.invoke }.not_to change { Spree::Order.count }
+ end
+ end
+
+ context 'more than the config allowed days have passed' do
+ let(:travel_time) { (Spree::Config[:expedited_exchanges_days_window] + 1).days }
+
+ it 'creates a new completed order' do
+ expect { subject.invoke }.to change { Spree::Order.count }
+ expect(Spree::Order.last).to be_completed
+ end
+
+ it 'moves the shipment for the unreturned items to the new order' do
+ subject.invoke
+ new_order = Spree::Order.last
+ expect(new_order.shipments.count).to eq 1
+ expect(return_item_2.reload.exchange_shipments.first.order).to eq Spree::Order.last
+ end
+
+ it 'creates line items on the order for the unreturned items' do
+ subject.invoke
+ expect(Spree::Order.last.line_items.map(&:variant)).to eq [return_item_2.exchange_variant]
+ end
+
+ it 'associates the exchanges inventory units with the new line items' do
+ subject.invoke
+ expect(return_item_2.reload.exchange_inventory_units.last.try(:line_item).try(:order)).to eq Spree::Order.last
+ end
+
+ it 'uses the credit card from the previous order' do
+ subject.invoke
+ new_order = Spree::Order.last
+ expect(new_order.credit_cards).to be_present
+ expect(new_order.credit_cards.first).to eq order.valid_credit_cards.first
+ end
+
+ it 'authorizes the order for the full amount of the unreturned items including taxes' do
+ expect { subject.invoke }.to change { Spree::Payment.count }.by(1)
+ new_order = Spree::Order.last
+ expected_amount = return_item_2.reload.exchange_variant.price + new_order.additional_tax_total + new_order.included_tax_total
+ expect(new_order.total).to eq expected_amount
+ payment = new_order.payments.first
+ expect(payment.amount).to eq expected_amount
+ expect(payment).to be_pending
+ expect(new_order.item_total).to eq return_item_2.reload.exchange_variant.price
+ end
+
+ it 'does not attempt to create a new order for the item more than once' do
+ subject.invoke
+ subject.reenable
+ expect { subject.invoke }.not_to change { Spree::Order.count }
+ end
+
+ it 'associates the store of the original order with the exchange order' do
+ allow_any_instance_of(Spree::Order).to receive(:store_id).and_return(123)
+
+ expect(Spree::Order).to receive(:create!).once.with(hash_including(store_id: 123)) { |attrs| Spree::Order.new(attrs.except(:store_id)).tap(&:save!) }
+ subject.invoke
+ end
+
+ context 'there is no card from the previous order' do
+ let!(:credit_card) { create(:credit_card, user: order.user, default: true, gateway_customer_profile_id: 'BGS-123') }
+
+ before { allow_any_instance_of(Spree::Order).to receive(:valid_credit_cards).and_return([]) }
+
+ it "attempts to use the user's default card" do
+ expect { subject.invoke }.to change { Spree::Payment.count }.by(1)
+ new_order = Spree::Order.last
+ expect(new_order.credit_cards).to be_present
+ expect(new_order.credit_cards.first).to eq credit_card
+ end
+ end
+
+ context 'it is unable to authorize the credit card' do
+ before { allow_any_instance_of(Spree::Payment).to receive(:authorize!).and_raise(RuntimeError) }
+
+ it 'raises an error with the order' do
+ expect { subject.invoke }.to raise_error(UnableToChargeForUnreturnedItems)
+ end
+ end
+
+ context 'the exchange inventory unit is not shipped' do
+ before { return_item_2.reload.exchange_inventory_units.update_all(state: 'on hand') }
+
+ it 'does not create a new order' do
+ expect { subject.invoke }.not_to change { Spree::Order.count }
+ end
+ end
+
+ context 'the exchange inventory unit has been returned' do
+ before { return_item_2.reload.exchange_inventory_units.update_all(state: 'returned') }
+
+ it 'does not create a new order' do
+ expect { subject.invoke }.not_to change { Spree::Order.count }
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/mailers/order_mailer_spec.rb b/core/spec/mailers/order_mailer_spec.rb
new file mode 100644
index 00000000000..a66845d12e5
--- /dev/null
+++ b/core/spec/mailers/order_mailer_spec.rb
@@ -0,0 +1,122 @@
+require 'spec_helper'
+require 'email_spec'
+
+describe Spree::OrderMailer, type: :mailer do
+ include EmailSpec::Helpers
+ include EmailSpec::Matchers
+
+ before { create(:store) }
+
+ let(:order) do
+ order = stub_model(Spree::Order)
+ product = stub_model(Spree::Product, name: %{The "BEST" product})
+ variant = stub_model(Spree::Variant, product: product)
+ price = stub_model(Spree::Price, variant: variant, amount: 5.00)
+ line_item = stub_model(Spree::LineItem, variant: variant, order: order, quantity: 1, price: 4.99)
+ allow(variant).to receive_messages(default_price: price)
+ allow(order).to receive_messages(line_items: [line_item])
+ order
+ end
+
+ context ':from not set explicitly' do
+ it 'falls back to spree config' do
+ message = Spree::OrderMailer.confirm_email(order)
+ expect(message.from).to eq([Spree::Store.current.mail_from_address])
+ end
+ end
+
+ it "doesn't aggressively escape double quotes in confirmation body" do
+ confirmation_email = Spree::OrderMailer.confirm_email(order)
+ expect(confirmation_email.body).not_to include('"')
+ end
+
+ it 'confirm_email accepts an order id as an alternative to an Order object' do
+ expect(Spree::Order).to receive(:find).with(order.id).and_return(order)
+ expect do
+ Spree::OrderMailer.confirm_email(order.id).body
+ end.not_to raise_error
+ end
+
+ it 'cancel_email accepts an order id as an alternative to an Order object' do
+ expect(Spree::Order).to receive(:find).with(order.id).and_return(order)
+ expect do
+ Spree::OrderMailer.cancel_email(order.id).body
+ end.not_to raise_error
+ end
+
+ context 'only shows eligible adjustments in emails' do
+ before do
+ create(:adjustment, order: order, eligible: true, label: 'Eligible Adjustment')
+ create(:adjustment, order: order, eligible: false, label: 'Ineligible Adjustment')
+ end
+
+ let!(:confirmation_email) { Spree::OrderMailer.confirm_email(order) }
+ let!(:cancel_email) { Spree::OrderMailer.cancel_email(order) }
+
+ specify do
+ expect(confirmation_email.body).not_to include('Ineligible Adjustment')
+ end
+
+ specify do
+ expect(cancel_email.body).not_to include('Ineligible Adjustment')
+ end
+ end
+
+ context 'displays unit costs from line item' do
+ # Regression test for #2772
+
+ # Tests mailer view spree/order_mailer/confirm_email.text.erb
+ specify do
+ confirmation_email = Spree::OrderMailer.confirm_email(order)
+ expect(confirmation_email).to have_body_text('4.99')
+ expect(confirmation_email).not_to have_body_text('5.00')
+ end
+
+ # Tests mailer view spree/order_mailer/cancel_email.text.erb
+ specify do
+ cancel_email = Spree::OrderMailer.cancel_email(order)
+ expect(cancel_email).to have_body_text('4.99')
+ expect(cancel_email).not_to have_body_text('5.00')
+ end
+ end
+
+ context 'emails must be translatable' do
+ context 'pt-BR locale' do
+ before do
+ I18n.enforce_available_locales = false
+ pt_br_confirm_mail = { spree: { order_mailer: { confirm_email: { dear_customer: 'Caro Cliente,' } } } }
+ pt_br_cancel_mail = { spree: { order_mailer: { cancel_email: { order_summary_canceled: 'Resumo da Pedido [CANCELADA]' } } } }
+ I18n.backend.store_translations :'pt-BR', pt_br_confirm_mail
+ I18n.backend.store_translations :'pt-BR', pt_br_cancel_mail
+ I18n.locale = :'pt-BR'
+ end
+
+ after do
+ I18n.locale = I18n.default_locale
+ I18n.enforce_available_locales = true
+ end
+
+ context 'confirm_email' do
+ specify do
+ confirmation_email = Spree::OrderMailer.confirm_email(order)
+ expect(confirmation_email).to have_body_text('Caro Cliente,')
+ end
+ end
+
+ context 'cancel_email' do
+ specify do
+ cancel_email = Spree::OrderMailer.cancel_email(order)
+ expect(cancel_email).to have_body_text('Resumo da Pedido [CANCELADA]')
+ end
+ end
+ end
+ end
+
+ context 'with preference :send_core_emails set to false' do
+ it 'sends no email' do
+ Spree::Config.set(:send_core_emails, false)
+ message = Spree::OrderMailer.confirm_email(order)
+ expect(message.body).to be_blank
+ end
+ end
+end
diff --git a/core/spec/mailers/reimbursement_mailer_spec.rb b/core/spec/mailers/reimbursement_mailer_spec.rb
new file mode 100644
index 00000000000..990d934dc14
--- /dev/null
+++ b/core/spec/mailers/reimbursement_mailer_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+require 'email_spec'
+
+describe Spree::ReimbursementMailer, type: :mailer do
+ include EmailSpec::Helpers
+ include EmailSpec::Matchers
+
+ let(:reimbursement) { create(:reimbursement) }
+
+ context ':from not set explicitly' do
+ it 'falls back to spree config' do
+ message = Spree::ReimbursementMailer.reimbursement_email(reimbursement)
+ expect(message.from).to eq [Spree::Store.current.mail_from_address]
+ end
+ end
+
+ it 'accepts a reimbursement id as an alternative to a Reimbursement object' do
+ expect(Spree::Reimbursement).to receive(:find).with(reimbursement.id).and_return(reimbursement)
+
+ expect do
+ Spree::ReimbursementMailer.reimbursement_email(reimbursement.id).body
+ end.not_to raise_error
+ end
+
+ context 'emails must be translatable' do
+ context 'reimbursement_email' do
+ context 'pt-BR locale' do
+ before do
+ I18n.enforce_available_locales = false
+ pt_br_shipped_email = { spree: { reimbursement_mailer: { reimbursement_email: { dear_customer: 'Caro Cliente,' } } } }
+ I18n.backend.store_translations :'pt-BR', pt_br_shipped_email
+ I18n.locale = :'pt-BR'
+ end
+
+ after do
+ I18n.locale = I18n.default_locale
+ I18n.enforce_available_locales = true
+ end
+
+ it 'localized in HTML template' do
+ reimbursement_email = Spree::ReimbursementMailer.reimbursement_email(reimbursement)
+ reimbursement_email.html_part.to include('Caro Cliente,')
+ end
+
+ it 'localized in text template' do
+ reimbursement_email = Spree::ReimbursementMailer.reimbursement_email(reimbursement)
+ reimbursement_email.text_part.to include('Caro Cliente,')
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/mailers/shipment_mailer_spec.rb b/core/spec/mailers/shipment_mailer_spec.rb
new file mode 100644
index 00000000000..697faf9107b
--- /dev/null
+++ b/core/spec/mailers/shipment_mailer_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+require 'email_spec'
+
+describe Spree::ShipmentMailer, type: :mailer do
+ include EmailSpec::Helpers
+ include EmailSpec::Matchers
+
+ before { create(:store) }
+
+ let(:order) { stub_model(Spree::Order, number: 'R12345') }
+ let(:shipping_method) { stub_model(Spree::ShippingMethod, name: 'USPS') }
+ let(:product) { stub_model(Spree::Product, name: %{The "BEST" product}, sku: 'SKU0001') }
+ let(:variant) { stub_model(Spree::Variant, product: product) }
+ let(:line_item) { stub_model(Spree::LineItem, variant: variant, order: order, quantity: 1, price: 5) }
+ let(:shipment) do
+ shipment = stub_model(Spree::Shipment)
+ allow(shipment).to receive_messages(line_items: [line_item], order: order)
+ allow(shipment).to receive_messages(tracking_url: 'http://track.com/me')
+ allow(shipment).to receive_messages(shipping_method: shipping_method)
+ shipment
+ end
+
+ context ':from not set explicitly' do
+ it 'falls back to spree config' do
+ message = Spree::ShipmentMailer.shipped_email(shipment)
+ expect(message.from).to eq([Spree::Store.current.mail_from_address])
+ end
+ end
+
+ # Regression test for #2196
+ it "doesn't include out of stock in the email body" do
+ shipment_email = Spree::ShipmentMailer.shipped_email(shipment)
+ expect(shipment_email.body).not_to include(%q{Out of Stock})
+ end
+
+ it 'shipment_email accepts an shipment id as an alternative to an Shipment object' do
+ expect(Spree::Shipment).to receive(:find).with(shipment.id).and_return(shipment)
+ expect do
+ Spree::ShipmentMailer.shipped_email(shipment.id).body
+ end.not_to raise_error
+ end
+
+ context 'emails must be translatable' do
+ context 'shipped_email' do
+ context 'pt-BR locale' do
+ before do
+ I18n.enforce_available_locales = false
+ pt_br_shipped_email = { spree: { shipment_mailer: { shipped_email: { dear_customer: 'Caro Cliente,' } } } }
+ I18n.backend.store_translations :'pt-BR', pt_br_shipped_email
+ I18n.locale = :'pt-BR'
+ end
+
+ after do
+ I18n.locale = I18n.default_locale
+ I18n.enforce_available_locales = true
+ end
+
+ specify do
+ shipped_email = Spree::ShipmentMailer.shipped_email(shipment)
+ expect(shipped_email).to have_body_text('Caro Cliente,')
+ end
+ end
+ end
+ end
+
+ context 'shipped_email' do
+ let(:shipped_email) { Spree::ShipmentMailer.shipped_email(shipment) }
+
+ specify do
+ expect(shipped_email).to have_body_text(order.number)
+ end
+
+ specify do
+ expect(shipped_email).to have_body_text(shipping_method.name)
+ end
+
+ specify do
+ expect(shipped_email).to have_body_text("href=\"#{shipment.tracking_url}\"")
+ end
+ end
+end
diff --git a/core/spec/mailers/test_mailer_spec.rb b/core/spec/mailers/test_mailer_spec.rb
new file mode 100644
index 00000000000..8d696465578
--- /dev/null
+++ b/core/spec/mailers/test_mailer_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+require 'email_spec'
+
+describe Spree::TestMailer, type: :mailer do
+ include EmailSpec::Helpers
+ include EmailSpec::Matchers
+
+ before { create(:store) }
+
+ let(:user) { create(:user) }
+
+ context ':from not set explicitly' do
+ it 'falls back to spree config' do
+ message = Spree::TestMailer.test_email('test@example.com')
+ expect(message.from).to eq([Spree::Store.current.mail_from_address])
+ end
+ end
+
+ it 'confirm_email accepts a user id as an alternative to a User object' do
+ expect do
+ Spree::TestMailer.test_email('test@example.com')
+ end.not_to raise_error
+ end
+
+ context 'action mailer host' do
+ it 'falls back to spree store url' do
+ ActionMailer::Base.default_url_options = {}
+ Spree::TestMailer.test_email('test@example.com').deliver_now
+ expect(ActionMailer::Base.default_url_options[:host]).to eq(Spree::Store.current.url)
+ end
+
+ it 'uses developer set host' do
+ ActionMailer::Base.default_url_options[:host] = 'test.test'
+ Spree::TestMailer.test_email('test@example.com').deliver_now
+ expect(ActionMailer::Base.default_url_options[:host]).to eq('test.test')
+ end
+ end
+end
diff --git a/core/spec/models/spree/ability_spec.rb b/core/spec/models/spree/ability_spec.rb
new file mode 100644
index 00000000000..4adb79cc8f8
--- /dev/null
+++ b/core/spec/models/spree/ability_spec.rb
@@ -0,0 +1,269 @@
+require 'spec_helper'
+require 'cancan/matchers'
+require 'spree/testing_support/ability_helpers'
+require 'spree/testing_support/bar_ability'
+
+# Fake ability for testing registration of additional abilities
+class FooAbility
+ include CanCan::Ability
+
+ def initialize(_user)
+ # allow anyone to perform index on Order
+ can :index, Spree::Order
+ # allow anyone to update an Order with id of 1
+ can :update, Spree::Order do |order|
+ order.id == 1
+ end
+ end
+end
+
+describe Spree::Ability, type: :model do
+ let(:user) { build(:user) }
+ let(:ability) { Spree::Ability.new(user) }
+ let(:token) { nil }
+
+ after do
+ Spree::Ability.abilities = Set.new
+ end
+
+ context 'register_ability' do
+ it 'adds the ability to the list of abilties' do
+ Spree::Ability.register_ability(FooAbility)
+ expect(Spree::Ability.new(user).abilities).not_to be_empty
+ end
+
+ it 'applies the registered abilities permissions' do
+ Spree::Ability.register_ability(FooAbility)
+ expect(Spree::Ability.new(user).can?(:update, mock_model(Spree::Order, id: 1))).to be true
+ end
+ end
+
+ context '#abilities_to_register' do
+ it 'adds the ability to the list of abilities' do
+ allow_any_instance_of(Spree::Ability).to receive(:abilities_to_register).and_return([FooAbility])
+ expect(Spree::Ability.new(user).abilities).to include FooAbility
+ end
+
+ it 'applies the registered abilities permissions' do
+ allow_any_instance_of(Spree::Ability).to receive(:abilities_to_register).and_return([FooAbility])
+ expect(Spree::Ability.new(user).can?(:update, mock_model(Spree::Order, id: 1))).to be true
+ end
+ end
+
+ context 'for general resource' do
+ let(:resource) { Object.new }
+
+ context 'with admin user' do
+ before { allow(user).to receive(:has_spree_role?).and_return(true) }
+
+ it_behaves_like 'access granted'
+ it_behaves_like 'index allowed'
+ end
+
+ context 'with customer' do
+ it_behaves_like 'access denied'
+ it_behaves_like 'no index allowed'
+ end
+ end
+
+ context 'for admin protected resources' do
+ let(:resource) { Object.new }
+ let(:resource_shipment) { Spree::Shipment.new }
+ let(:resource_product) { Spree::Product.new }
+ let(:resource_user) { create :user }
+ let(:resource_order) { Spree::Order.new }
+ let(:fakedispatch_user) { Spree.user_class.create }
+ let(:fakedispatch_ability) { Spree::Ability.new(fakedispatch_user) }
+
+ context 'with admin user' do
+ it 'is able to admin' do
+ user.spree_roles << Spree::Role.find_or_create_by(name: 'admin')
+ expect(ability).to be_able_to :admin, resource
+ expect(ability).to be_able_to :index, resource_order
+ expect(ability).to be_able_to :show, resource_product
+ expect(ability).to be_able_to :create, resource_user
+ end
+ end
+
+ context 'with fakedispatch user' do
+ it 'is able to admin on the order and shipment pages' do
+ user.spree_roles << Spree::Role.find_or_create_by(name: 'bar')
+
+ Spree::Ability.register_ability(BarAbility)
+
+ expect(ability).not_to be_able_to :admin, resource
+
+ expect(ability).to be_able_to :admin, resource_order
+ expect(ability).to be_able_to :index, resource_order
+ expect(ability).not_to be_able_to :update, resource_order
+ # ability.should_not be_able_to :create, resource_order # Fails
+
+ expect(ability).to be_able_to :admin, resource_shipment
+ expect(ability).to be_able_to :index, resource_shipment
+ expect(ability).to be_able_to :create, resource_shipment
+
+ expect(ability).not_to be_able_to :admin, resource_product
+ expect(ability).not_to be_able_to :update, resource_product
+ # ability.should_not be_able_to :show, resource_product # Fails
+
+ expect(ability).not_to be_able_to :admin, resource_user
+ expect(ability).not_to be_able_to :update, resource_user
+ expect(ability).to be_able_to :update, user
+ # ability.should_not be_able_to :create, resource_user # Fails
+ # It can create new users if is has access to the :admin, User!!
+
+ # TODO: change the Ability class so only users and customers get the extra premissions?
+
+ Spree::Ability.remove_ability(BarAbility)
+ end
+ end
+
+ context 'with customer' do
+ it 'is not able to admin' do
+ expect(ability).not_to be_able_to :admin, resource
+ expect(ability).not_to be_able_to :admin, resource_order
+ expect(ability).not_to be_able_to :admin, resource_product
+ expect(ability).not_to be_able_to :admin, resource_user
+ end
+ end
+ end
+
+ context 'as Guest User' do
+ context 'for Country' do
+ let(:resource) { Spree::Country.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for OptionType' do
+ let(:resource) { Spree::OptionType.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for OptionValue' do
+ let(:resource) { Spree::OptionType.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for Order' do
+ let(:resource) { Spree::Order.new }
+
+ context 'requested by same user' do
+ before { resource.user = user }
+
+ it_behaves_like 'access granted'
+ it_behaves_like 'no index allowed'
+ end
+
+ context 'requested by other user' do
+ before { resource.user = Spree.user_class.new }
+
+ it_behaves_like 'create only'
+ end
+
+ context 'requested with proper token' do
+ let(:token) { 'TOKEN123' }
+
+ before { allow(resource).to receive_messages token: token }
+
+ it_behaves_like 'access granted'
+ it_behaves_like 'no index allowed'
+ end
+
+ context 'requested with inproper token' do
+ let(:token) { 'FAIL' }
+
+ before { allow(resource).to receive_messages token: token }
+
+ it_behaves_like 'create only'
+ end
+ end
+
+ context 'for Product' do
+ let(:resource) { Spree::Product.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for ProductProperty' do
+ let(:resource) { Spree::Product.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for Property' do
+ let(:resource) { Spree::Product.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for State' do
+ let(:resource) { Spree::State.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for Taxons' do
+ let(:resource) { Spree::Taxon.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for Taxonomy' do
+ let(:resource) { Spree::Taxonomy.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for User' do
+ context 'requested by same user' do
+ let(:resource) { user }
+
+ it_behaves_like 'access granted'
+ it_behaves_like 'no index allowed'
+ end
+
+ context 'requested by other user' do
+ let(:resource) { create(:user) }
+
+ it_behaves_like 'create only'
+ end
+ end
+
+ context 'for Variant' do
+ let(:resource) { Spree::Variant.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+
+ context 'for Zone' do
+ let(:resource) { Spree::Zone.new }
+
+ context 'requested by any user' do
+ it_behaves_like 'read only'
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/address_spec.rb b/core/spec/models/spree/address_spec.rb
new file mode 100644
index 00000000000..29cb6884fa3
--- /dev/null
+++ b/core/spec/models/spree/address_spec.rb
@@ -0,0 +1,471 @@
+require 'spec_helper'
+
+describe Spree::Address, type: :model do
+ describe 'clone' do
+ it 'creates a copy of the address with the exception of the id, updated_at and created_at attributes' do
+ state = create(:state)
+ original = create(:address,
+ address1: 'address1',
+ address2: 'address2',
+ alternative_phone: 'alternative_phone',
+ city: 'city',
+ country: Spree::Country.first,
+ firstname: 'firstname',
+ lastname: 'lastname',
+ company: 'company',
+ phone: 'phone',
+ state_id: state.id,
+ state_name: state.name,
+ zipcode: '10001')
+
+ cloned = original.clone
+
+ expect(cloned.address1).to eq(original.address1)
+ expect(cloned.address2).to eq(original.address2)
+ expect(cloned.alternative_phone).to eq(original.alternative_phone)
+ expect(cloned.city).to eq(original.city)
+ expect(cloned.country_id).to eq(original.country_id)
+ expect(cloned.firstname).to eq(original.firstname)
+ expect(cloned.lastname).to eq(original.lastname)
+ expect(cloned.company).to eq(original.company)
+ expect(cloned.phone).to eq(original.phone)
+ expect(cloned.state_id).to eq(original.state_id)
+ expect(cloned.state_name).to eq(original.state_name)
+ expect(cloned.zipcode).to eq(original.zipcode)
+
+ expect(cloned.id).not_to eq(original.id)
+ expect(cloned.created_at).not_to eq(original.created_at)
+ expect(cloned.updated_at).not_to eq(original.updated_at)
+ end
+ end
+
+ describe 'delegated method' do
+ context 'Country' do
+ let(:country) { create(:country, name: 'United States', iso_name: 'UNITED STATES', iso: 'US', iso3: 'USA') }
+ let(:address) { create(:address, country: country) }
+
+ context '#country_name' do
+ it 'return proper country_iso_name' do
+ expect(address.country_name).to eq 'United States'
+ end
+ end
+
+ context '#country_iso_name' do
+ it 'return proper country_iso_name' do
+ expect(address.country_iso_name).to eq 'UNITED STATES'
+ end
+ end
+
+ context '#country_iso' do
+ it 'return proper country_iso_name' do
+ expect(address.country_iso).to eq 'US'
+ end
+ end
+
+ context '#country_iso3' do
+ it 'return proper country_iso_name' do
+ expect(address.country_iso3).to eq 'USA'
+ end
+ end
+ end
+ end
+
+ context 'aliased attributes' do
+ let(:address) { Spree::Address.new }
+
+ it 'first_name' do
+ address.firstname = 'Ryan'
+ expect(address.first_name).to eq('Ryan')
+ end
+
+ it 'last_name' do
+ address.lastname = 'Bigg'
+ expect(address.last_name).to eq('Bigg')
+ end
+ end
+
+ context 'validation' do
+ let(:country) { stub_model(Spree::Country, states: [state], states_required: true) }
+ let(:state) { stub_model(Spree::State, name: 'maryland', abbr: 'md') }
+ let(:address) { build(:address, country: country) }
+
+ before do
+ allow(Spree::State).to receive(:find_all_by_name_or_abbr) { [state] }
+
+ configure_spree_preferences do |config|
+ config.address_requires_state = true
+ end
+ end
+
+ it 'state_name is not nil and country does not have any states' do
+ address.state = nil
+ address.state_name = 'alabama'
+ expect(address).to be_valid
+ end
+
+ it 'errors when state_name is nil' do
+ address.state_name = nil
+ address.state = nil
+ expect(address).not_to be_valid
+ end
+
+ it 'full state name is in state_name and country does contain that state' do
+ address.state_name = 'alabama'
+ # called by state_validate to set up state_id.
+ # Perhaps this should be a before_validation instead?
+ expect(address).to be_valid
+ expect(address.state).not_to be_nil
+ expect(address.state_name).to be_nil
+ end
+
+ it 'state abbr is in state_name and country does contain that state' do
+ address.state_name = state.abbr
+ expect(address).to be_valid
+ expect(address.state_id).not_to be_nil
+ expect(address.state_name).to be_nil
+ end
+
+ it 'state is entered but country does not contain that state' do
+ address.state = state
+ address.country = stub_model(Spree::Country, states_required: true)
+ address.valid?
+ expect(address.errors['state']).to eq(['is invalid'])
+ end
+
+ it 'both state and state_name are entered but country does not contain the state' do
+ address.state = state
+ address.state_name = 'maryland'
+ address.country = stub_model(Spree::Country, states_required: true)
+ expect(address).to be_valid
+ expect(address.state_id).to be_nil
+ end
+
+ it 'both state and state_name are entered and country does contain the state' do
+ address.state = state
+ address.state_name = 'maryland'
+ expect(address).to be_valid
+ expect(address.state_name).to be_nil
+ end
+
+ it 'address_requires_state preference is false' do
+ Spree::Config.set address_requires_state: false
+ address.state = nil
+ address.state_name = nil
+ expect(address).to be_valid
+ end
+
+ it 'requires phone' do
+ address.phone = ''
+ address.valid?
+ expect(address.errors['phone']).to eq(["can't be blank"])
+ end
+
+ it 'requires zipcode' do
+ address.zipcode = ''
+ address.valid?
+ expect(address.errors['zipcode']).to include("can't be blank")
+ end
+
+ context 'zipcode validation' do
+ it 'validates the zipcode' do
+ allow(address.country).to receive(:iso).and_return('US')
+ address.zipcode = 'abc'
+ address.valid?
+ expect(address.errors['zipcode']).to include('is invalid')
+ end
+
+ it 'accepts a zip code with surrounding white space' do
+ allow(address.country).to receive(:iso).and_return('US')
+ address.zipcode = ' 12345 '
+ address.valid?
+ expect(address.errors['zipcode']).not_to include('is invalid')
+ end
+
+ context 'does not validate' do
+ it 'does not have a country' do
+ address.country = nil
+ address.valid?
+ expect(address.errors['zipcode']).not_to include('is invalid')
+ end
+
+ it 'country does not requires zipcode' do
+ allow(address.country).to receive(:zipcode_required?).and_return(false)
+ address.valid?
+ expect(address.errors['zipcode']).not_to include('is invalid')
+ end
+
+ it 'does not have an iso' do
+ allow(address.country).to receive(:iso).and_return(nil)
+ address.valid?
+ expect(address.errors['zipcode']).not_to include('is invalid')
+ end
+
+ it 'does not have a zipcode' do
+ address.zipcode = ''
+ address.valid?
+ expect(address.errors['zipcode']).not_to include('is invalid')
+ end
+
+ it 'does not have a supported country iso' do
+ allow(address.country).to receive(:iso).and_return('BO')
+ address.valid?
+ expect(address.errors['zipcode']).not_to include('is invalid')
+ end
+ end
+ end
+
+ context 'phone not required' do
+ before { allow(address).to receive_messages require_phone?: false }
+
+ it 'shows no errors when phone is blank' do
+ address.phone = ''
+ address.valid?
+ expect(address.errors[:phone].size).to eq 0
+ end
+ end
+
+ context 'zipcode not required' do
+ before { allow(address).to receive_messages require_zipcode?: false }
+
+ it 'shows no errors when phone is blank' do
+ address.zipcode = ''
+ address.valid?
+ expect(address.errors[:zipcode].size).to eq 0
+ end
+ end
+ end
+
+ context '.default' do
+ context 'no user given' do
+ before do
+ @default_country_id = Spree::Config[:default_country_id]
+ new_country = create(:country)
+ Spree::Config[:default_country_id] = new_country.id
+ end
+
+ after do
+ Spree::Config[:default_country_id] = @default_country_id
+ end
+
+ it 'sets up a new record with Spree::Config[:default_country_id]' do
+ expect(Spree::Address.default.country).to eq(Spree::Country.find(Spree::Config[:default_country_id]))
+ end
+
+ # Regression test for #1142
+ it 'uses the first available country if :default_country_id is set to an invalid value' do
+ Spree::Config[:default_country_id] = '0'
+ expect(Spree::Address.default.country).to eq(Spree::Country.first)
+ end
+ end
+
+ context 'user given' do
+ let(:bill_address) { Spree::Address.new(phone: Time.current.to_i) }
+ let(:ship_address) { double('ShipAddress') }
+ let(:user) { double('User', bill_address: bill_address, ship_address: ship_address) }
+
+ it 'returns a copy of that user bill address' do
+ expect(Spree::Address.default(user).phone).to eq bill_address.phone
+ end
+
+ it 'falls back to build default when user has no address' do
+ allow(user).to receive_messages(bill_address: nil)
+ expect(Spree::Address.default(user)).to eq Spree::Address.build_default
+ end
+ end
+ end
+
+ context '#full_name' do
+ context 'both first and last names are present' do
+ let(:address) { stub_model(Spree::Address, firstname: 'Michael', lastname: 'Jackson') }
+
+ specify { expect(address.full_name).to eq('Michael Jackson') }
+ end
+
+ context 'first name is blank' do
+ let(:address) { stub_model(Spree::Address, firstname: nil, lastname: 'Jackson') }
+
+ specify { expect(address.full_name).to eq('Jackson') }
+ end
+
+ context 'last name is blank' do
+ let(:address) { stub_model(Spree::Address, firstname: 'Michael', lastname: nil) }
+
+ specify { expect(address.full_name).to eq('Michael') }
+ end
+
+ context 'both first and last names are blank' do
+ let(:address) { stub_model(Spree::Address, firstname: nil, lastname: nil) }
+
+ specify { expect(address.full_name).to eq('') }
+ end
+ end
+
+ context '#state_text' do
+ context 'state is blank' do
+ let(:address) { stub_model(Spree::Address, state: nil, state_name: 'virginia') }
+
+ specify { expect(address.state_text).to eq('virginia') }
+ end
+
+ context 'both name and abbr is present' do
+ let(:state) { stub_model(Spree::State, name: 'virginia', abbr: 'va') }
+ let(:address) { stub_model(Spree::Address, state: state) }
+
+ specify { expect(address.state_text).to eq('va') }
+ end
+
+ context 'only name is present' do
+ let(:state) { stub_model(Spree::State, name: 'virginia', abbr: nil) }
+ let(:address) { stub_model(Spree::Address, state: state) }
+
+ specify { expect(address.state_text).to eq('virginia') }
+ end
+ end
+
+ context '#state_name_text' do
+ context 'state_name is blank' do
+ let(:state) { create(:state, name: 'virginia', abbr: nil) }
+ let(:address) { create(:address, state: state, state_name: nil) }
+
+ specify { expect(address.state_name_text).to eq('virginia') }
+ end
+
+ context 'state is blank' do
+ let(:address) { create(:address, state: nil, state_name: 'virginia') }
+
+ specify { expect(address.state_name_text).to eq('virginia') }
+ end
+
+ context 'state and state_name are present' do
+ let(:state) { create(:state, name: 'virginia', abbr: nil) }
+ let(:address) { create(:address, state: state, state_name: 'virginia') }
+
+ specify { expect(address.state_name_text).to eq('virginia') }
+ end
+ end
+
+ context 'defines require_phone? helper method' do
+ let(:address) { stub_model(Spree::Address) }
+
+ specify { expect(address.instance_eval { require_phone? }).to be true }
+ end
+
+ context '#clear_state' do
+ let (:address) { create(:address) }
+
+ before { address.state_name = 'maryland' }
+
+ it { expect { address.send(:clear_state) }.to change(address, :state).to(nil).from(address.state) }
+ it { expect { address.send(:clear_state) }.not_to change(address, :state_name) }
+ end
+
+ context '#clear_state_name' do
+ let (:address) { create(:address) }
+
+ before { address.state_name = 'maryland' }
+
+ it { expect { address.send(:clear_state_name) }.not_to change(address, :state_id) }
+ it { expect { address.send(:clear_state_name) }.to change(address, :state_name).to(nil).from('maryland') }
+ end
+
+ context '#clear_invalid_state_entities' do
+ let(:country) { create(:country) }
+ let(:state) { create(:state, country: country) }
+ let (:address) { create(:address, country: country, state: state) }
+
+ def clear_state_entities
+ address.send(:clear_invalid_state_entities)
+ end
+
+ context 'state not present and state_name both not present' do
+ before do
+ address.state = nil
+ address.state_name = nil
+ clear_state_entities
+ end
+
+ it { expect(address.state).to be_nil }
+ it { expect(address.state_name).to be_nil }
+ end
+
+ context 'state_name not present and state present ' do
+ before { address.state_name = nil }
+
+ context 'state belongs to a different country than to which address is associated' do
+ before do
+ address.country = create(:country)
+ clear_state_entities
+ end
+
+ it { expect(address.state).to be_nil }
+ it { expect(address.state_name).to be_nil }
+ end
+
+ context 'state belongs to the same country associated with address' do
+ before { clear_state_entities }
+
+ it { expect(address.state).to eq(state) }
+ it { expect(address.state_name).to be_nil }
+ end
+ end
+
+ context 'state not present and state_name present' do
+ before do
+ address.state = nil
+ address.state_name = state.name
+ end
+
+ context 'when country has no states and state is required' do
+ before do
+ address.country = create(:country, states_required: true)
+ clear_state_entities
+ end
+
+ it { expect(address.state).to be_nil }
+ it { expect(address.state_name).to eq(state.name) }
+ end
+
+ context 'when country has states' do
+ before do
+ address.state_name = state.name
+ clear_state_entities
+ end
+
+ it { expect(address.state).to be_nil }
+ it { expect(address.state_name).to eq(state.name) }
+ end
+
+ context 'when country has no states and state is not required' do
+ before do
+ address.country = create(:country, states_required: false)
+ address.state_name = state.name
+ clear_state_entities
+ end
+
+ it { expect(address.state).to be_nil }
+ it { expect(address.state_name).to be_nil }
+ end
+ end
+ end
+
+ context '#==' do
+ let(:address) { create(:address) }
+ let(:address2) { address.clone }
+
+ context 'same addresses' do
+ it { expect(address == address2).to eq(true) }
+ end
+
+ context 'different addresses' do
+ before { address2.first_name = 'Someone Else' }
+
+ it { expect(address == address2).to eq(false) }
+ end
+ end
+
+ describe '.build_default' do
+ let(:_address) { described_class.build_default }
+
+ it { expect(_address.country).to eq(Spree::Country.default) }
+ end
+end
diff --git a/core/spec/models/spree/adjustable/adjuster/base_spec.rb b/core/spec/models/spree/adjustable/adjuster/base_spec.rb
new file mode 100644
index 00000000000..4cac0dd430b
--- /dev/null
+++ b/core/spec/models/spree/adjustable/adjuster/base_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe Spree::Adjustable::Adjuster::Base, type: :model do
+ let(:line_item) { create(:line_item) }
+ let(:subject) { Spree::Adjustable::Adjuster::Base }
+
+ it 'raises missing update method' do
+ expect { subject.adjust(line_item, {}) }.to raise_error(NotImplementedError)
+ end
+end
diff --git a/core/spec/models/spree/adjustable/adjuster/promotion_spec.rb b/core/spec/models/spree/adjustable/adjuster/promotion_spec.rb
new file mode 100644
index 00000000000..fd502161bf7
--- /dev/null
+++ b/core/spec/models/spree/adjustable/adjuster/promotion_spec.rb
@@ -0,0 +1,211 @@
+require 'spec_helper'
+
+describe Spree::Adjustable::Adjuster::Promotion, type: :model do
+ let(:order) { create :order_with_line_items, line_items_count: 1 }
+ let(:line_item) { order.line_items.first }
+ let(:subject) { Spree::Adjustable::AdjustmentsUpdater.new(line_item) }
+ let(:order_subject) { Spree::Adjustable::AdjustmentsUpdater.new(order) }
+
+ context 'best promotion is always applied' do
+ let(:calculator) { Spree::Calculator::FlatRate.new(preferred_amount: 10) }
+ let(:source) { Spree::Promotion::Actions::CreateItemAdjustments.create calculator: calculator }
+
+ def create_adjustment(label, amount)
+ create(:adjustment,
+ order: order,
+ adjustable: line_item,
+ source: source,
+ amount: amount,
+ state: 'closed',
+ label: label,
+ mandatory: false)
+ end
+
+ describe 'competing promos' do
+ before { Spree::Adjustment.competing_promos_source_types = ['Spree::PromotionAction', 'Custom'] }
+
+ it 'do not update promo_total' do
+ create(:adjustment,
+ order: order,
+ adjustable: line_item,
+ source_type: 'Custom',
+ source_id: nil,
+ amount: -3.50,
+ label: 'Other',
+ mandatory: false)
+ create_adjustment('Promotion A', -2.50)
+
+ subject.update
+ expect(line_item.promo_total).to eq(0.0)
+ end
+ end
+
+ it 'uses only the most valuable promotion' do
+ create_adjustment('Promotion A', -100)
+ create_adjustment('Promotion B', -200)
+ create_adjustment('Promotion C', -300)
+ create(:adjustment,
+ order: order,
+ adjustable: line_item,
+ source: nil,
+ amount: -500,
+ state: 'closed',
+ label: 'Some other credit')
+ line_item.adjustments.each { |a| a.update_column(:eligible, true) }
+
+ subject.update
+
+ expect(line_item.adjustments.promotion.eligible.count).to eq(1)
+ expect(line_item.adjustments.promotion.eligible.first.label).to eq('Promotion C')
+ end
+
+ it 'chooses the most recent promotion adjustment when amounts are equal' do
+ # Using Timecop is a regression test
+ Timecop.freeze do
+ create_adjustment('Promotion A', -200)
+ create_adjustment('Promotion B', -200)
+ end
+ line_item.adjustments.each { |a| a.update_column(:eligible, true) }
+
+ subject.update
+
+ expect(line_item.adjustments.promotion.eligible.count).to eq(1)
+ expect(line_item.adjustments.promotion.eligible.first.label).to eq('Promotion B')
+ end
+
+ context 'when previously ineligible promotions become available' do
+ let(:order_promo1) do
+ create(:promotion,
+ :with_order_adjustment,
+ :with_item_total_rule,
+ weighted_order_adjustment_amount: 5,
+ item_total_threshold_amount: 10)
+ end
+
+ let(:order_promo2) do
+ create(:promotion,
+ :with_order_adjustment,
+ :with_item_total_rule,
+ weighted_order_adjustment_amount: 10,
+ item_total_threshold_amount: 20)
+ end
+
+ let(:order_promos) { [order_promo1, order_promo2] }
+
+ let(:line_item_promo1) do
+ create(:promotion,
+ :with_line_item_adjustment,
+ :with_item_total_rule,
+ adjustment_rate: 2.5,
+ item_total_threshold_amount: 10)
+ end
+
+ let(:line_item_promo2) do
+ create(:promotion,
+ :with_line_item_adjustment,
+ :with_item_total_rule,
+ adjustment_rate: 5,
+ item_total_threshold_amount: 20)
+ end
+
+ let(:line_item_promos) { [line_item_promo1, line_item_promo2] }
+ let(:order) { create(:order_with_line_items, line_items_count: 1) }
+
+ # Apply promotions in different sequences. Results should be the same.
+ promo_sequences = [[0, 1], [1, 0]]
+
+ promo_sequences.each do |promo_sequence|
+ it 'picks the best order-level promo according to current eligibility' do
+ # apply both promos to the order, even though only promo1 is eligible
+ order_promos[promo_sequence[0]].activate order: order
+ order_promos[promo_sequence[1]].activate order: order
+
+ order.reload
+ msg = "Expected two adjustments (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.count).to eq(2), msg
+
+ msg = "Expected one elegible adjustment (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.eligible.count).to eq(1), msg
+
+ msg = "Expected promo1 to be used (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.eligible.first.source.promotion).to eq(order_promo1), msg
+
+ Spree::Cart::AddItem.call(order: order, variant: create(:variant, price: 10))
+ order.save
+
+ order.reload
+ msg = "Expected two adjustments (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.count).to eq(2), msg
+
+ msg = "Expected one elegible adjustment (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.eligible.count).to eq(1), msg
+
+ msg = "Expected promo2 to be used (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.eligible.first.source.promotion).to eq(order_promo2), msg
+ end
+ end
+
+ promo_sequences.each do |promo_sequence|
+ it 'picks the best line-item-level promo according to current eligibility' do
+ # apply both promos to the order, even though only promo1 is eligible
+ line_item_promos[promo_sequence[0]].activate order: order
+ line_item_promos[promo_sequence[1]].activate order: order
+
+ order.reload
+ msg = "Expected one adjustment (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.count).to eq(1), msg
+
+ msg = "Expected one elegible adjustment (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.eligible.count).to eq(1), msg
+
+ # line_item_promo1 is the only one that has thus far met the order total threshold,
+ # it is the only promo which should be applied.
+ msg = "Expected line_item_promo1 to be used (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.first.source.promotion).to eq(line_item_promo1), msg
+
+ Spree::Cart::AddItem.call(order: order, variant: create(:variant, price: 10))
+ order.save
+
+ order.reload
+ msg = "Expected four adjustments (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.count).to eq(4), msg
+
+ msg = "Expected two elegible adjustments (using sequence #{promo_sequence})"
+ expect(order.all_adjustments.eligible.count).to eq(2), msg
+
+ order.all_adjustments.eligible.each do |adjustment|
+ msg = "Expected line_item_promo2 to be used (using sequence #{promo_sequence})"
+ expect(adjustment.source.promotion).to eq(line_item_promo2), msg
+ end
+ end
+ end
+ end
+
+ context 'multiple adjustments and the best one is not eligible' do
+ let!(:promo_a) { create_adjustment('Promotion A', -100) }
+ let!(:promo_c) { create_adjustment('Promotion C', -300) }
+
+ before do
+ promo_a.update_column(:eligible, true)
+ promo_c.update_column(:eligible, false)
+ end
+
+ # regression for #3274
+ it 'still makes the previous best eligible adjustment valid' do
+ subject.update
+ expect(line_item.adjustments.promotion.eligible.first.label).to eq('Promotion A')
+ end
+ end
+
+ it 'only leaves one adjustment even if 2 have the same amount' do
+ create_adjustment('Promotion A', -100)
+ create_adjustment('Promotion B', -200)
+ create_adjustment('Promotion C', -200)
+
+ subject.update
+
+ expect(line_item.adjustments.promotion.eligible.count).to eq(1)
+ expect(line_item.adjustments.promotion.eligible.first.amount.to_i).to eq(-200)
+ end
+ end
+end
diff --git a/core/spec/models/spree/adjustable/adjuster/tax_spec.rb b/core/spec/models/spree/adjustable/adjuster/tax_spec.rb
new file mode 100644
index 00000000000..1d7b142b644
--- /dev/null
+++ b/core/spec/models/spree/adjustable/adjuster/tax_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Spree::Adjustable::Adjuster::Tax, type: :model do
+ let(:order) { create :order_with_line_items, line_items_count: 1 }
+ let(:line_item) { order.line_items.first }
+
+ let(:subject) { Spree::Adjustable::AdjustmentsUpdater.new(line_item) }
+ let(:order_subject) { Spree::Adjustable::AdjustmentsUpdater.new(order) }
+
+ context 'taxes with promotions' do
+ let!(:tax_rate) do
+ create(:tax_rate, amount: 0.05)
+ end
+
+ let!(:promotion) do
+ Spree::Promotion.create(name: '$10 off')
+ end
+
+ let!(:promotion_action) do
+ calculator = Spree::Calculator::FlatRate.new(preferred_amount: 10)
+ Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: calculator,
+ promotion: promotion)
+ end
+
+ before do
+ line_item.price = 20
+ line_item.tax_category = tax_rate.tax_category
+ line_item.save
+ create(:adjustment, order: order, source: promotion_action, adjustable: line_item)
+ end
+
+ context 'tax included in price' do
+ before do
+ create(:adjustment,
+ source: tax_rate,
+ adjustable: line_item,
+ order: order,
+ included: true)
+ end
+
+ it 'tax has no bearing on final price' do
+ subject.update
+ line_item.reload
+ expect(line_item.included_tax_total).to eq(0.5)
+ expect(line_item.additional_tax_total).to eq(0)
+ expect(line_item.promo_total).to eq(-10)
+ expect(line_item.adjustment_total).to eq(-10)
+ end
+
+ it 'tax linked to order' do
+ order_subject.update
+ order.reload
+ expect(order.included_tax_total).to eq(0.5)
+ expect(order.additional_tax_total).to eq(0o0)
+ end
+ end
+
+ context 'tax excluded from price' do
+ before do
+ create(:adjustment,
+ source: tax_rate,
+ adjustable: line_item,
+ order: order,
+ included: false)
+ end
+
+ it 'tax applies to line item' do
+ subject.update
+ line_item.reload
+ # Taxable amount is: $20 (base) - $10 (promotion) = $10
+ # Tax rate is 5% (of $10).
+ expect(line_item.included_tax_total).to eq(0)
+ expect(line_item.additional_tax_total).to eq(0.5)
+ expect(line_item.promo_total).to eq(-10)
+ expect(line_item.adjustment_total).to eq(-9.5)
+ end
+
+ it 'tax linked to order' do
+ order_subject.update
+ order.reload
+ expect(order.included_tax_total).to eq(0)
+ expect(order.additional_tax_total).to eq(0.5)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/adjustable/adjustments_updater_spec.rb b/core/spec/models/spree/adjustable/adjustments_updater_spec.rb
new file mode 100644
index 00000000000..ddfb06c92c2
--- /dev/null
+++ b/core/spec/models/spree/adjustable/adjustments_updater_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+module Spree
+ module Adjustable
+ describe AdjustmentsUpdater do
+ let(:order) { create :order_with_line_items, line_items_count: 1 }
+ let(:line_item) { order.line_items.first }
+ let(:tax_rate) { create(:tax_rate, amount: 0.05) }
+
+ describe '#update' do
+ before do
+ create(:adjustment, order: order, source: tax_rate, adjustable: line_item)
+ end
+
+ context 'persisted object' do
+ let(:subject) { AdjustmentsUpdater.new(line_item) }
+
+ it 'updates all linked adjusters' do
+ line_item.price = 10
+ line_item.tax_category = tax_rate.tax_category
+
+ subject.update
+ expect(line_item.adjustment_total).to eq(0.5)
+ expect(line_item.additional_tax_total).to eq(0.5)
+ end
+ end
+
+ context 'non-persisted object' do
+ let(:new_line_item) { order.line_items.new }
+ let(:subject) { AdjustmentsUpdater.new(new_line_item) }
+
+ it 'does nothing' do
+ expect { subject.update }.not_to change(new_line_item, :adjustment_total)
+ end
+ end
+
+ context 'nil' do
+ let(:subject) { AdjustmentsUpdater.new(nil) }
+
+ it 'does not raise an error' do
+ expect { subject.update }.not_to raise_error
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/adjustment_spec.rb b/core/spec/models/spree/adjustment_spec.rb
new file mode 100644
index 00000000000..45ba8bf0b91
--- /dev/null
+++ b/core/spec/models/spree/adjustment_spec.rb
@@ -0,0 +1,194 @@
+require 'spec_helper'
+
+describe Spree::Adjustment, type: :model do
+ let(:order) { Spree::Order.new }
+ let(:adjustment) { Spree::Adjustment.create!(label: 'Adjustment', adjustable: order, order: order, amount: 5) }
+
+ before do
+ allow(order).to receive(:update_with_updater!)
+ end
+
+ describe '#amount=' do
+ let(:amount) { '1,599,99' }
+
+ before { adjustment.amount = amount }
+
+ it 'is expected to equal to localized number' do
+ expect(adjustment.amount).to eq(Spree::LocalizedNumber.parse(amount))
+ end
+ end
+
+ describe 'scopes' do
+ describe '.for_complete_order' do
+ subject { Spree::Adjustment.for_complete_order }
+
+ let(:complete_order) { Spree::Order.create! completed_at: Time.current }
+ let(:incomplete_order) { Spree::Order.create! completed_at: nil }
+ let(:adjustment_for_complete_order) { Spree::Adjustment.create!(label: 'Adjustment', adjustable: complete_order, order: complete_order, amount: 5) }
+ let(:adjustment_for_incomplete_order) { Spree::Adjustment.create!(label: 'Adjustment', adjustable: incomplete_order, order: incomplete_order, amount: 5) }
+
+ it { is_expected.to include(adjustment_for_complete_order) }
+ it { is_expected.not_to include(adjustment_for_incomplete_order) }
+ end
+
+ describe '.for_incomplete_order' do
+ subject { Spree::Adjustment.for_incomplete_order }
+
+ let(:complete_order) { Spree::Order.create! completed_at: Time.current }
+ let(:incomplete_order) { Spree::Order.create! completed_at: nil }
+ let(:adjustment_for_complete_order) { Spree::Adjustment.create!(label: 'Adjustment', adjustable: complete_order, order: complete_order, amount: 5) }
+ let(:adjustment_for_incomplete_order) { Spree::Adjustment.create!(label: 'Adjustment', adjustable: incomplete_order, order: incomplete_order, amount: 5) }
+
+ it { is_expected.not_to include(adjustment_for_complete_order) }
+ it { is_expected.to include(adjustment_for_incomplete_order) }
+ end
+ end
+
+ context '#create & #destroy' do
+ let(:adjustment) { Spree::Adjustment.new(label: 'Adjustment', amount: 5, order: order, adjustable: create(:line_item)) }
+
+ it 'calls #update_adjustable_adjustment_total' do
+ expect(adjustment).to receive(:update_adjustable_adjustment_total).twice
+ adjustment.save
+ adjustment.destroy
+ end
+ end
+
+ context '#save' do
+ let(:order) { Spree::Order.create! }
+ let!(:adjustment) { Spree::Adjustment.create(label: 'Adjustment', amount: 5, order: order, adjustable: order) }
+
+ it 'touches the adjustable' do
+ expect(adjustment.adjustable).to receive(:touch)
+ adjustment.amount = 3
+ adjustment.save
+ end
+ end
+
+ describe 'non_tax scope' do
+ subject do
+ Spree::Adjustment.non_tax.to_a
+ end
+
+ let!(:tax_adjustment) { create(:adjustment, order: order, source: create(:tax_rate)) }
+ let!(:non_tax_adjustment_with_source) { create(:adjustment, order: order, source_type: 'Spree::Order', source_id: nil) }
+ let!(:non_tax_adjustment_without_source) { create(:adjustment, order: order, source: nil) }
+
+ it 'select non-tax adjustments' do
+ expect(subject).not_to include tax_adjustment
+ expect(subject).to include non_tax_adjustment_with_source
+ expect(subject).to include non_tax_adjustment_without_source
+ end
+ end
+
+ describe 'competing_promos scope' do
+ subject do
+ Spree::Adjustment.competing_promos.to_a
+ end
+
+ before do
+ allow_any_instance_of(Spree::Adjustment).to receive(:update_adjustable_adjustment_total).and_return(true)
+ end
+
+ let!(:promotion_adjustment) { create(:adjustment, order: order, source_type: 'Spree::PromotionAction', source_id: nil) }
+ let!(:custom_adjustment_with_source) { create(:adjustment, order: order, source_type: 'Custom', source_id: nil) }
+ let!(:non_promotion_adjustment_with_source) { create(:adjustment, order: order, source_type: 'Spree::Order', source_id: nil) }
+ let!(:non_promotion_adjustment_without_source) { create(:adjustment, order: order, source: nil) }
+
+ context 'no custom source_types have been added to competing_promos' do
+ before { Spree::Adjustment.competing_promos_source_types = ['Spree::PromotionAction'] }
+
+ it 'selects promotion adjustments by default' do
+ expect(subject).to include promotion_adjustment
+ expect(subject).not_to include custom_adjustment_with_source
+ expect(subject).not_to include non_promotion_adjustment_with_source
+ expect(subject).not_to include non_promotion_adjustment_without_source
+ end
+ end
+
+ context 'a custom source_type has been added to competing_promos' do
+ before { Spree::Adjustment.competing_promos_source_types = ['Spree::PromotionAction', 'Custom'] }
+
+ it 'selects adjustments with registered source_types' do
+ expect(subject).to include promotion_adjustment
+ expect(subject).to include custom_adjustment_with_source
+ expect(subject).not_to include non_promotion_adjustment_with_source
+ expect(subject).not_to include non_promotion_adjustment_without_source
+ end
+ end
+ end
+
+ context 'adjustment state' do
+ let(:adjustment) { create(:adjustment, order: order, state: 'open') }
+
+ context '#closed?' do
+ it 'is true when adjustment state is closed' do
+ adjustment.state = 'closed'
+ expect(adjustment).to be_closed
+ end
+
+ it 'is false when adjustment state is open' do
+ adjustment.state = 'open'
+ expect(adjustment).not_to be_closed
+ end
+ end
+ end
+
+ context '#currency' do
+ let(:order) { Spree::Order.new(currency: 'EUR') }
+
+ it 'returns the order currency' do
+ expect(adjustment.currency).to eq 'EUR'
+ end
+ end
+
+ context '#display_amount' do
+ before { adjustment.amount = 10.55 }
+
+ it 'shows the amount' do
+ expect(adjustment.display_amount.to_s).to eq '$10.55'
+ end
+
+ context 'with currency set to JPY' do
+ context 'when adjustable is set to an order' do
+ before do
+ expect(order).to receive(:currency).and_return('JPY')
+ adjustment.adjustable = order
+ end
+
+ it 'displays in JPY' do
+ expect(adjustment.display_amount.to_s).to eq 'Â¥11'
+ end
+ end
+
+ context 'when adjustable is nil' do
+ it 'displays in the default currency' do
+ expect(adjustment.display_amount.to_s).to eq '$10.55'
+ end
+ end
+ end
+ end
+
+ context '#update!' do
+ context 'when adjustment is closed' do
+ before { expect(adjustment).to receive(:closed?).and_return(true) }
+
+ it 'does not update the adjustment' do
+ expect(adjustment).not_to receive(:update_column)
+ adjustment.update!
+ end
+ end
+
+ context 'when adjustment is open' do
+ before { expect(adjustment).to receive(:closed?).and_return(false) }
+
+ it 'updates the amount' do
+ expect(adjustment).to receive(:adjustable).and_return(double('Adjustable')).at_least(:once)
+ expect(adjustment).to receive(:source).and_return(double('Source')).at_least(:once)
+ expect(adjustment.source).to receive('compute_amount').with(adjustment.adjustable).and_return(5)
+ expect(adjustment).to receive(:update_columns).with(amount: 5, updated_at: kind_of(Time))
+ adjustment.update!
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/app_configuration_spec.rb b/core/spec/models/spree/app_configuration_spec.rb
new file mode 100644
index 00000000000..19dbc90527b
--- /dev/null
+++ b/core/spec/models/spree/app_configuration_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Spree::AppConfiguration, type: :model do
+ let (:prefs) { Rails.application.config.spree.preferences }
+
+ it 'is available from the environment' do
+ prefs.layout = 'my/layout'
+ expect(prefs.layout).to eq 'my/layout'
+ end
+
+ it 'is available as Spree::Config for legacy access' do
+ Spree::Config.layout = 'my/layout'
+ expect(Spree::Config.layout).to eq 'my/layout'
+ end
+
+ it 'uses base searcher class by default' do
+ prefs.searcher_class = nil
+ expect(prefs.searcher_class).to eq Spree::Core::Search::Base
+ end
+
+ describe 'admin_path' do
+ it { expect(Spree::Config).to have_preference(:admin_path) }
+ it { expect(Spree::Config.preferred_admin_path_type).to eq(:string) }
+ it { expect(Spree::Config.preferred_admin_path_default).to eq('/admin') }
+ end
+end
diff --git a/core/spec/models/spree/app_dependencies_spec.rb b/core/spec/models/spree/app_dependencies_spec.rb
new file mode 100644
index 00000000000..480cfdb49c5
--- /dev/null
+++ b/core/spec/models/spree/app_dependencies_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+class MyCustomCreateService
+end
+
+describe Spree::AppDependencies, type: :model do
+ let (:deps) { Spree::AppDependencies.new }
+
+ it 'returns the default value' do
+ expect(deps.cart_create_service).to eq('Spree::Cart::Create')
+ end
+
+ it 'allows to overwrite the value' do
+ deps.cart_create_service = MyCustomCreateService
+ expect(deps.cart_create_service).to eq MyCustomCreateService
+ end
+end
diff --git a/core/spec/models/spree/asset_spec.rb b/core/spec/models/spree/asset_spec.rb
new file mode 100644
index 00000000000..26ccfc299e8
--- /dev/null
+++ b/core/spec/models/spree/asset_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Spree::Asset, type: :model do
+ describe '#viewable' do
+ it 'touches association' do
+ Timecop.scale(3600) do
+ product = create(:custom_product)
+ asset = Spree::Asset.create! { |a| a.viewable = product.master }
+
+ expect do
+ asset.touch
+ end.to change { product.reload.updated_at }
+ end
+ end
+ end
+
+ describe '#acts_as_list scope' do
+ it 'starts from first position for different viewables' do
+ asset1 = Spree::Asset.create(viewable_type: 'Spree::Image', viewable_id: 1)
+ asset2 = Spree::Asset.create(viewable_type: 'Spree::LineItem', viewable_id: 1)
+
+ expect(asset1.position).to eq 1
+ expect(asset2.position).to eq 1
+ end
+ end
+end
diff --git a/core/spec/models/spree/base_spec.rb b/core/spec/models/spree/base_spec.rb
new file mode 100644
index 00000000000..152aef9d5e9
--- /dev/null
+++ b/core/spec/models/spree/base_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+module Test
+ class Parent < ActiveRecord::Base
+ self.table_name = 'test_parents'
+ end
+
+ class Child < ActiveRecord::Base
+ self.table_name = 'test_children'
+ belongs_to :parent, class_name: 'Test::Parent'
+ end
+end
+
+describe Spree::Base do
+ let(:connection) { ActiveRecord::Base.connection }
+
+ before do
+ connection.create_table :test_parents, force: true
+ connection.create_table :test_children, force: true do |t|
+ t.belongs_to :test_parent
+ end
+ end
+
+ after do
+ connection.drop_table 'test_parents', if_exists: true
+ connection.drop_table 'test_children', if_exists: true
+ end
+
+ it 'does not override Rails 5 default belongs_to_required_by_default' do
+ expect(described_class.belongs_to_required_by_default).to eq(false)
+ expect(Spree::Product.belongs_to_required_by_default).to be(false)
+
+ expect(ApplicationRecord.belongs_to_required_by_default).to be(true)
+ expect(ActiveRecord::Base.belongs_to_required_by_default).to be(true)
+ expect(Test::Parent.belongs_to_required_by_default).to be(true)
+ expect(Test::Child.belongs_to_required_by_default).to be(true)
+ end
+
+ it 'does not disable non-spree, Rails 5 models to validate their associated belongs_to model' do
+ model_instance = Test::Child.new
+
+ expect(model_instance.validate).to eq(false)
+ expect(model_instance.errors.messages).to include(:parent)
+ expect(model_instance.errors.messages[:parent]).to include('must exist')
+ end
+end
diff --git a/core/spec/models/spree/calculator/default_tax_spec.rb b/core/spec/models/spree/calculator/default_tax_spec.rb
new file mode 100644
index 00000000000..c7b4940e13a
--- /dev/null
+++ b/core/spec/models/spree/calculator/default_tax_spec.rb
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+describe Spree::Calculator::DefaultTax, type: :model do
+ let!(:country) { create(:country) }
+ let!(:zone) { create(:zone, name: 'Country Zone', default_tax: true, zone_members: []) }
+ let!(:tax_category) { create(:tax_category, tax_rates: []) }
+ let!(:rate) { create(:tax_rate, tax_category: tax_category, amount: 0.05, included_in_price: included_in_price) }
+ let(:included_in_price) { false }
+ let!(:calculator) { Spree::Calculator::DefaultTax.new(calculable: rate) }
+ let!(:order) { create(:order) }
+ let!(:line_item) { create(:line_item, price: 10, quantity: 3, tax_category: tax_category) }
+ let!(:shipment) { create(:shipment, cost: 15) }
+
+ context '#compute' do
+ context 'when given an order' do
+ let!(:line_item_1) { line_item }
+ let!(:line_item_2) { create(:line_item, price: 10, quantity: 3, tax_category: tax_category) }
+
+ before do
+ allow(order).to receive_messages line_items: [line_item_1, line_item_2]
+ end
+
+ context 'when no line items match the tax category' do
+ before do
+ line_item_1.tax_category = nil
+ line_item_2.tax_category = nil
+ end
+
+ it 'is 0' do
+ expect(calculator.compute(order)).to eq(0)
+ end
+ end
+
+ context 'when one item matches the tax category' do
+ before do
+ line_item_1.tax_category = tax_category
+ line_item_2.tax_category = nil
+ end
+
+ it 'is equal to the item total * rate' do
+ expect(calculator.compute(order)).to eq(1.5)
+ end
+
+ context 'correctly rounds to within two decimal places' do
+ before do
+ line_item_1.price = 10.333
+ line_item_1.quantity = 1
+ end
+
+ specify do
+ # Amount is 0.51665, which will be rounded to...
+ expect(calculator.compute(order)).to eq(0.52)
+ end
+ end
+ end
+
+ context 'when more than one item matches the tax category' do
+ it 'is equal to the sum of the item totals * rate' do
+ expect(calculator.compute(order)).to eq(3)
+ end
+ end
+
+ context 'when tax is included in price' do
+ let(:included_in_price) { true }
+
+ it 'will return the deducted amount from the totals' do
+ # total price including 5% tax = $60
+ # ex pre-tax = $57.14
+ # 57.14 + %5 = 59.997 (or "close enough" to $60)
+ # 60 - 57.14 = $2.86
+ expect(calculator.compute(order).to_f).to eq(2.86)
+ end
+ end
+ end
+
+ context 'when tax is included in price' do
+ let(:included_in_price) { true }
+
+ context 'when the variant matches the tax category' do
+ context 'when line item is discounted' do
+ before do
+ line_item.taxable_adjustment_total = -1
+ Spree::TaxRate.store_pre_tax_amount(line_item, [rate])
+ end
+
+ it "is equal to the item's discounted total * rate" do
+ expect(calculator.compute(line_item)).to eq(1.38)
+ end
+ end
+
+ it "is equal to the item's full price * rate" do
+ Spree::TaxRate.store_pre_tax_amount(line_item, [rate])
+ expect(calculator.compute(line_item)).to eq(1.43)
+ end
+ end
+ end
+
+ context 'when tax is not included in price' do
+ context 'when the line item is discounted' do
+ before { line_item.taxable_adjustment_total = -1 }
+
+ it "is equal to the item's pre-tax total * rate" do
+ expect(calculator.compute(line_item)).to eq(1.45)
+ end
+ end
+
+ context 'when the variant matches the tax category' do
+ it 'is equal to the item pre-tax total * rate' do
+ expect(calculator.compute(line_item)).to eq(1.50)
+ end
+ end
+ end
+
+ context 'when given a shipment' do
+ it 'is 5% of 15' do
+ expect(calculator.compute(shipment)).to eq(0.75)
+ end
+
+ it 'takes discounts into consideration' do
+ shipment.promo_total = -1
+ # 5% of 14
+ expect(calculator.compute(shipment)).to eq(0.7)
+ end
+ end
+ end
+
+ context 'when given a line_item' do
+ subject do
+ Spree::Calculator::DefaultTax.new(calculable: rate).
+ compute_line_item(line_item)
+ end
+
+ let(:rate) { create(:tax_rate, amount: 0.07, included_in_price: true) }
+ let(:line_item) { create(:line_item, quantity: 50, price: 8.50) }
+
+ describe '#compute_line_item' do
+ it 'computes the line item right' do
+ Spree::TaxRate.store_pre_tax_amount(line_item, [rate])
+ expect(subject).to eq(27.80)
+ end
+
+ context 'with a 40$ promo' do
+ before do
+ allow(line_item).to receive(:taxable_adjustment_total).and_return(BigDecimal('-40'))
+ Spree::TaxRate.store_pre_tax_amount(line_item, [rate])
+ end
+
+ it 'computes the line item right' do
+ expect(subject).to eq(25.19)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/flat_percent_item_total_spec.rb b/core/spec/models/spree/calculator/flat_percent_item_total_spec.rb
new file mode 100644
index 00000000000..6197c087aab
--- /dev/null
+++ b/core/spec/models/spree/calculator/flat_percent_item_total_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Spree::Calculator::FlatPercentItemTotal, type: :model do
+ let(:calculator) { Spree::Calculator::FlatPercentItemTotal.new }
+ let(:line_item) { mock_model Spree::LineItem }
+
+ before { allow(calculator).to receive_messages preferred_flat_percent: 10 }
+
+ context 'compute' do
+ it 'rounds result correctly' do
+ allow(line_item).to receive_messages amount: 31.08
+ expect(calculator.compute(line_item)).to eq 3.11
+
+ allow(line_item).to receive_messages amount: 31.00
+ expect(calculator.compute(line_item)).to eq 3.10
+ end
+
+ it 'returns object.amount if computed amount is greater' do
+ allow(calculator).to receive_messages preferred_flat_percent: 110
+ allow(line_item).to receive_messages amount: 30.00
+
+ expect(calculator.compute(line_item)).to eq 30.0
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/flat_rate_spec.rb b/core/spec/models/spree/calculator/flat_rate_spec.rb
new file mode 100644
index 00000000000..68d8935c879
--- /dev/null
+++ b/core/spec/models/spree/calculator/flat_rate_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Spree::Calculator::FlatRate, type: :model do
+ let(:calculator) { Spree::Calculator::FlatRate.new }
+
+ let(:order) do
+ mock_model(
+ Spree::Order, quantity: 10, currency: 'USD'
+ )
+ end
+
+ context 'compute' do
+ it "computes the amount as the rate when currency matches the order's currency" do
+ calculator.preferred_amount = 25.0
+ calculator.preferred_currency = 'GBP'
+ allow(order).to receive_messages currency: 'GBP'
+ expect(calculator.compute(order).round(2)).to eq(25.0)
+ end
+
+ it "computes the amount as 0 when currency does not match the order's currency" do
+ calculator.preferred_amount = 100.0
+ calculator.preferred_currency = 'GBP'
+ allow(order).to receive_messages currency: 'USD'
+ expect(calculator.compute(order).round(2)).to eq(0.0)
+ end
+
+ it 'computes the amount as 0 when currency is blank' do
+ calculator.preferred_amount = 100.0
+ calculator.preferred_currency = ''
+ allow(order).to receive_messages currency: 'GBP'
+ expect(calculator.compute(order).round(2)).to eq(0.0)
+ end
+
+ it 'computes the amount as the rate when the currencies use different casing' do
+ calculator.preferred_amount = 100.0
+ calculator.preferred_currency = 'gBp'
+ allow(order).to receive_messages currency: 'GBP'
+ expect(calculator.compute(order).round(2)).to eq(100.0)
+ end
+
+ it 'computes the amount as 0 when there is no object' do
+ calculator.preferred_amount = 100.0
+ calculator.preferred_currency = 'GBP'
+ expect(calculator.compute.round(2)).to eq(0.0)
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/flexi_rate_spec.rb b/core/spec/models/spree/calculator/flexi_rate_spec.rb
new file mode 100644
index 00000000000..8c53c00fe03
--- /dev/null
+++ b/core/spec/models/spree/calculator/flexi_rate_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Spree::Calculator::FlexiRate, type: :model do
+ let(:calculator) { Spree::Calculator::FlexiRate.new }
+
+ let(:order) do
+ mock_model(
+ Spree::Order, quantity: 10
+ )
+ end
+
+ context 'compute' do
+ it 'computes amount correctly when all fees are 0' do
+ expect(calculator.compute(order).round(2)).to eq(0.0)
+ end
+
+ it 'computes amount correctly when first_item has a value' do
+ allow(calculator).to receive_messages preferred_first_item: 1.0
+ expect(calculator.compute(order).round(2)).to eq(1.0)
+ end
+
+ it 'computes amount correctly when additional_items has a value' do
+ allow(calculator).to receive_messages preferred_additional_item: 1.0
+ expect(calculator.compute(order).round(2)).to eq(9.0)
+ end
+
+ it 'computes amount correctly when additional_items and first_item have values' do
+ allow(calculator).to receive_messages preferred_first_item: 5.0, preferred_additional_item: 1.0
+ expect(calculator.compute(order).round(2)).to eq(14.0)
+ end
+
+ it 'computes amount correctly when additional_items and first_item have values AND max items has value' do
+ allow(calculator).to receive_messages preferred_first_item: 5.0, preferred_additional_item: 1.0, preferred_max_items: 3
+ expect(calculator.compute(order).round(2)).to eq(7.0)
+ end
+
+ it 'allows creation of new object with all the attributes' do
+ Spree::Calculator::FlexiRate.new(preferred_first_item: 1, preferred_additional_item: 1, preferred_max_items: 1)
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/percent_on_line_item_spec.rb b/core/spec/models/spree/calculator/percent_on_line_item_spec.rb
new file mode 100644
index 00000000000..5cc666763cf
--- /dev/null
+++ b/core/spec/models/spree/calculator/percent_on_line_item_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+module Spree
+ class Calculator
+ describe PercentOnLineItem, type: :model do
+ let(:line_item) { double('LineItem', amount: 100) }
+
+ before { subject.preferred_percent = 15 }
+
+ it 'computes based on item price and quantity' do
+ expect(subject.compute(line_item)).to eq 15
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/price_sack_spec.rb b/core/spec/models/spree/calculator/price_sack_spec.rb
new file mode 100644
index 00000000000..e1dd02e1b26
--- /dev/null
+++ b/core/spec/models/spree/calculator/price_sack_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Spree::Calculator::PriceSack, type: :model do
+ let(:calculator) do
+ calculator = Spree::Calculator::PriceSack.new
+ calculator.preferred_minimal_amount = 5
+ calculator.preferred_normal_amount = 10
+ calculator.preferred_discount_amount = 1
+ calculator
+ end
+
+ let(:order) { stub_model(Spree::Order) }
+ let(:shipment) { stub_model(Spree::Shipment, amount: 10) }
+
+ # Regression test for #714 and #739
+ it 'computes with an order object' do
+ calculator.compute(order)
+ end
+
+ # Regression test for #1156
+ it 'computes with a shipment object' do
+ calculator.compute(shipment)
+ end
+
+ # Regression test for #2055
+ it 'computes the correct amount' do
+ expect(calculator.compute(2)).to eq(calculator.preferred_normal_amount)
+ expect(calculator.compute(6)).to eq(calculator.preferred_discount_amount)
+ end
+end
diff --git a/core/spec/models/spree/calculator/refunds/default_refund_amount_spec.rb b/core/spec/models/spree/calculator/refunds/default_refund_amount_spec.rb
new file mode 100644
index 00000000000..323e8cb542f
--- /dev/null
+++ b/core/spec/models/spree/calculator/refunds/default_refund_amount_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Spree::Calculator::Returns::DefaultRefundAmount, type: :model do
+ subject { calculator.compute(return_item) }
+
+ let(:order) { create(:order) }
+ let(:line_item_quantity) { 2 }
+ let(:item_price) { 100.0 }
+ let(:pre_tax_amount) { line_item_quantity * item_price }
+ let(:line_item) { create(:line_item, price: item_price, quantity: line_item_quantity) }
+ let(:inventory_unit) { build(:inventory_unit, order: order, line_item: line_item, quantity: 1) }
+ let(:return_item) { build(:return_item, inventory_unit: inventory_unit) }
+ let(:calculator) { Spree::Calculator::Returns::DefaultRefundAmount.new }
+
+ before { order.line_items << line_item }
+
+ context 'not an exchange' do
+ context 'no promotions or taxes' do
+ it { is_expected.to eq pre_tax_amount / line_item_quantity }
+ end
+
+ context 'order adjustments' do
+ let(:adjustment_amount) { -10.0 }
+
+ before do
+ order.adjustments << create(:adjustment, order: order, amount: adjustment_amount, eligible: true, label: 'Adjustment', source_type: 'Spree::Order')
+ order.adjustments.first.update_attributes(amount: adjustment_amount)
+ end
+
+ it { is_expected.to eq (pre_tax_amount - adjustment_amount.abs) / line_item_quantity }
+ end
+
+ context 'shipping adjustments' do
+ let(:adjustment_total) { -50.0 }
+
+ before { order.shipments << Spree::Shipment.new(adjustment_total: adjustment_total) }
+
+ it { is_expected.to eq pre_tax_amount / line_item_quantity }
+ end
+ end
+
+ context 'an exchange' do
+ let(:return_item) { build(:exchange_return_item) }
+
+ it { is_expected.to eq 0.0 }
+ end
+end
diff --git a/core/spec/models/spree/calculator/shipping.rb b/core/spec/models/spree/calculator/shipping.rb
new file mode 100644
index 00000000000..71c3c0f7560
--- /dev/null
+++ b/core/spec/models/spree/calculator/shipping.rb
@@ -0,0 +1,7 @@
+require_dependency 'spree/calculator'
+
+module Spree
+ class Calculator::Shipping < Calculator
+ def compute(content_items); end
+ end
+end
diff --git a/core/spec/models/spree/calculator/shipping/flat_percent_item_total_spec.rb b/core/spec/models/spree/calculator/shipping/flat_percent_item_total_spec.rb
new file mode 100644
index 00000000000..9655767a089
--- /dev/null
+++ b/core/spec/models/spree/calculator/shipping/flat_percent_item_total_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+module Spree
+ module Calculator::Shipping
+ describe FlatPercentItemTotal, type: :model do
+ subject { FlatPercentItemTotal.new(preferred_flat_percent: 10) }
+
+ let(:variant1) { build(:variant, price: 10.11) }
+ let(:variant2) { build(:variant, price: 20.2222) }
+
+ let(:line_item1) { build(:line_item, variant: variant1) }
+ let(:line_item2) { build(:line_item, variant: variant2) }
+
+ let(:package) do
+ build(:stock_package, variants_contents: { variant1 => 2, variant2 => 1 })
+ end
+
+ it 'rounds result correctly' do
+ expect(subject.compute(package)).to eq(4.04)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/shipping/flat_rate_spec.rb b/core/spec/models/spree/calculator/shipping/flat_rate_spec.rb
new file mode 100644
index 00000000000..2dbb55b3752
--- /dev/null
+++ b/core/spec/models/spree/calculator/shipping/flat_rate_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+module Spree
+ module Calculator::Shipping
+ describe FlatRate, type: :model do
+ subject { Calculator::Shipping::FlatRate.new(preferred_amount: 4.00) }
+
+ it 'always returns the same rate' do
+ expect(subject.compute(build(:stock_package_fulfilled))).to eq(4.00)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/shipping/flexi_rate_spec.rb b/core/spec/models/spree/calculator/shipping/flexi_rate_spec.rb
new file mode 100644
index 00000000000..4d5635155d0
--- /dev/null
+++ b/core/spec/models/spree/calculator/shipping/flexi_rate_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+module Spree
+ module Calculator::Shipping
+ describe FlexiRate, type: :model do
+ let(:variant1) { build(:variant, price: 10) }
+ let(:variant2) { build(:variant, price: 20) }
+
+ let(:package) do
+ build(:stock_package, variants_contents: { variant1 => 4, variant2 => 6 })
+ end
+
+ let(:subject) { FlexiRate.new }
+
+ context 'compute' do
+ it 'computes amount correctly when all fees are 0' do
+ expect(subject.compute(package).round(2)).to eq(0.0)
+ end
+
+ it 'computes amount correctly when first_item has a value' do
+ subject.preferred_first_item = 1.0
+ expect(subject.compute(package).round(2)).to eq(1.0)
+ end
+
+ it 'computes amount correctly when additional_items has a value' do
+ subject.preferred_additional_item = 1.0
+ expect(subject.compute(package).round(2)).to eq(9.0)
+ end
+
+ it 'computes amount correctly when additional_items and first_item have values' do
+ subject.preferred_first_item = 5.0
+ subject.preferred_additional_item = 1.0
+ expect(subject.compute(package).round(2)).to eq(14.0)
+ end
+
+ it 'computes amount correctly when additional_items and first_item have values AND max items has value' do
+ subject.preferred_first_item = 5.0
+ subject.preferred_additional_item = 1.0
+ subject.preferred_max_items = 3
+ expect(subject.compute(package).round(2)).to eq(7.0)
+ end
+
+ it 'allows creation of new object with all the attributes' do
+ FlexiRate.new(
+ preferred_first_item: 1,
+ preferred_additional_item: 1,
+ preferred_max_items: 1
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/shipping/per_item_spec.rb b/core/spec/models/spree/calculator/shipping/per_item_spec.rb
new file mode 100644
index 00000000000..c5a40398deb
--- /dev/null
+++ b/core/spec/models/spree/calculator/shipping/per_item_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+module Spree
+ module Calculator::Shipping
+ describe PerItem, type: :model do
+ subject { PerItem.new(preferred_amount: 10) }
+
+ let(:variant1) { build(:variant) }
+ let(:variant2) { build(:variant) }
+
+ let(:package) do
+ build(:stock_package, variants_contents: { variant1 => 5, variant2 => 3 })
+ end
+
+ it 'correctly calculates per item shipping' do
+ expect(subject.compute(package).to_f).to eq(80) # 5 x 10 + 3 x 10
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/shipping/price_sack_spec.rb b/core/spec/models/spree/calculator/shipping/price_sack_spec.rb
new file mode 100644
index 00000000000..2038e403a82
--- /dev/null
+++ b/core/spec/models/spree/calculator/shipping/price_sack_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+module Spree
+ module Calculator::Shipping
+ describe PriceSack do
+ subject(:calculator) do
+ calculator = PriceSack.new
+ calculator.preferred_minimal_amount = 5
+ calculator.preferred_normal_amount = 10
+ calculator.preferred_discount_amount = 1
+ calculator
+ end
+
+ let(:variant) { build(:variant, price: 2) }
+
+ let(:normal_package) do
+ build(:stock_package, variants_contents: { variant => 2 })
+ end
+
+ let(:discount_package) do
+ build(:stock_package, variants_contents: { variant => 4 })
+ end
+
+ it 'computes the correct amount' do
+ expect(calculator.compute(normal_package)).to eq(calculator.preferred_normal_amount)
+ expect(calculator.compute(discount_package)).to eq(calculator.preferred_discount_amount)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/tiered_flat_rate_spec.rb b/core/spec/models/spree/calculator/tiered_flat_rate_spec.rb
new file mode 100644
index 00000000000..99047eb6045
--- /dev/null
+++ b/core/spec/models/spree/calculator/tiered_flat_rate_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Spree::Calculator::TieredFlatRate, type: :model do
+ let(:calculator) { Spree::Calculator::TieredFlatRate.new }
+
+ describe '#valid?' do
+ subject { calculator.valid? }
+
+ context 'when tiers is not a hash' do
+ before { calculator.preferred_tiers = ['nope', 0] }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when tiers is a hash' do
+ context 'and one of the keys is not a positive number' do
+ before { calculator.preferred_tiers = { 'nope' => 20 } }
+
+ it { is_expected.to be false }
+ end
+ end
+ end
+
+ describe '#compute' do
+ subject { calculator.compute(line_item) }
+
+ let(:line_item) { mock_model Spree::LineItem, amount: amount }
+
+ before do
+ calculator.preferred_base_amount = 10
+ calculator.preferred_tiers = {
+ 100 => 15,
+ 200 => 20
+ }
+ end
+
+ context 'when amount falls within the first tier' do
+ let(:amount) { 50 }
+
+ it { is_expected.to eq 10 }
+ end
+
+ context 'when amount falls within the second tier' do
+ let(:amount) { 150 }
+
+ it { is_expected.to eq 15 }
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator/tiered_percent_spec.rb b/core/spec/models/spree/calculator/tiered_percent_spec.rb
new file mode 100644
index 00000000000..7e430fbdb23
--- /dev/null
+++ b/core/spec/models/spree/calculator/tiered_percent_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Spree::Calculator::TieredPercent, type: :model do
+ let(:calculator) { Spree::Calculator::TieredPercent.new }
+
+ describe '#valid?' do
+ subject { calculator.valid? }
+
+ context 'when base percent is less than zero' do
+ before { calculator.preferred_base_percent = -1 }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when base percent is greater than 100' do
+ before { calculator.preferred_base_percent = 110 }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when tiers is not a hash' do
+ before { calculator.preferred_tiers = ['nope', 0] }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when tiers is a hash' do
+ context 'and one of the keys is not a positive number' do
+ before { calculator.preferred_tiers = { 'nope' => 20 } }
+
+ it { is_expected.to be false }
+ end
+
+ context 'and one of the values is not a percent' do
+ before { calculator.preferred_tiers = { 10 => 110 } }
+
+ it { is_expected.to be false }
+ end
+ end
+ end
+
+ describe '#compute' do
+ subject { calculator.compute(line_item) }
+
+ let(:line_item) { mock_model Spree::LineItem, amount: amount }
+
+ before do
+ calculator.preferred_base_percent = 10
+ calculator.preferred_tiers = {
+ 100 => 15,
+ 200 => 20
+ }
+ end
+
+ context 'when amount falls within the first tier' do
+ let(:amount) { 50 }
+
+ it { is_expected.to eq 5 }
+ end
+
+ context 'when amount falls within the second tier' do
+ let(:amount) { 150 }
+
+ it { is_expected.to eq 22 }
+ end
+ end
+end
diff --git a/core/spec/models/spree/calculator_spec.rb b/core/spec/models/spree/calculator_spec.rb
new file mode 100644
index 00000000000..f008b16c97f
--- /dev/null
+++ b/core/spec/models/spree/calculator_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Spree::Calculator, type: :model do
+ let(:order) { create(:order) }
+ let!(:line_item) { create(:line_item, order: order) }
+ let(:shipment) { create(:shipment, order: order, stock_location: create(:stock_location_with_items)) }
+
+ context 'with computable' do
+ context 'and compute methods stubbed out' do
+ context 'with a Spree::LineItem' do
+ it 'calls compute_line_item' do
+ expect(subject).to receive(:compute_line_item).with(line_item)
+ subject.compute(line_item)
+ end
+ end
+
+ context 'with a Spree::Order' do
+ it 'calls compute_order' do
+ expect(subject).to receive(:compute_order).with(order)
+ subject.compute(order)
+ end
+ end
+
+ context 'with a Spree::Shipment' do
+ it 'calls compute_shipment' do
+ expect(subject).to receive(:compute_shipment).with(shipment)
+ subject.compute(shipment)
+ end
+ end
+
+ context 'with a arbitray object' do
+ it 'calls the correct compute' do
+ s = 'Calculator can all'
+ expect(subject).to receive(:compute_string).with(s)
+ subject.compute(s)
+ end
+ end
+ end
+
+ context 'with no stubbing' do
+ context 'with a Spree::LineItem' do
+ it 'raises NotImplementedError' do
+ expect { subject.compute(line_item) }.to raise_error NotImplementedError, /Please implement \'compute_line_item\(line_item\)\' in your calculator/
+ end
+ end
+
+ context 'with a Spree::Order' do
+ it 'raises NotImplementedError' do
+ expect { subject.compute(order) }.to raise_error NotImplementedError, /Please implement \'compute_order\(order\)\' in your calculator/
+ end
+ end
+
+ context 'with a Spree::Shipment' do
+ it 'raises NotImplementedError' do
+ expect { subject.compute(shipment) }.to raise_error NotImplementedError, /Please implement \'compute_shipment\(shipment\)\' in your calculator/
+ end
+ end
+
+ context 'with a arbitray object' do
+ it 'raises NotImplementedError' do
+ s = 'Calculator can all'
+ expect { subject.compute(s) }.to raise_error NotImplementedError, /Please implement \'compute_string\(string\)\' in your calculator/
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/classification_spec.rb b/core/spec/models/spree/classification_spec.rb
new file mode 100644
index 00000000000..22f8a40831f
--- /dev/null
+++ b/core/spec/models/spree/classification_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+module Spree
+ describe Classification, type: :model do
+ # Regression test for #3494
+ let(:taxon_with_5_products) do
+ products = []
+ 5.times do
+ products << create(:base_product)
+ end
+
+ create(:taxon, products: products)
+ end
+
+ it 'cannot link the same taxon to the same product more than once' do
+ product = create(:product)
+ taxon = create(:taxon)
+ add_taxon = -> { product.taxons << taxon }
+ expect(add_taxon).not_to raise_error
+ expect(add_taxon).to raise_error(ActiveRecord::RecordInvalid)
+ end
+
+ def positions_to_be_valid(taxon)
+ positions = taxon.reload.classifications.map(&:position)
+ expect(positions).to eq((1..taxon.classifications.count).to_a)
+ end
+
+ it 'has a valid fixtures' do
+ expect positions_to_be_valid(taxon_with_5_products)
+ expect(Spree::Classification.count).to eq 5
+ end
+
+ context 'removing product from taxon' do
+ before do
+ p = taxon_with_5_products.products[1]
+ expect(p.classifications.first.position).to eq(2)
+ taxon_with_5_products.products.destroy(p)
+ end
+
+ it 'resets positions' do
+ expect positions_to_be_valid(taxon_with_5_products)
+ end
+ end
+
+ context "replacing taxon's products" do
+ before do
+ products = taxon_with_5_products.products.to_a
+ products.pop(1)
+ taxon_with_5_products.products = products
+ taxon_with_5_products.save!
+ end
+
+ it 'resets positions' do
+ expect positions_to_be_valid(taxon_with_5_products)
+ end
+ end
+
+ context 'removing taxon from product' do
+ before do
+ p = taxon_with_5_products.products[1]
+ p.taxons.destroy(taxon_with_5_products)
+ p.save!
+ end
+
+ it 'resets positions' do
+ expect positions_to_be_valid(taxon_with_5_products)
+ end
+ end
+
+ context "replacing product's taxons" do
+ before do
+ p = taxon_with_5_products.products[1]
+ p.taxons = []
+ p.save!
+ end
+
+ it 'resets positions' do
+ expect positions_to_be_valid(taxon_with_5_products)
+ end
+ end
+
+ context 'destroying classification' do
+ before do
+ classification = taxon_with_5_products.classifications[1]
+ classification.destroy
+ end
+
+ it 'resets positions' do
+ expect positions_to_be_valid(taxon_with_5_products)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/concerns/display_money_spec.rb b/core/spec/models/spree/concerns/display_money_spec.rb
new file mode 100644
index 00000000000..3ba191e2db4
--- /dev/null
+++ b/core/spec/models/spree/concerns/display_money_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+module Spree
+ describe DisplayMoney do
+ let(:test_class) do
+ Class.new do
+ extend DisplayMoney
+ def total
+ 10.0
+ end
+ end
+ end
+
+ describe '.money_methods' do
+ before { test_class.money_methods :total }
+
+ context 'currency is not defined' do
+ it 'generates a display_method that builds a Spree::Money without options' do
+ expect(test_class.new.display_total).to eq Spree::Money.new(10.0)
+ end
+ end
+
+ context 'currency is defined' do
+ before do
+ test_class.class_eval do
+ def currency
+ 'USD'
+ end
+ end
+ end
+
+ it 'generates a display_* method that builds a Spree::Money with currency' do
+ expect(test_class.new.display_total).to eq Spree::Money.new(10.0, currency: 'USD')
+ end
+ end
+
+ context 'with multiple + options' do
+ before do
+ test_class.class_eval do
+ def amount
+ 20.0
+ end
+ end
+ test_class.money_methods :total, amount: { no_cents: true }
+ end
+
+ it 'generates a display_* method that builds a Spree::Money with the specified options' do
+ expect(test_class.new.display_total).to eq Spree::Money.new(10.0)
+ expect(test_class.new.display_amount).to eq Spree::Money.new(20.0, no_cents: true)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/concerns/user_methods_spec.rb b/core/spec/models/spree/concerns/user_methods_spec.rb
new file mode 100644
index 00000000000..595ea202c48
--- /dev/null
+++ b/core/spec/models/spree/concerns/user_methods_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe Spree::UserMethods do
+ let(:test_user) { create :user }
+ let(:current_store) { create :store }
+
+ describe '#has_spree_role?' do
+ subject { test_user.has_spree_role? name }
+
+ let(:role) { Spree::Role.create(name: name) }
+ let(:name) { 'test' }
+
+ context 'with a role' do
+ before { test_user.spree_roles << role }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'without a role' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#last_incomplete_spree_order' do
+ subject { test_user.last_incomplete_spree_order(current_store) }
+
+ context 'with an incomplete order' do
+ let(:last_incomplete_order) { create :order, user: test_user, store: current_store }
+
+ before do
+ create(:order, user: test_user, created_at: 1.day.ago, store: current_store)
+ create(:order, user: create(:user), store: current_store)
+ last_incomplete_order
+ end
+
+ it { is_expected.to eq last_incomplete_order }
+ end
+
+ context 'without an incomplete order' do
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context '#check_completed_orders' do
+ let(:possible_promotion) { create(:promotion, advertise: true, starts_at: 1.day.ago) }
+
+ context 'rstrict t delete dependent destroyed' do
+ before do
+ test_user.promotion_rules.create!(promotion: possible_promotion)
+ create(:order, user: test_user, completed_at: Time.current)
+ end
+
+ it 'does not destroy dependent destroy items' do
+ expect { test_user.destroy }.to raise_error(Spree::Core::DestroyWithOrdersError)
+ expect(test_user.promotion_rule_users.any?).to be(true)
+ end
+ end
+
+ context 'allow to destroy dependent destroy' do
+ before do
+ test_user.promotion_rules.create!(promotion: possible_promotion)
+ create(:order, user: test_user, created_at: 1.day.ago)
+ test_user.destroy
+ end
+
+ it 'does not destroy dependent destroy items' do
+ expect(test_user.promotion_rule_users.any?).to be(false)
+ end
+ end
+ end
+
+ context 'when user destroyed with approved orders' do
+ let(:order) { create(:order, approver_id: test_user.id, created_at: 1.day.ago) }
+
+ it 'nullifies all approver ids' do
+ expect(test_user).to receive(:nullify_approver_id_in_approved_orders)
+ test_user.destroy
+ end
+ end
+end
diff --git a/core/spec/models/spree/concerns/vat_price_calculation_spec.rb b/core/spec/models/spree/concerns/vat_price_calculation_spec.rb
new file mode 100644
index 00000000000..5ae32b91f81
--- /dev/null
+++ b/core/spec/models/spree/concerns/vat_price_calculation_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+module Spree
+ describe VatPriceCalculation do
+ let(:test_class) do
+ Class.new do
+ include VatPriceCalculation
+ def total
+ 10.0
+ end
+ end
+ end
+
+ describe '#gross_amount' do
+ subject(:gross_amount) { test_class.new.gross_amount(amount, price_options) }
+
+ let(:zone) { Zone.new }
+ let(:tax_category) { TaxCategory.new }
+ let(:price_options) do
+ {
+ tax_zone: zone,
+ tax_category: tax_category
+ }
+ end
+ let(:amount) { 100 }
+
+ context 'with no default zone set' do
+ it 'does not call TaxRate.included_tax_amount_for' do
+ expect(TaxRate).not_to receive(:included_tax_amount_for)
+ gross_amount
+ end
+ end
+
+ context 'with no zone given' do
+ let(:zone) { nil }
+
+ it 'does not call TaxRate.included_tax_amount_for' do
+ expect(TaxRate).not_to receive(:included_tax_amount_for)
+ gross_amount
+ end
+ end
+
+ context 'with a default zone set' do
+ let(:default_zone) { Spree::Zone.new }
+
+ before do
+ allow(Spree::Zone).to receive(:default_tax).and_return(default_zone)
+ end
+
+ context 'and zone equal to the default zone' do
+ let(:zone) { default_zone }
+
+ it "does not call 'TaxRate.included_tax_amount_for'" do
+ expect(TaxRate).not_to receive(:included_tax_amount_for)
+ gross_amount
+ end
+ end
+
+ context 'and zone not equal to default zone' do
+ let(:zone) { Spree::Zone.new }
+
+ it 'calls TaxRate.included_tax_amount_for two times' do
+ expect(TaxRate).to receive(:included_tax_amount_for).twice
+ gross_amount
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/country_spec.rb b/core/spec/models/spree/country_spec.rb
new file mode 100644
index 00000000000..1a1c83ca4a5
--- /dev/null
+++ b/core/spec/models/spree/country_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Spree::Country, type: :model do
+ let(:america) { create :country }
+ let(:canada) { create :country, name: 'Canada', iso_name: 'CANADA', numcode: '124' }
+
+
+ describe '.by_iso' do
+ let(:dummy_iso) { 'XY' }
+
+ it 'will return Country by iso' do
+ expect(described_class.by_iso(america.iso)).to eq america
+ end
+
+ it 'will return Country by iso3' do
+ expect(described_class.by_iso(america.iso3)).to eq america
+ end
+
+ it 'will return nil with wrong iso or iso3' do
+ expect(described_class.by_iso(dummy_iso)).to eq nil
+ end
+
+ it 'will return Country by lower iso' do
+ expect(described_class.by_iso(america.iso.downcase)).to eq america
+ end
+ end
+
+ describe '.default' do
+ context 'when default_country_id config is set' do
+ before { Spree::Config[:default_country_id] = canada.id }
+
+ it 'will return the country from the config' do
+ expect(described_class.default.id).to eql canada.id
+ end
+ end
+
+ context 'config is not set though record for america exists' do
+ before { america.touch }
+
+ it 'will return the US' do
+ expect(described_class.default.id).to eql america.id
+ end
+ end
+
+ context 'when config is not set and america is not created' do
+ before { canada.touch }
+
+ it 'will return the first record' do
+ expect(described_class.default.id).to eql canada.id
+ end
+ end
+ end
+
+ describe 'ensure default country in not deleted' do
+ before { Spree::Config[:default_country_id] = america.id }
+
+ context 'will not destroy country if it is default' do
+ subject { america.destroy }
+
+ it { is_expected.to be_falsy }
+
+ context 'error should be default country cannot be deleted' do
+ before { subject }
+
+ it { expect(america.errors[:base]).to include(Spree.t(:default_country_cannot_be_deleted)) }
+ end
+ end
+
+ context 'will destroy if it is not a default' do
+ it { expect(canada.destroy).to be_truthy }
+ end
+ end
+
+ context '#default?' do
+ before { Spree::Config[:default_country_id] = america.id }
+
+ it 'returns true for default country' do
+ expect(america.default?).to eq(true)
+ end
+
+ it 'returns false for other countries' do
+ expect(canada.default?).to eq(false)
+ end
+ end
+end
diff --git a/core/spec/models/spree/credit_card_spec.rb b/core/spec/models/spree/credit_card_spec.rb
new file mode 100644
index 00000000000..e0e3ad5642f
--- /dev/null
+++ b/core/spec/models/spree/credit_card_spec.rb
@@ -0,0 +1,318 @@
+require 'spec_helper'
+
+describe Spree::CreditCard, type: :model do
+ let(:valid_credit_card_attributes) do
+ {
+ number: '4111111111111111',
+ verification_value: '123',
+ expiry: "12 / #{(Time.current.year + 1).to_s.last(2)}",
+ name: 'Spree Commerce'
+ }
+ end
+ let(:credit_card) { Spree::CreditCard.new }
+
+ def self.payment_states
+ Spree::Payment.state_machine.states.keys
+ end
+
+ before do
+ @order = create(:order)
+ @payment = Spree::Payment.create(amount: 100, order: @order)
+
+ @success_response = double('gateway_response', success?: true, authorization: '123', avs_result: { 'code' => 'avs-code' })
+ @fail_response = double('gateway_response', success?: false)
+
+ @payment_gateway = mock_model(Spree::PaymentMethod,
+ payment_profiles_supported?: true,
+ authorize: @success_response,
+ purchase: @success_response,
+ capture: @success_response,
+ void: @success_response,
+ credit: @success_response)
+
+ allow(@payment).to receive_messages payment_method: @payment_gateway
+ end
+
+ it 'responds to track_data' do
+ expect(credit_card.respond_to?(:track_data)).to be true
+ end
+
+ context '#can_capture?' do
+ it 'is true if payment is pending' do
+ payment = mock_model(Spree::Payment, pending?: true, created_at: Time.current)
+ expect(credit_card.can_capture?(payment)).to be true
+ end
+
+ it 'is true if payment is checkout' do
+ payment = mock_model(Spree::Payment, pending?: false, checkout?: true, created_at: Time.current)
+ expect(credit_card.can_capture?(payment)).to be true
+ end
+ end
+
+ context '#can_void?' do
+ it 'is true if payment is not void' do
+ payment = mock_model(Spree::Payment, failed?: false, void?: false)
+ expect(credit_card.can_void?(payment)).to be true
+ end
+ end
+
+ context '#can_credit?' do
+ it 'is false if payment is not completed' do
+ payment = mock_model(Spree::Payment, completed?: false)
+ expect(credit_card.can_credit?(payment)).to be false
+ end
+
+ it 'is false when credit_allowed is zero' do
+ payment = mock_model(Spree::Payment, completed?: true, credit_allowed: 0, order: mock_model(Spree::Order, payment_state: 'credit_owed'))
+ expect(credit_card.can_credit?(payment)).to be false
+ end
+ end
+
+ context '#valid?' do
+ it 'validates presence of number' do
+ credit_card.attributes = valid_credit_card_attributes.except(:number)
+ expect(credit_card).not_to be_valid
+ expect(credit_card.errors[:number]).to eq(["can't be blank"])
+ end
+
+ it 'validates presence of security code' do
+ credit_card.attributes = valid_credit_card_attributes.except(:verification_value)
+ expect(credit_card).not_to be_valid
+ expect(credit_card.errors[:verification_value]).to eq(["can't be blank"])
+ end
+
+ it 'validates name presence' do
+ credit_card.valid?
+ expect(credit_card.error_on(:name).size).to eq(1)
+ end
+
+ it 'only validates on create' do
+ credit_card.attributes = valid_credit_card_attributes
+ credit_card.save
+ expect(credit_card).to be_valid
+ end
+
+ context 'encrypted data is present' do
+ it 'does not validate presence of number or cvv' do
+ credit_card.encrypted_data = '$fdgsfgdgfgfdg&gfdgfdgsf-'
+ credit_card.valid?
+ expect(credit_card.errors[:number]).to be_empty
+ expect(credit_card.errors[:verification_value]).to be_empty
+ end
+ end
+
+ context 'imported is true' do
+ it 'does not validate presence of number or cvv' do
+ credit_card.imported = true
+ credit_card.valid?
+ expect(credit_card.errors[:number]).to be_empty
+ expect(credit_card.errors[:verification_value]).to be_empty
+ end
+ end
+ end
+
+ context '#save' do
+ before do
+ credit_card.attributes = valid_credit_card_attributes
+ credit_card.save!
+ end
+
+ let!(:persisted_card) { Spree::CreditCard.find(credit_card.id) }
+
+ it 'does not actually store the number' do
+ expect(persisted_card.number).to be_blank
+ end
+
+ it 'does not actually store the security code' do
+ expect(persisted_card.verification_value).to be_blank
+ end
+ end
+
+ context '#number=' do
+ it 'strips non-numeric characters from card input' do
+ credit_card.number = '6011000990139424'
+ expect(credit_card.number).to eq('6011000990139424')
+
+ credit_card.number = ' 6011-0009-9013-9424 '
+ expect(credit_card.number).to eq('6011000990139424')
+ end
+
+ it 'does not raise an exception on non-string input' do
+ credit_card.number = ({})
+ expect(credit_card.number).to be_nil
+ end
+ end
+
+ # Regression test for #3847 & #3896
+ context '#expiry=' do
+ it 'can set with a 2-digit month and year' do
+ credit_card.expiry = '04 / 14'
+ expect(credit_card.month).to eq(4)
+ expect(credit_card.year).to eq(2014)
+ end
+
+ it 'can set with a 2-digit month and 4-digit year' do
+ credit_card.expiry = '04 / 2014'
+ expect(credit_card.month).to eq(4)
+ expect(credit_card.year).to eq(2014)
+ end
+
+ it 'can set with a 2-digit month and 4-digit year without whitespace' do
+ credit_card.expiry = '04/14'
+ expect(credit_card.month).to eq(4)
+ expect(credit_card.year).to eq(2014)
+ end
+
+ it 'can set with a 2-digit month and 4-digit year without whitespace and slash' do
+ credit_card.expiry = '042014'
+ expect(credit_card.month).to eq(4)
+ expect(credit_card.year).to eq(2014)
+ end
+
+ it 'can set with a 2-digit month and 2-digit year without whitespace and slash' do
+ credit_card.expiry = '0414'
+ expect(credit_card.month).to eq(4)
+ expect(credit_card.year).to eq(2014)
+ end
+
+ it 'does not blow up when passed an empty string' do
+ expect { credit_card.expiry = '' }.not_to raise_error
+ end
+
+ # Regression test for #4725
+ it 'does not blow up when passed one number' do
+ expect { credit_card.expiry = '12' }.not_to raise_error
+ end
+ end
+
+ context '#cc_type=' do
+ it 'converts between the different types' do
+ credit_card.cc_type = 'mastercard'
+ expect(credit_card.cc_type).to eq('master')
+
+ credit_card.cc_type = 'maestro'
+ expect(credit_card.cc_type).to eq('master')
+
+ credit_card.cc_type = 'amex'
+ expect(credit_card.cc_type).to eq('american_express')
+
+ credit_card.cc_type = 'dinersclub'
+ expect(credit_card.cc_type).to eq('diners_club')
+
+ credit_card.cc_type = 'some_outlandish_cc_type'
+ expect(credit_card.cc_type).to eq('some_outlandish_cc_type')
+ end
+
+ it 'assigns the type based on card number in the event of js failure' do
+ credit_card.number = '4242424242424242'
+ credit_card.cc_type = ''
+ expect(credit_card.cc_type).to eq('visa')
+
+ credit_card.number = '5555555555554444'
+ credit_card.cc_type = ''
+ expect(credit_card.cc_type).to eq('master')
+
+ credit_card.number = '2223000010309703'
+ credit_card.cc_type = ''
+ expect(credit_card.cc_type).to eq('master')
+
+ credit_card.number = '378282246310005'
+ credit_card.cc_type = ''
+ expect(credit_card.cc_type).to eq('american_express')
+
+ credit_card.number = '30569309025904'
+ credit_card.cc_type = ''
+ expect(credit_card.cc_type).to eq('diners_club')
+
+ credit_card.number = '3530111333300000'
+ credit_card.cc_type = ''
+ expect(credit_card.cc_type).to eq('jcb')
+
+ credit_card.number = ''
+ credit_card.cc_type = ''
+ expect(credit_card.cc_type).to eq('')
+
+ credit_card.number = nil
+ credit_card.cc_type = ''
+ expect(credit_card.cc_type).to eq('')
+ end
+ end
+
+ context '#associations' do
+ it 'is able to access its payments' do
+ expect { credit_card.payments.to_a }.not_to raise_error
+ end
+ end
+
+ context '#first_name' do
+ before do
+ credit_card.name = 'Ludwig van Beethoven'
+ end
+
+ it 'extracts the first name' do
+ expect(credit_card.first_name).to eq 'Ludwig'
+ end
+ end
+
+ context '#last_name' do
+ before do
+ credit_card.name = 'Ludwig van Beethoven'
+ end
+
+ it 'extracts the last name' do
+ expect(credit_card.last_name).to eq 'van Beethoven'
+ end
+ end
+
+ context '#to_active_merchant' do
+ before do
+ credit_card.number = '4111111111111111'
+ credit_card.year = Time.current.year
+ credit_card.month = Time.current.month
+ credit_card.name = 'Ludwig van Beethoven'
+ credit_card.verification_value = 123
+ end
+
+ it 'converts to an ActiveMerchant::Billing::CreditCard object' do
+ am_card = credit_card.to_active_merchant
+ expect(am_card.number).to eq('4111111111111111')
+ expect(am_card.year).to eq(Time.current.year)
+ expect(am_card.month).to eq(Time.current.month)
+ expect(am_card.first_name).to eq('Ludwig')
+ expect(am_card.last_name).to eq('van Beethoven')
+ expect(am_card.verification_value).to eq(123)
+ end
+ end
+
+ it 'ensures only one credit card per user is default at a time' do
+ user = FactoryBot.create(:user)
+ first = FactoryBot.create(:credit_card, user: user, default: true)
+ second = FactoryBot.create(:credit_card, user: user, default: true)
+
+ expect(first.reload.default).to eq false
+ expect(second.reload.default).to eq true
+
+ first.default = true
+ first.save!
+
+ expect(first.reload.default).to eq true
+ expect(second.reload.default).to eq false
+ end
+
+ it 'allows default credit cards for different users' do
+ first = FactoryBot.create(:credit_card, user: FactoryBot.create(:user), default: true)
+ second = FactoryBot.create(:credit_card, user: FactoryBot.create(:user), default: true)
+
+ expect(first.reload.default).to eq true
+ expect(second.reload.default).to eq true
+ end
+
+ it 'allows this card to save even if the previously default card has expired' do
+ user = FactoryBot.create(:user)
+ first = FactoryBot.create(:credit_card, user: user, default: true)
+ second = FactoryBot.create(:credit_card, user: user, default: false)
+ first.update_columns(year: Time.current.year, month: 1.month.ago.month)
+
+ expect { second.update_attributes!(default: true) }.not_to raise_error
+ end
+end
diff --git a/core/spec/models/spree/customer_return_spec.rb b/core/spec/models/spree/customer_return_spec.rb
new file mode 100644
index 00000000000..44a6a377509
--- /dev/null
+++ b/core/spec/models/spree/customer_return_spec.rb
@@ -0,0 +1,238 @@
+require 'spec_helper'
+
+describe Spree::CustomerReturn, type: :model do
+ before do
+ allow_any_instance_of(Spree::Order).to receive_messages(return!: true)
+ end
+
+ describe '.validation' do
+ describe '#must_have_return_authorization' do
+ subject { customer_return.valid? }
+
+ let(:customer_return) { build(:customer_return) }
+
+ let(:inventory_unit) { build(:inventory_unit) }
+ let(:return_item) { build(:return_item, inventory_unit: inventory_unit) }
+
+ before do
+ customer_return.return_items << return_item
+ end
+
+ context 'return item does not belong to return authorization' do
+ before do
+ return_item.return_authorization = nil
+ end
+
+ it 'is not valid' do
+ expect(subject).to eq false
+ end
+
+ it 'adds an error message' do
+ subject
+ expect(customer_return.errors.full_messages).to include(Spree.t(:missing_return_authorization, item_name: inventory_unit.variant.name))
+ end
+ end
+
+ context 'return item belongs to return authorization' do
+ it 'is valid' do
+ expect(subject).to eq true
+ end
+ end
+ end
+
+ describe '#return_items_belong_to_same_order' do
+ subject { customer_return.valid? }
+
+ let(:customer_return) { build(:customer_return) }
+
+ let(:first_inventory_unit) { build(:inventory_unit) }
+ let(:first_return_item) { build(:return_item, inventory_unit: first_inventory_unit) }
+
+ let(:second_inventory_unit) { build(:inventory_unit, order: second_order) }
+ let(:second_return_item) { build(:return_item, inventory_unit: second_inventory_unit) }
+
+ before do
+ customer_return.return_items << first_return_item
+ customer_return.return_items << second_return_item
+ end
+
+ context 'return items are part of different orders' do
+ let(:second_order) { create(:order) }
+
+ it 'is not valid' do
+ expect(subject).to eq false
+ end
+
+ it 'adds an error message' do
+ subject
+ expect(customer_return.errors.full_messages).to include(Spree.t(:return_items_cannot_be_associated_with_multiple_orders))
+ end
+ end
+
+ context 'return items are part of the same order' do
+ let(:second_order) { first_inventory_unit.order }
+
+ it 'is valid' do
+ expect(subject).to eq true
+ end
+ end
+ end
+ end
+
+ describe 'whitelisted_ransackable_attributes' do
+ it { expect(Spree::CustomerReturn.whitelisted_ransackable_attributes).to eq(%w(number)) }
+ end
+
+ describe '#pre_tax_total' do
+ subject { customer_return.pre_tax_total }
+
+ let(:pre_tax_amount) { 15.0 }
+ let(:customer_return) { create(:customer_return, line_items_count: 2) }
+
+ before do
+ Spree::ReturnItem.where(customer_return_id: customer_return.id).update_all(pre_tax_amount: pre_tax_amount)
+ end
+
+ it "returns the sum of the return item's pre_tax_amount" do
+ expect(subject).to eq (pre_tax_amount * 2)
+ end
+ end
+
+ describe '#display_pre_tax_total' do
+ let(:customer_return) { Spree::CustomerReturn.new }
+
+ it 'returns a Spree::Money' do
+ allow(customer_return).to receive_messages(pre_tax_total: 21.22)
+ expect(customer_return.display_pre_tax_total).to eq(Spree::Money.new(21.22))
+ end
+ end
+
+ describe '#order' do
+ subject { customer_return.order }
+
+ let(:return_item) { create(:return_item) }
+ let(:customer_return) { build(:customer_return, return_items: [return_item]) }
+
+ it "returns the order associated with the return item's inventory unit" do
+ expect(subject).to eq return_item.inventory_unit.order
+ end
+ end
+
+ describe '#order_id' do
+ subject { customer_return.order_id }
+
+ context 'return item is not associated yet' do
+ let(:customer_return) { build(:customer_return) }
+
+ it 'is nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'has an associated return item' do
+ let(:return_item) { create(:return_item) }
+ let(:customer_return) { build(:customer_return, return_items: [return_item]) }
+
+ it "is the return item's inventory unit's order id" do
+ expect(subject).to eq return_item.inventory_unit.order.id
+ end
+ end
+ end
+
+ context '.after_save' do
+ let(:inventory_unit) { create(:inventory_unit, state: 'shipped', order: create(:shipped_order)) }
+ let(:return_item) { create(:return_item, inventory_unit: inventory_unit) }
+
+ context 'to the initial stock location' do
+ it 'marks all inventory units are returned' do
+ create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: inventory_unit.shipment.stock_location_id)
+ expect(inventory_unit.reload.state).to eq 'returned'
+ end
+
+ it 'updates the stock item counts in the stock location' do
+ expect do
+ create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: inventory_unit.shipment.stock_location_id)
+ end.to change { inventory_unit.find_stock_item.count_on_hand }.by(1)
+ end
+
+ context 'with Config.track_inventory_levels == false' do
+ before do
+ Spree::Config.track_inventory_levels = false
+ expect(Spree::StockItem).not_to receive(:find_by)
+ expect(Spree::StockMovement).not_to receive(:create!)
+ end
+
+ it 'does not update the stock item counts in the stock location' do
+ count_on_hand = inventory_unit.find_stock_item.count_on_hand
+ create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: inventory_unit.shipment.stock_location_id)
+ expect(inventory_unit.find_stock_item.count_on_hand).to eql count_on_hand
+ end
+ end
+ end
+
+ context 'to a different stock location' do
+ let(:new_stock_location) { create(:stock_location, name: 'other') }
+
+ it 'updates the stock item counts in new stock location' do
+ expect do
+ create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: new_stock_location.id)
+ end.to change {
+ Spree::StockItem.where(variant_id: inventory_unit.variant_id, stock_location_id: new_stock_location.id).first.count_on_hand
+ }.by(1)
+ end
+
+ it 'does not raise an error when no stock item exists in the stock location' do
+ inventory_unit.find_stock_item.destroy
+ expect { create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: new_stock_location.id) }.not_to raise_error
+ end
+
+ it 'does not update the stock item counts in the original stock location' do
+ count_on_hand = inventory_unit.find_stock_item.count_on_hand
+ create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: new_stock_location.id)
+ expect(inventory_unit.find_stock_item.count_on_hand).to eq(count_on_hand)
+ end
+ end
+ end
+
+ describe '#fully_reimbursed?' do
+ subject { customer_return.fully_reimbursed? }
+
+ let(:customer_return) { create(:customer_return) }
+
+ let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) }
+
+ context 'when some return items are undecided' do
+ it { is_expected.to be false }
+ end
+
+ context 'when all return items are decided' do
+ context 'when all return items are rejected' do
+ before { customer_return.return_items.each(&:reject!) }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when all return items are accepted' do
+ before { customer_return.return_items.each(&:accept!) }
+
+ context 'when some return items have no reimbursement' do
+ it { is_expected.to be false }
+ end
+
+ context 'when all return items have a reimbursement' do
+ let!(:reimbursement) { create(:reimbursement, customer_return: customer_return) }
+
+ context 'when some reimbursements are not reimbursed' do
+ it { is_expected.to be false }
+ end
+
+ context 'when all reimbursements are reimbursed' do
+ before { reimbursement.perform! }
+
+ it { is_expected.to be true }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/exchange_spec.rb b/core/spec/models/spree/exchange_spec.rb
new file mode 100644
index 00000000000..96ce30c879d
--- /dev/null
+++ b/core/spec/models/spree/exchange_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+module Spree
+ describe Exchange, type: :model do
+ let(:order) { Spree::Order.new }
+
+ let(:return_item_1) { build(:exchange_return_item) }
+ let(:return_item_2) { build(:exchange_return_item) }
+ let(:return_items) { [return_item_1, return_item_2] }
+ let(:exchange) { Exchange.new(order, return_items) }
+
+ describe '#description' do
+ before do
+ allow(return_item_1).to receive(:variant) { double(options_text: 'foo') }
+ allow(return_item_1).to receive(:exchange_variant) { double(options_text: 'bar') }
+ allow(return_item_2).to receive(:variant) { double(options_text: 'baz') }
+ allow(return_item_2).to receive(:exchange_variant) { double(options_text: 'qux') }
+ end
+
+ it "describes the return items' change in options" do
+ expect(exchange.description).to match(/foo => bar/)
+ expect(exchange.description).to match(/baz => qux/)
+ end
+ end
+
+ describe '#display_amount' do
+ it 'is the total amount of all return items' do
+ expect(exchange.display_amount).to eq Spree::Money.new(0.0)
+ end
+ end
+
+ describe '#perform!' do
+ subject { exchange.perform! }
+
+ let(:return_item) { create(:exchange_return_item) }
+ let(:return_items) { [return_item] }
+ let(:order) { return_item.return_authorization.order }
+
+ before { return_item.exchange_variant.stock_items.first.adjust_count_on_hand(20) }
+
+ it 'creates shipments for the order with the return items exchange inventory units' do
+ expect { subject }.to change { order.shipments.count }.by(1)
+ new_shipment = order.shipments.last
+ expect(new_shipment).to be_ready
+ new_inventory_units = new_shipment.inventory_units
+ expect(new_inventory_units.count).to eq 1
+ expect(new_inventory_units.first.original_return_item).to eq return_item
+ expect(new_inventory_units.first.line_item).to eq return_item.inventory_unit.line_item
+ end
+
+ context 'when it cannot create shipments for all items' do
+ before do
+ StockItem.where(variant_id: return_item.exchange_variant_id).destroy_all
+ end
+
+ it 'raises an UnableToCreateShipments error' do
+ expect do
+ subject
+ end.to raise_error(Spree::Exchange::UnableToCreateShipments)
+ end
+ end
+ end
+
+ describe '#to_key' do # for dom_id
+ it { expect(Exchange.new(nil, nil).to_key).to be_nil }
+ end
+
+ describe '.param_key' do # for dom_id
+ it { expect(Exchange.param_key).to eq 'spree_exchange' }
+ end
+
+ describe '.model_name' do # for dom_id
+ it { expect(Exchange.model_name).to eq Spree::Exchange }
+ end
+ end
+end
diff --git a/core/spec/models/spree/fulfilment_changer_spec.rb b/core/spec/models/spree/fulfilment_changer_spec.rb
new file mode 100644
index 00000000000..70e72d8c32e
--- /dev/null
+++ b/core/spec/models/spree/fulfilment_changer_spec.rb
@@ -0,0 +1,317 @@
+require 'spec_helper'
+
+describe Spree::FulfilmentChanger do
+ subject { shipment_splitter.run! }
+
+ let(:variant) { create(:variant) }
+
+ let(:order) do
+ create(
+ :completed_order_with_totals,
+ without_line_items: true,
+ line_items_attributes: [
+ {
+ quantity: current_shipment_inventory_unit_count,
+ variant: variant
+ }
+ ]
+ )
+ end
+
+ let(:current_shipment) { order.shipments.first }
+ let(:desired_shipment) { order.shipments.create(stock_location: desired_stock_location) }
+ let(:desired_stock_location) { current_shipment.stock_location }
+
+ let(:shipment_splitter) do
+ described_class.new(
+ current_stock_location: current_shipment.stock_location,
+ desired_stock_location: desired_shipment.stock_location,
+ current_shipment: current_shipment,
+ desired_shipment: desired_shipment,
+ variant: variant,
+ quantity: quantity
+ )
+ end
+
+ before do
+ order && desired_shipment
+ variant.stock_items.first.update_column(:count_on_hand, 100)
+ end
+
+ context 'when the current shipment has enough inventory units' do
+ let(:current_shipment_inventory_unit_count) { 2 }
+ let(:quantity) { 1 }
+
+ it 'adds the desired inventory units to the desired shipment' do
+ expect { subject }.to change { desired_shipment.inventory_units.length }.by(quantity)
+ end
+
+ it 'removes the desired inventory units from the current shipment' do
+ expect { subject }.to change { current_shipment.inventory_units.length }.by(-quantity)
+ end
+
+ it 'recalculates shipping costs for the current shipment' do
+ expect(current_shipment).to receive(:refresh_rates)
+ subject
+ end
+
+ it 'recalculates shipping costs for the new shipment' do
+ expect(desired_shipment).to receive(:refresh_rates)
+ subject
+ end
+
+ context 'when transferring to another stock location' do
+ let(:desired_stock_location) { create(:stock_location) }
+ let!(:stock_item) do
+ variant.stock_items.find_or_create_by!(
+ stock_location: desired_stock_location,
+ variant: variant
+ )
+ end
+
+ before do
+ stock_item.set_count_on_hand(desired_count_on_hand)
+ stock_item.update(backorderable: false)
+ end
+
+ context 'when the other stock location has enough stock' do
+ let(:desired_count_on_hand) { 2 }
+
+ it 'is marked as a successful transfer' do
+ expect(subject).to be true
+ end
+
+ it 'stocks the current stock location back up' do
+ expect { subject }.to change { current_shipment.stock_location.count_on_hand(variant) }.by(quantity)
+ end
+
+ it 'unstocks the desired stock location' do
+ expect { subject }.to change { desired_shipment.stock_location.count_on_hand(variant) }.by(-quantity)
+ end
+
+ context 'when the order is not completed' do
+ before do
+ allow_any_instance_of(order.class).to receive(:completed?).and_return(false)
+ end
+
+ it 'does not stock the current stock location back up' do
+ expect { subject }.not_to change { current_shipment.stock_location.count_on_hand(variant) }
+ end
+
+ it 'does not unstock the desired location' do
+ expect { subject }.not_to change(stock_item, :count_on_hand)
+ end
+ end
+ end
+
+ context 'when the desired stock location can only partially fulfil the quantity' do
+ let(:current_shipment_inventory_unit_count) { 10 }
+ let(:quantity) { 7 }
+ let(:desired_count_on_hand) { 5 }
+
+ before do
+ stock_item.update(backorderable: true)
+ end
+
+ it 'restocks seven at the original stock location' do
+ expect { subject }.to change { current_shipment.stock_location.count_on_hand(variant) }.by(7)
+ end
+
+ it 'unstocks seven at the desired stock location' do
+ expect { subject }.to change { desired_shipment.stock_location.count_on_hand(variant) }.by(-7)
+ end
+
+ it 'creates a shipment with the correct number of on hand and backordered units' do
+ subject
+ expect(desired_shipment.inventory_units.on_hand.count).to eq(5)
+ expect(desired_shipment.inventory_units.backordered.count).to eq(2)
+ end
+
+ context 'when the desired stock location already has a backordered units' do
+ let(:desired_count_on_hand) { -1 }
+
+ it 'restocks seven at the original stock location' do
+ expect { subject }.to change { current_shipment.stock_location.count_on_hand(variant) }.by(7)
+ end
+
+ it 'unstocks seven at the desired stock location' do
+ expect { subject }.to change { desired_shipment.stock_location.count_on_hand(variant) }.by(-7)
+ end
+
+ it 'creates a shipment with the correct number of on hand and backordered units' do
+ subject
+ expect(desired_shipment.inventory_units.on_hand.count).to eq(0)
+ expect(desired_shipment.inventory_units.backordered.count).to eq(7)
+ end
+ end
+
+ context 'when the original shipment has on hand and backordered units' do
+ before do
+ current_shipment.inventory_units.limit(6).update_all(state: :backordered)
+ end
+
+ it 'removes the backordered items first' do
+ subject
+ expect(current_shipment.inventory_units.backordered.count).to eq(0)
+ expect(current_shipment.inventory_units.on_hand.count).to eq(3)
+ end
+ end
+
+ context 'when the original shipment had some backordered units' do
+ let(:current_stock_item) { current_shipment.stock_location.stock_items.find_by(variant: variant) }
+ let(:desired_stock_item) { desired_shipment.stock_location.stock_items.find_by(variant: variant) }
+ let(:backordered_units) { 6 }
+
+ before do
+ current_shipment.inventory_units.limit(backordered_units).update_all(state: :backordered)
+ current_stock_item.set_count_on_hand(-backordered_units)
+ end
+
+ it 'restocks four at the original stock location' do
+ expect { subject }.to change { current_stock_item.reload.count_on_hand }.from(-backordered_units).to(1)
+ end
+
+ it 'unstocks five at the desired stock location' do
+ expect { subject }.to change { desired_stock_item.reload.count_on_hand }.from(5).to(-2)
+ end
+
+ it 'creates a shipment with the correct number of on hand and backordered units' do
+ subject
+ expect(desired_shipment.inventory_units.on_hand.count).to eq(5)
+ expect(desired_shipment.inventory_units.backordered.count).to eq(2)
+ end
+ end
+ end
+
+ context 'when the other stock location does not have enough stock' do
+ let(:desired_count_on_hand) { 0 }
+
+ it 'is not successful' do
+ expect(subject).to be false
+ end
+
+ it 'has an activemodel error hash' do
+ subject
+ expect(shipment_splitter.errors.messages).to eq(desired_shipment: ['not enough stock in desired stock location'])
+ end
+ end
+ end
+
+ context 'when the quantity to transfer is not positive' do
+ let(:quantity) { 0 }
+
+ it 'is not successful' do
+ expect(subject).to be false
+ end
+
+ it 'has an activemodel error hash' do
+ subject
+ expect(shipment_splitter.errors.messages).to eq(quantity: ['must be greater than 0'])
+ end
+ end
+
+ context 'when the desired shipment is identical to the current shipment' do
+ let(:desired_shipment) { current_shipment }
+
+ it 'is not successful' do
+ expect(subject).to be false
+ end
+
+ it 'has an activemodel error hash' do
+ subject
+ expect(shipment_splitter.errors.messages).to eq(desired_shipment: ['can not be same as current shipment'])
+ end
+ end
+
+ context 'when the desired shipment has no stock location' do
+ let(:desired_stock_location) { nil }
+
+ it 'is not successful' do
+ expect(subject).to be false
+ end
+
+ it 'has an activemodel error hash' do
+ subject
+ expect(shipment_splitter.errors.messages).to eq(desired_stock_location: ["can't be blank"])
+ end
+ end
+
+ context 'when the current shipment has been shipped already' do
+ let(:order) do
+ create(
+ :shipped_order,
+ line_items_attributes: [
+ {
+ quantity: current_shipment_inventory_unit_count,
+ variant: variant
+ }
+ ]
+ )
+ end
+
+ it 'is not successful' do
+ expect(subject).to be false
+ end
+
+ it 'has an activemodel error hash' do
+ subject
+ expect(shipment_splitter.errors.messages).to eq(current_shipment: ['has already been shipped'])
+ end
+ end
+ end
+
+ context 'when the current shipment is emptied out by the transfer' do
+ let(:current_shipment_inventory_unit_count) { 30 }
+ let(:quantity) { 30 }
+
+ it 'adds the desired inventory units to the desired shipment' do
+ expect { subject }.to change { desired_shipment.inventory_units.length }.by(quantity)
+ end
+
+ it 'removes the current shipment' do
+ expect { subject }.to change { Spree::Shipment.count }.by(-1)
+ end
+ end
+
+ context 'when the desired shipment is not yet persisted' do
+ let(:current_shipment_inventory_unit_count) { 2 }
+ let(:quantity) { 1 }
+
+ let(:desired_shipment) { order.shipments.build(stock_location: current_shipment.stock_location) }
+
+ it 'adds the desired inventory units to the desired shipment' do
+ expect { subject }.to change { Spree::Shipment.count }.by(1)
+ end
+
+ context 'if the desired shipment is invalid' do
+ let(:desired_shipment) { order.shipments.build(stock_location_id: 99_999_999) }
+
+ it 'is not successful' do
+ expect(subject).to be false
+ end
+
+ it 'has an activemodel error hash' do
+ subject
+ expect(shipment_splitter.errors.messages).to eq(desired_stock_location: ["can't be blank"])
+ end
+ end
+ end
+
+ context 'when stock_item is last on_hand' do
+ before do
+ variant.stock_items.first.update_column(:count_on_hand, 1)
+ end
+
+ let(:current_shipment_inventory_unit_count) { 1 }
+ let(:quantity) { 1 }
+
+ it 'is successful' do
+ expect(subject).to be true
+ end
+
+ it 'has inventory unit on_hand' do
+ subject
+ expect(desired_shipment.inventory_units.first.state).to eq('on_hand')
+ end
+ end
+end
diff --git a/core/spec/models/spree/gateway/bogus_simple.rb b/core/spec/models/spree/gateway/bogus_simple.rb
new file mode 100644
index 00000000000..31c49f7c6aa
--- /dev/null
+++ b/core/spec/models/spree/gateway/bogus_simple.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Spree::Gateway::BogusSimple, type: :model do
+ subject { Spree::Gateway::BogusSimple.new }
+
+ # regression test for #3824
+ describe '#capture' do
+ it 'returns success with the right response code' do
+ response = subject.capture(123, '12345', {})
+ expect(response.message).to include('success')
+ end
+
+ it 'returns failure with the wrong response code' do
+ response = subject.capture(123, 'wrong', {})
+ expect(response.message).to include('failure')
+ end
+ end
+end
diff --git a/core/spec/models/spree/gateway/bogus_spec.rb b/core/spec/models/spree/gateway/bogus_spec.rb
new file mode 100644
index 00000000000..1167b180efa
--- /dev/null
+++ b/core/spec/models/spree/gateway/bogus_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+module Spree
+ describe Gateway::Bogus, type: :model do
+ let(:bogus) { create(:credit_card_payment_method) }
+ let!(:cc) { create(:credit_card, payment_method: bogus, gateway_customer_profile_id: 'BGS-RERTERT') }
+
+ it 'disable recurring contract by destroying payment source' do
+ bogus.disable_customer_profile(cc)
+ expect(cc.gateway_customer_profile_id).to be_nil
+ end
+ end
+end
diff --git a/core/spec/models/spree/gateway_spec.rb b/core/spec/models/spree/gateway_spec.rb
new file mode 100644
index 00000000000..f121b34e672
--- /dev/null
+++ b/core/spec/models/spree/gateway_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Spree::Gateway, type: :model do
+ class Provider
+ def initialize(options); end
+
+ def imaginary_method; end
+ end
+
+ class TestGateway < Spree::Gateway
+ def provider_class
+ Provider
+ end
+ end
+
+ it 'passes through all arguments on a method_missing call' do
+ gateway = TestGateway.new
+ expect(gateway.provider).to receive(:imaginary_method).with('foo')
+ gateway.imaginary_method('foo')
+ end
+
+ context 'fetching payment sources' do
+ let(:order) { Spree::Order.create(user_id: 1) }
+
+ let(:has_card) { create(:credit_card_payment_method) }
+ let(:no_card) { create(:credit_card_payment_method) }
+
+ let(:cc) do
+ create(:credit_card, payment_method: has_card, gateway_customer_profile_id: 'EFWE')
+ end
+
+ let(:payment) do
+ create(:payment, order: order, source: cc, payment_method: has_card)
+ end
+
+ it 'finds credit cards associated on a order completed' do
+ allow(payment.order).to receive_messages completed?: true
+
+ expect(no_card.reusable_sources(payment.order)).to be_empty
+ expect(has_card.reusable_sources(payment.order)).not_to be_empty
+ end
+
+ it 'finds credit cards associated with the order user' do
+ cc.update_column :user_id, 1
+ allow(payment.order).to receive_messages completed?: false
+
+ expect(no_card.reusable_sources(payment.order)).to be_empty
+ expect(has_card.reusable_sources(payment.order)).not_to be_empty
+ end
+ end
+
+ it 'returns exchange multiplier for gateway' do
+ gateway = TestGateway.new
+
+ rate = Spree::Gateway::FROM_DOLLAR_TO_CENT_RATE
+ expect(gateway.exchange_multiplier).to eq rate
+ end
+end
diff --git a/core/spec/models/spree/image_spec.rb b/core/spec/models/spree/image_spec.rb
new file mode 100644
index 00000000000..aee9e764c51
--- /dev/null
+++ b/core/spec/models/spree/image_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Spree::Image, type: :model do
+ context 'validation' do
+ let(:spree_image) { Spree::Image.new }
+ let(:image_file) { File.open(Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg') }
+ let(:text_file) { File.open(Spree::Core::Engine.root + 'spec/fixtures' + 'text-file.txt') }
+
+ it 'has attachment present' do
+ if Rails.application.config.use_paperclip
+ spree_image.attachment = image_file
+ else
+ spree_image.attachment.attach(io: image_file, filename: 'thinking-cat.jpg')
+ end
+ expect(spree_image).to be_valid
+ end
+
+ it 'has attachment absent' do
+ if Rails.application.config.use_paperclip
+ spree_image.attachment = nil
+ else
+ spree_image.attachment.attach(nil)
+ end
+ expect(spree_image).not_to be_valid
+ end
+
+ it 'has allowed attachment content type' do
+ if Rails.application.config.use_paperclip
+ spree_image.attachment = image_file
+ else
+ spree_image.attachment.attach(io: image_file, filename: 'thinking-cat.jpg', content_type: 'image/jpeg')
+ end
+ expect(spree_image).to be_valid
+ end
+
+ it 'has no allowed attachment content type' do
+ if Rails.application.config.use_paperclip
+ spree_image.attachment = text_file
+ else
+ spree_image.attachment.attach(io: text_file, filename: 'text-file.txt', content_type: 'text/plain')
+ end
+ expect(spree_image).not_to be_valid
+ end
+ end
+end
diff --git a/core/spec/models/spree/inventory_unit_spec.rb b/core/spec/models/spree/inventory_unit_spec.rb
new file mode 100644
index 00000000000..63598fb11ce
--- /dev/null
+++ b/core/spec/models/spree/inventory_unit_spec.rb
@@ -0,0 +1,249 @@
+require 'spec_helper'
+
+describe Spree::InventoryUnit, type: :model do
+ let(:stock_location) { create(:stock_location_with_items) }
+ let(:stock_item) { stock_location.stock_items.order(:id).first }
+
+ describe 'scopes' do
+ let!(:inventory_unit_1) { create(:inventory_unit, state: 'on_hand') }
+ let!(:inventory_unit_2) { create(:inventory_unit, state: 'backordered') }
+ let!(:inventory_unit_3) { create(:inventory_unit, state: 'shipped') }
+ let!(:inventory_unit_4) { create(:inventory_unit, state: 'returned') }
+
+ describe '.backordered' do
+ it { expect(Spree::InventoryUnit.backordered).to eq([inventory_unit_2]) }
+ end
+
+ describe '.on_hand' do
+ it { expect(Spree::InventoryUnit.on_hand).to eq([inventory_unit_1]) }
+ end
+
+ describe '.on_hand_or_backordered' do
+ it { expect(Spree::InventoryUnit.on_hand_or_backordered).to match_array([inventory_unit_1, inventory_unit_2]) }
+ end
+
+ describe '.shipped' do
+ it { expect(Spree::InventoryUnit.shipped).to eq([inventory_unit_3]) }
+ end
+
+ describe '.returned' do
+ it { expect(Spree::InventoryUnit.returned).to eq([inventory_unit_4]) }
+ end
+ end
+
+ context '#backordered_for_stock_item' do
+ let(:order) do
+ order = create(:order, state: 'complete', ship_address: create(:ship_address))
+ order.completed_at = Time.current
+ create(:shipment, order: order, stock_location: stock_location)
+ order.shipments.reload
+ create(:line_item, order: order, variant: stock_item.variant)
+ order.line_items.reload
+ order.tap(&:save!)
+ end
+
+ let(:shipment) do
+ order.shipments.first
+ end
+
+ let(:shipping_method) do
+ shipment.shipping_methods.first
+ end
+
+ let!(:unit) do
+ unit = shipment.inventory_units.first
+ unit.state = 'backordered'
+ unit.tap(&:save!)
+ end
+
+ before do
+ stock_item.set_count_on_hand(-2)
+ end
+
+ # Regression for #3066
+ it 'returns modifiable objects' do
+ units = Spree::InventoryUnit.backordered_for_stock_item(stock_item)
+ expect { units.first.save! }.not_to raise_error
+ end
+
+ it "finds inventory units from its stock location when the unit's variant matches the stock item's variant" do
+ expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).to match_array([unit])
+ end
+
+ it "does not find inventory units that aren't backordered" do
+ on_hand_unit = shipment.inventory_units.build
+ on_hand_unit.state = 'on_hand'
+ on_hand_unit.variant_id = 1
+ on_hand_unit.save!
+
+ expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).not_to include(on_hand_unit)
+ end
+
+ it "does not find inventory units that don't match the stock item's variant" do
+ other_variant_unit = shipment.inventory_units.build
+ other_variant_unit.state = 'backordered'
+ other_variant_unit.variant = create(:variant)
+ other_variant_unit.save!
+
+ expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).not_to include(other_variant_unit)
+ end
+
+ it 'does not change shipping cost when fulfilling the order' do
+ current_shipment_cost = shipment.cost
+ shipping_method.calculator.set_preference(:amount, current_shipment_cost + 5.0)
+ stock_item.set_count_on_hand(0)
+ expect(shipment.reload.cost).to eq(current_shipment_cost)
+ end
+
+ context 'other shipments' do
+ let(:other_order) do
+ order = create(:order)
+ order.state = 'payment'
+ order.completed_at = nil
+ order.tap(&:save!)
+ end
+
+ let(:other_shipment) do
+ shipment = Spree::Shipment.new
+ shipment.stock_location = stock_location
+ shipment.shipping_methods << create(:shipping_method)
+ shipment.order = other_order
+ # We don't care about this in this test
+ allow(shipment).to receive(:ensure_correct_adjustment)
+ shipment.tap(&:save!)
+ end
+
+ let!(:other_unit) do
+ unit = other_shipment.inventory_units.build
+ unit.state = 'backordered'
+ unit.variant_id = stock_item.variant.id
+ unit.order_id = other_order.id
+ unit.tap(&:save!)
+ end
+
+ it 'does not find inventory units belonging to incomplete orders' do
+ expect(Spree::InventoryUnit.backordered_for_stock_item(stock_item)).not_to include(other_unit)
+ end
+ end
+ end
+
+ context '#finalize_units!' do
+ let!(:stock_location) { create(:stock_location) }
+ let(:variant) { create(:variant) }
+ let (:shipment) { create(:shipment) }
+ let(:inventory_units) do
+ [
+ create(:inventory_unit, variant: variant),
+ create(:inventory_unit, variant: variant)
+ ]
+ end
+
+ before do
+ shipment.inventory_units = inventory_units
+ end
+
+ it 'creates a stock movement' do
+ expect { shipment.inventory_units.finalize_units! }.
+ to change { shipment.inventory_units.where(pending: false).count }.by 2
+ end
+ end
+
+ describe '#current_or_new_return_item' do
+ subject { inventory_unit.current_or_new_return_item }
+
+ before { allow(inventory_unit).to receive_messages(pre_tax_amount: 100.0) }
+
+ context 'associated with a return item' do
+ let(:return_item) { create(:return_item) }
+ let(:inventory_unit) { return_item.inventory_unit }
+
+ it 'returns a persisted return item' do
+ expect(subject).to be_persisted
+ end
+
+ it "returns it's associated return_item" do
+ expect(subject).to eq return_item
+ end
+ end
+
+ context 'no associated return item' do
+ let(:inventory_unit) { create(:inventory_unit) }
+
+ it 'returns a new return item' do
+ expect(subject).not_to be_persisted
+ end
+
+ it 'associates itself to the new return_item' do
+ expect(subject.inventory_unit).to eq inventory_unit
+ end
+ end
+ end
+
+ describe '#additional_tax_total' do
+ subject do
+ build(:inventory_unit, line_item: line_item)
+ end
+
+ let(:quantity) { 2 }
+ let(:line_item_additional_tax_total) { 10.00 }
+ let(:line_item) do
+ build(:line_item, quantity: quantity,
+ additional_tax_total: line_item_additional_tax_total)
+ end
+
+ it 'is the correct amount' do
+ expect(subject.additional_tax_total).to eq line_item_additional_tax_total / quantity
+ end
+ end
+
+ describe '#included_tax_total' do
+ subject do
+ build(:inventory_unit, line_item: line_item)
+ end
+
+ let(:quantity) { 2 }
+ let(:line_item_included_tax_total) { 10.00 }
+ let(:line_item) do
+ build(:line_item, quantity: quantity,
+ included_tax_total: line_item_included_tax_total)
+ end
+
+ it 'is the correct amount' do
+ expect(subject.included_tax_total).to eq line_item_included_tax_total / quantity
+ end
+ end
+
+ describe '#additional_tax_total' do
+ subject do
+ build(:inventory_unit, line_item: line_item)
+ end
+
+ let(:quantity) { 2 }
+ let(:line_item_additional_tax_total) { 10.00 }
+ let(:line_item) do
+ build(:line_item, quantity: quantity,
+ additional_tax_total: line_item_additional_tax_total)
+ end
+
+ it 'is the correct amount' do
+ expect(subject.additional_tax_total).to eq line_item_additional_tax_total / quantity
+ end
+ end
+
+ describe '#included_tax_total' do
+ subject do
+ build(:inventory_unit, line_item: line_item)
+ end
+
+ let(:quantity) { 2 }
+ let(:line_item_included_tax_total) { 10.00 }
+ let(:line_item) do
+ build(:line_item, quantity: quantity,
+ included_tax_total: line_item_included_tax_total)
+ end
+
+ it 'is the correct amount' do
+ expect(subject.included_tax_total).to eq line_item_included_tax_total / quantity
+ end
+ end
+end
diff --git a/core/spec/models/spree/line_item_spec.rb b/core/spec/models/spree/line_item_spec.rb
new file mode 100644
index 00000000000..f7b1442d9b9
--- /dev/null
+++ b/core/spec/models/spree/line_item_spec.rb
@@ -0,0 +1,389 @@
+require 'spec_helper'
+
+describe Spree::LineItem, type: :model do
+ let(:order) { create :order_with_line_items, line_items_count: 1 }
+ let(:line_item) { order.line_items.first }
+
+ before { create(:store) }
+
+ describe 'Validations' do
+ describe 'ensure_proper_currency' do
+ context 'order is present' do
+ context "when line_item's currency matches with order's" do
+ it { expect(line_item).to be_valid }
+ end
+
+ context "when line_item's currency does not matches with order's" do
+ before do
+ line_item.currency = 'Invalid Currency'
+ end
+
+ it { expect(line_item).not_to be_valid }
+ end
+ end
+ end
+ end
+
+ describe '#ensure_valid_quantity' do
+ context 'quantity.nil?' do
+ before do
+ line_item.quantity = nil
+ line_item.valid?
+ end
+
+ it { expect(line_item.quantity).to be_zero }
+ end
+
+ context 'quantity < 0' do
+ before do
+ line_item.quantity = -1
+ line_item.valid?
+ end
+
+ it { expect(line_item.quantity).to be_zero }
+ end
+
+ context 'quantity = 0' do
+ before do
+ line_item.quantity = 0
+ line_item.valid?
+ end
+
+ it { expect(line_item.quantity).to be_zero }
+ end
+
+ context 'quantity > 0' do
+ let(:original_quantity) { 1 }
+
+ before do
+ line_item.quantity = original_quantity
+ line_item.valid?
+ end
+
+ it { expect(line_item.quantity).to eq(original_quantity) }
+ end
+ end
+
+ context '#save' do
+ it 'touches the order' do
+ expect(line_item.order).to receive(:touch)
+ line_item.touch
+ end
+ end
+
+ context '#discontinued' do
+ it 'fetches discontinued products' do
+ line_item.product.discontinue!
+ expect(line_item.reload.product).to be_a Spree::Product
+ end
+
+ it 'fetches discontinued variants' do
+ line_item.variant.discontinue!
+ expect(line_item.reload.variant).to be_a Spree::Variant
+ end
+ end
+
+ context '#destroy' do
+ let!(:line_item) { order.line_items.first }
+
+ it 'deletes inventory units' do
+ expect { line_item.destroy }.to change { line_item.inventory_units.count }.from(1).to(0)
+ end
+ end
+
+ context '#save' do
+ context 'line item changes' do
+ before do
+ line_item.quantity = line_item.quantity + 1
+ end
+
+ it 'triggers adjustment total recalculation' do
+ expect(line_item).to receive(:update_tax_charge) # Regression test for https://github.com/spree/spree/issues/4671
+ expect(line_item).to receive(:recalculate_adjustments)
+ line_item.save
+ end
+ end
+
+ context 'line item does not change' do
+ it 'does not trigger adjustment total recalculation' do
+ expect(line_item).not_to receive(:recalculate_adjustments)
+ line_item.save
+ end
+ end
+
+ context 'target_shipment is provided' do
+ it 'verifies inventory' do
+ line_item.target_shipment = Spree::Shipment.new
+ expect_any_instance_of(Spree::OrderInventory).to receive(:verify)
+ line_item.save
+ end
+ end
+ end
+
+ context '#create' do
+ let(:variant) { create(:variant) }
+
+ before do
+ create(:tax_rate, zone: order.tax_zone, tax_category: variant.tax_category)
+ end
+
+ context 'when order has a tax zone' do
+ before do
+ expect(order.tax_zone).to be_present
+ end
+
+ it 'creates a tax adjustment' do
+ Spree::Cart::AddItem.call(order: order, variant: variant)
+ line_item = order.find_line_item_by_variant(variant)
+ expect(line_item.adjustments.tax.count).to eq(1)
+ end
+ end
+
+ context 'when order does not have a tax zone' do
+ before do
+ order.bill_address = nil
+ order.ship_address = nil
+ order.save
+ expect(order.reload.tax_zone).to be_nil
+ end
+
+ it 'does not create a tax adjustment' do
+ Spree::Cart::AddItem.call(order: order, variant: variant)
+
+ line_item = order.find_line_item_by_variant(variant)
+ expect(line_item.adjustments.tax.count).to eq(0)
+ end
+ end
+ end
+
+ # Test for #3391
+ context '#copy_price' do
+ it "copies over a variant's prices" do
+ line_item.price = nil
+ line_item.cost_price = nil
+ line_item.currency = nil
+ line_item.copy_price
+ variant = line_item.variant
+ expect(line_item.price).to eq(variant.price)
+ expect(line_item.cost_price).to eq(variant.cost_price)
+ expect(line_item.currency).to eq(variant.currency)
+ end
+ end
+
+ # test for copying prices when the vat changes
+ context '#update_price' do
+ it 'copies over a variants differing price for another vat zone' do
+ expect(line_item.variant).to receive(:price_including_vat_for).and_return(12)
+ line_item.price = 10
+ line_item.update_price
+ expect(line_item.price).to eq(12)
+ end
+ end
+
+ # Test for #3481
+ context '#copy_tax_category' do
+ it "copies over a variant's tax category" do
+ line_item.tax_category = nil
+ line_item.copy_tax_category
+ expect(line_item.tax_category).to eq(line_item.variant.tax_category)
+ end
+ end
+
+ describe '#discounted_amount' do
+ it 'returns the amount minus any discounts' do
+ line_item.price = 10
+ line_item.quantity = 2
+ line_item.taxable_adjustment_total = -5
+ expect(line_item.discounted_amount).to eq(15)
+ end
+ end
+
+ describe '.currency' do
+ it 'returns the globally configured currency' do
+ line_item.currency == 'USD'
+ end
+ end
+
+ describe '#discounted_money' do
+ it 'returns a money object with the discounted amount' do
+ expect(line_item.discounted_money.to_s).to eq '$10.00'
+ end
+ end
+
+ describe '#money' do
+ before do
+ line_item.price = 3.50
+ line_item.quantity = 2
+ end
+
+ it 'returns a Spree::Money representing the total for this line item' do
+ expect(line_item.money.to_s).to eq('$7.00')
+ end
+ end
+
+ describe '#single_money' do
+ before do
+ line_item.price = 3.50
+ line_item.quantity = 2
+ end
+
+ it 'returns a Spree::Money representing the price for one variant' do
+ expect(line_item.single_money.to_s).to eq('$3.50')
+ end
+ end
+
+ context 'has inventory (completed order so items were already unstocked)' do
+ let(:order) { Spree::Order.create(email: 'spree@example.com') }
+ let(:variant) { create(:variant) }
+
+ context 'nothing left on stock' do
+ before do
+ variant.stock_items.update_all count_on_hand: 5, backorderable: false
+ Spree::Cart::AddItem.call(order: order, variant: variant, quantity: 5)
+ order.create_proposed_shipments
+ order.finalize!
+ order.reload
+ end
+
+ it 'allows to decrease item quantity' do
+ line_item = order.line_items.first
+ line_item.quantity -= 1
+ line_item.target_shipment = order.shipments.first
+ line_item.valid?
+
+ expect(line_item.errors_on(:quantity).size).to eq(0)
+ end
+
+ it 'doesnt allow to increase item quantity' do
+ line_item = order.line_items.first
+ line_item.quantity += 2
+ line_item.target_shipment = order.shipments.first
+ line_item.valid?
+
+ expect(line_item.errors_on(:quantity).size).to eq(1)
+ end
+ end
+
+ context '2 items left on stock' do
+ before do
+ variant.stock_items.update_all count_on_hand: 7, backorderable: false
+ Spree::Cart::AddItem.call(order: order, variant: variant, quantity: 5)
+ order.create_proposed_shipments
+ order.finalize!
+ order.reload
+ end
+
+ it 'allows to increase quantity up to stock availability' do
+ line_item = order.line_items.first
+ line_item.quantity += 2
+ line_item.target_shipment = order.shipments.first
+ line_item.valid?
+
+ expect(line_item.errors_on(:quantity).size).to eq(0)
+ end
+
+ it 'doesnt allow to increase quantity over stock availability' do
+ line_item = order.line_items.first
+ line_item.quantity += 3
+ line_item.target_shipment = order.shipments.first
+ line_item.valid?
+
+ expect(line_item.errors_on(:quantity).size).to eq(1)
+ end
+ end
+ end
+
+ context 'currency same as order.currency' do
+ it 'is a valid line item' do
+ line_item = order.line_items.first
+ line_item.currency = order.currency
+ line_item.valid?
+
+ expect(line_item.error_on(:currency).size).to eq(0)
+ end
+ end
+
+ context 'currency different than order.currency' do
+ it 'is not a valid line item' do
+ line_item = order.line_items.first
+ line_item.currency = 'no currency'
+ line_item.valid?
+
+ expect(line_item.error_on(:currency).size).to eq(1)
+ end
+ end
+
+ describe '#options=' do
+ it 'can handle updating a blank line item with no order' do
+ line_item.options = { price: 123 }
+ end
+
+ it 'updates the data provided in the options' do
+ line_item.options = { price: 123 }
+ expect(line_item.price).to eq 123
+ end
+
+ it 'updates the price based on the options provided' do
+ expect(line_item).to receive(:gift_wrap=).with(true)
+ expect(line_item.variant).to receive(:gift_wrap_price_modifier_amount_in).with('USD', true).and_return 1.99
+ line_item.options = { gift_wrap: true }
+ expect(line_item.price).to eq 21.98
+ end
+ end
+
+ describe 'precision of pre_tax_amount' do
+ let(:line_item) { create :line_item, pre_tax_amount: 4.2051 }
+
+ it 'keeps four digits of precision even when reloading' do
+ # prevent it from updating pre_tax_amount
+ allow_any_instance_of(Spree::LineItem).to receive(:update_tax_charge)
+ expect(line_item.reload.pre_tax_amount).to eq(4.2051)
+ end
+ end
+
+ describe '#update_price_from_modifier' do
+ context 'with specified currency' do
+ let(:line_item) { create :line_item }
+
+ it 'sets currency' do
+ expect do
+ line_item.send(:update_price_from_modifier, 'EUR', {})
+ end.to change(line_item, :currency).to('EUR').from('USD')
+ end
+
+ context 'variant with price in this currency' do
+ it 'sets the proper price' do
+ line_item.variant.prices.create(amount: 10, currency: 'EUR')
+ expect(line_item.variant).to receive(:gift_wrap_price_modifier_amount_in).with('EUR', true).and_return 1.99
+ expect do
+ line_item.send(:update_price_from_modifier, 'EUR', gift_wrap: true)
+ end.to change { line_item.price.to_f }.to(11.99)
+ end
+ end
+
+ context 'variant without price in this currency' do
+ it 'sets the proper price' do
+ expect(line_item.variant).to receive(:gift_wrap_price_modifier_amount_in).with('EUR', true).and_return 1.99
+ expect do
+ line_item.send(:update_price_from_modifier, 'EUR', gift_wrap: true)
+ end.to change { line_item.price.to_f }.to(1.99)
+ end
+ end
+ end
+
+ context 'without currency' do
+ let(:line_item) { create :line_item, variant: create(:variant, price: 10) }
+
+ before do
+ line_item.order.currency = nil
+ end
+
+ it 'sets the proper price' do
+ expect(line_item.variant).to receive(:gift_wrap_price_modifier_amount).with(true).and_return 1.99
+ expect do
+ line_item.send(:update_price_from_modifier, nil, gift_wrap: true)
+ end.to change { line_item.price.to_f }.to(11.99).from(10)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/option_type_spec.rb b/core/spec/models/spree/option_type_spec.rb
new file mode 100644
index 00000000000..5b5f7497b9e
--- /dev/null
+++ b/core/spec/models/spree/option_type_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Spree::OptionType, type: :model do
+ context 'touching' do
+ it 'touches a product' do
+ product_option_type = create(:product_option_type)
+ option_type = product_option_type.option_type
+ product = product_option_type.product
+ product.update_column(:updated_at, 1.day.ago)
+ option_type.touch
+ expect(product.reload.updated_at).to be_within(3.seconds).of(Time.current)
+ end
+ end
+end
diff --git a/core/spec/models/spree/option_value_spec.rb b/core/spec/models/spree/option_value_spec.rb
new file mode 100644
index 00000000000..b02ba31c902
--- /dev/null
+++ b/core/spec/models/spree/option_value_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Spree::OptionValue, type: :model do
+ context 'touching' do
+ it 'touches a variant' do
+ variant = create(:variant)
+ option_value = variant.option_values.first
+ variant.update_column(:updated_at, 1.day.ago)
+ option_value.touch
+ expect(variant.reload.updated_at).to be_within(3.seconds).of(Time.current)
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/address_spec.rb b/core/spec/models/spree/order/address_spec.rb
new file mode 100644
index 00000000000..f18beb13541
--- /dev/null
+++ b/core/spec/models/spree/order/address_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Spree::Order, type: :model do
+ let(:order) { Spree::Order.new }
+
+ context 'validation' do
+ context 'when @use_billing is populated' do
+ before do
+ order.bill_address = stub_model(Spree::Address)
+ order.ship_address = nil
+ end
+
+ context 'with true' do
+ before { order.use_billing = true }
+
+ it 'clones the bill address to the ship address' do
+ order.valid?
+ expect(order.ship_address).to eq(order.bill_address)
+ end
+ end
+
+ context "with 'true'" do
+ before { order.use_billing = 'true' }
+
+ it 'clones the bill address to the shipping' do
+ order.valid?
+ expect(order.ship_address).to eq(order.bill_address)
+ end
+ end
+
+ context "with '1'" do
+ before { order.use_billing = '1' }
+
+ it 'clones the bill address to the shipping' do
+ order.valid?
+ expect(order.ship_address).to eq(order.bill_address)
+ end
+ end
+
+ context "with something other than a 'truthful' value" do
+ before { order.use_billing = '0' }
+
+ it 'does not clone the bill address to the shipping' do
+ order.valid?
+ expect(order.ship_address).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/adjustments_spec.rb b/core/spec/models/spree/order/adjustments_spec.rb
new file mode 100644
index 00000000000..6ebc793d751
--- /dev/null
+++ b/core/spec/models/spree/order/adjustments_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Spree::Order do
+ context 'when an order has an adjustment that zeroes the total, but another adjustment for shipping that raises it above zero' do
+ let!(:persisted_order) { create(:order) }
+ let!(:line_item) { create(:line_item) }
+ let!(:shipping_method) do
+ sm = create(:shipping_method)
+ sm.calculator.preferred_amount = 10
+ sm.save
+ sm
+ end
+
+ before do
+ # Don't care about available payment methods in this test
+ allow(persisted_order).to receive_messages(has_available_payment: false)
+ persisted_order.line_items << line_item
+ create(:adjustment, amount: -line_item.amount, label: 'Promotion', adjustable: line_item, order: persisted_order)
+ persisted_order.state = 'delivery'
+ persisted_order.save # To ensure new state_change event
+ end
+
+ it 'transitions from delivery to payment' do
+ allow(persisted_order).to receive_messages(payment_required?: true)
+ persisted_order.next!
+ expect(persisted_order.state).to eq('payment')
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/callbacks_spec.rb b/core/spec/models/spree/order/callbacks_spec.rb
new file mode 100644
index 00000000000..5bc5f8ee775
--- /dev/null
+++ b/core/spec/models/spree/order/callbacks_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Spree::Order, type: :model do
+ let(:order) { stub_model(Spree::Order) }
+
+ before do
+ Spree::Order.define_state_machine!
+ end
+
+ context 'validations' do
+ context 'email validation' do
+ # Regression test for #1238
+ it "o'brien@gmail.com is a valid email address" do
+ order.state = 'address'
+ order.email = "o'brien@gmail.com"
+ expect(order.error_on(:email).size).to eq(0)
+ end
+ end
+ end
+
+ context '#save' do
+ context 'when associated with a registered user' do
+ let(:user) { double(:user, email: 'test@example.com') }
+
+ before do
+ allow(order).to receive_messages user: user
+ end
+
+ it 'assigns the email address of the user' do
+ order.run_callbacks(:create)
+ expect(order.email).to eq(user.email)
+ end
+ end
+ end
+
+ context 'in the cart state' do
+ it 'does not validate email address' do
+ order.state = 'cart'
+ order.email = nil
+ expect(order.error_on(:email).size).to eq(0)
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/checkout_spec.rb b/core/spec/models/spree/order/checkout_spec.rb
new file mode 100644
index 00000000000..0f77633c341
--- /dev/null
+++ b/core/spec/models/spree/order/checkout_spec.rb
@@ -0,0 +1,773 @@
+require 'spec_helper'
+require 'spree/testing_support/order_walkthrough'
+
+describe Spree::Order, type: :model do
+ let(:order) { Spree::Order.new }
+
+ before { create(:store) }
+
+ def assert_state_changed(order, from, to)
+ state_change_exists = order.state_changes.where(previous_state: from, next_state: to).exists?
+ assert state_change_exists, "Expected order to transition from #{from} to #{to}, but didn't."
+ end
+
+ context 'with default state machine' do
+ let(:transitions) do
+ [
+ { address: :delivery },
+ { delivery: :payment },
+ { payment: :confirm },
+ { confirm: :complete },
+ { payment: :complete },
+ { delivery: :complete }
+ ]
+ end
+
+ it 'has the following transitions' do
+ transitions.each do |transition|
+ transition = Spree::Order.find_transition(from: transition.keys.first, to: transition.values.first)
+ expect(transition).not_to be_nil
+ end
+ end
+
+ it 'does not have a transition from delivery to confirm' do
+ transition = Spree::Order.find_transition(from: :delivery, to: :confirm)
+ expect(transition).to be_nil
+ end
+
+ it '.find_transition when contract was broken' do
+ expect(Spree::Order.find_transition(foo: :bar, baz: :dog)).to be_falsey
+ end
+
+ it '.remove_transition' do
+ options = { from: transitions.first.keys.first, to: transitions.first.values.first }
+ expect(Spree::Order).to receive_messages(
+ removed_transitions: [],
+ next_event_transitions: transitions.dup
+ )
+ expect(Spree::Order.remove_transition(options)).to be_truthy
+ expect(Spree::Order.removed_transitions).to eql([options])
+ expect(Spree::Order.next_event_transitions).not_to include(transitions.first)
+ end
+
+ it '.remove_transition when contract was broken' do
+ expect(Spree::Order.remove_transition(nil)).to be_falsey
+ end
+
+ it 'always return integer on checkout_step_index' do
+ expect(order.checkout_step_index('imnotthere')).to be_a Integer
+ expect(order.checkout_step_index('delivery')).to be > 0
+ end
+
+ it 'passes delivery state when transitioning from address over delivery to payment' do
+ allow(order).to receive_messages payment_required?: true
+ order.state = 'address'
+ expect(order.passed_checkout_step?('delivery')).to be false
+ order.state = 'delivery'
+ expect(order.passed_checkout_step?('delivery')).to be false
+ order.state = 'payment'
+ expect(order.passed_checkout_step?('delivery')).to be true
+ end
+
+ context '#checkout_steps' do
+ context 'when confirmation not required' do
+ before do
+ allow(order).to receive_messages confirmation_required?: false
+ allow(order).to receive_messages payment_required?: true
+ end
+
+ specify do
+ expect(order.checkout_steps).to eq(%w(address delivery payment complete))
+ end
+ end
+
+ context 'when confirmation required' do
+ before do
+ allow(order).to receive_messages confirmation_required?: true
+ allow(order).to receive_messages payment_required?: true
+ end
+
+ specify do
+ expect(order.checkout_steps).to eq(%w(address delivery payment confirm complete))
+ end
+ end
+
+ context 'when payment not required' do
+ before { allow(order).to receive_messages payment_required?: false }
+
+ specify do
+ expect(order.checkout_steps).to eq(%w(address delivery complete))
+ end
+ end
+
+ context 'when payment required' do
+ before { allow(order).to receive_messages payment_required?: true }
+
+ specify do
+ expect(order.checkout_steps).to eq(%w(address delivery payment complete))
+ end
+ end
+ end
+
+ it 'starts out at cart' do
+ expect(order.state).to eq('cart')
+ end
+
+ context 'to address' do
+ before do
+ order.email = 'user@example.com'
+ order.save!
+ end
+
+ context 'with a line item' do
+ before do
+ order.line_items << FactoryBot.create(:line_item)
+ end
+
+ it 'transitions to address' do
+ order.next!
+ assert_state_changed(order, 'cart', 'address')
+ expect(order.state).to eq('address')
+ end
+
+ it "doesn't raise an error if the default address is invalid" do
+ order.user = mock_model(Spree::LegacyUser, ship_address: Spree::Address.new, bill_address: Spree::Address.new)
+ expect { order.next! }.not_to raise_error
+ end
+
+ context 'with default addresses' do
+ let(:default_address) { FactoryBot.create(:address) }
+
+ before do
+ order.user = FactoryBot.create(:user, "#{address_kind}_address" => default_address)
+ order.next!
+ order.reload
+ end
+
+ shared_examples 'it cloned the default address' do
+ it do
+ default_attributes = default_address.attributes
+ order_attributes = order.send("#{address_kind}_address".to_sym).try(:attributes) || {}
+
+ expect(order_attributes.except('id', 'created_at', 'updated_at')).to eql(default_attributes.except('id', 'created_at', 'updated_at'))
+ end
+ end
+
+ it_behaves_like 'it cloned the default address' do
+ let(:address_kind) { 'ship' }
+ end
+
+ it_behaves_like 'it cloned the default address' do
+ let(:address_kind) { 'bill' }
+ end
+ end
+ end
+
+ it 'cannot transition to address without any line items' do
+ expect(order.line_items).to be_blank
+ expect { order.next! }.to raise_error(StateMachines::InvalidTransition, /#{Spree.t(:there_are_no_items_for_this_order)}/)
+ end
+ end
+
+ context 'from address' do
+ before do
+ order.state = 'address'
+ allow(order).to receive(:has_available_payment)
+ create(:shipment, order: order)
+ order.email = 'user@example.com'
+ order.save!
+ end
+
+ it 'updates totals' do
+ allow(order).to receive_messages(ensure_available_shipping_rates: true)
+ line_item = FactoryBot.create(:line_item, price: 10, adjustment_total: 10)
+ line_item.variant.update_attributes!(price: 10)
+ order.line_items << line_item
+ tax_rate = create(:tax_rate, tax_category: line_item.tax_category, amount: 0.05)
+ allow(Spree::TaxRate).to receive_messages match: [tax_rate]
+ FactoryBot.create(:tax_adjustment, adjustable: line_item, source: tax_rate, order: order)
+ order.email = 'user@example.com'
+ order.next!
+ expect(order.adjustment_total).to eq(0.5)
+ expect(order.additional_tax_total).to eq(0.5)
+ expect(order.included_tax_total).to eq(0)
+ expect(order.total).to eq(10.5)
+ end
+
+ it 'updates prices' do
+ allow(order).to receive_messages(ensure_available_shipping_rates: true)
+ line_item = FactoryBot.create(:line_item, price: 10, adjustment_total: 10)
+ line_item.variant.update_attributes!(price: 20)
+ order.line_items << line_item
+ tax_rate = create :tax_rate,
+ included_in_price: true,
+ tax_category: line_item.tax_category,
+ amount: 0.05
+ allow(Spree::TaxRate).to receive_messages(match: [tax_rate])
+ FactoryBot.create :tax_adjustment,
+ adjustable: line_item,
+ source: tax_rate,
+ order: order
+ order.email = 'user@example.com'
+ order.next!
+ expect(order.adjustment_total).to eq(0)
+ expect(order.additional_tax_total).to eq(0)
+ expect(order.included_tax_total).to eq(0.95)
+ expect(order.total).to eq(20)
+ end
+
+ it 'transitions to delivery' do
+ allow(order).to receive_messages(ensure_available_shipping_rates: true)
+ order.next!
+ assert_state_changed(order, 'address', 'delivery')
+ expect(order.state).to eq('delivery')
+ end
+
+ it 'does not call persist_order_address if there is no address on the order' do
+ # otherwise, it will crash
+ allow(order).to receive_messages(ensure_available_shipping_rates: true)
+
+ order.user = FactoryBot.create(:user)
+ order.save!
+
+ expect(order.user).not_to receive(:persist_order_address).with(order)
+ order.next!
+ end
+
+ it "calls persist_order_address on the order's user" do
+ allow(order).to receive_messages(ensure_available_shipping_rates: true)
+
+ order.user = FactoryBot.create(:user)
+ order.ship_address = FactoryBot.create(:address)
+ order.bill_address = FactoryBot.create(:address)
+ order.save!
+
+ expect(order.user).to receive(:persist_order_address).with(order)
+ order.next!
+ end
+
+ it "does not call persist_order_address on the order's user for a temporary address" do
+ allow(order).to receive_messages(ensure_available_shipping_rates: true)
+
+ order.user = FactoryBot.create(:user)
+ order.temporary_address = true
+ order.save!
+
+ expect(order.user).not_to receive(:persist_order_address)
+ order.next!
+ end
+
+ context 'cannot transition to delivery' do
+ context 'with an existing shipment' do
+ before do
+ line_item = FactoryBot.create(:line_item, price: 10)
+ order.line_items << line_item
+ end
+
+ context 'if there are no shipping rates for any shipment' do
+ it 'raises an InvalidTransitionError' do
+ transition = -> { order.next! }
+ expect(transition).to raise_error(StateMachines::InvalidTransition, /#{Spree.t(:items_cannot_be_shipped)}/)
+ end
+
+ it 'deletes all the shipments' do
+ order.next
+ expect(order.shipments).to be_empty
+ end
+ end
+ end
+ end
+ end
+
+ context 'to delivery' do
+ context 'when order has default selected_shipping_rate_id' do
+ let(:shipment) { create(:shipment, order: order) }
+ let(:shipping_method) { create(:shipping_method) }
+ let(:shipping_rate) do
+ [
+ Spree::ShippingRate.create!(shipping_method: shipping_method, cost: 10.00, shipment: shipment)
+ ]
+ end
+
+ before do
+ order.state = 'address'
+ shipment.selected_shipping_rate_id = shipping_rate.first.id
+ order.email = 'user@example.com'
+ order.save!
+
+ allow(order).to receive(:has_available_payment)
+ allow(order).to receive(:create_proposed_shipments)
+ allow(order).to receive(:ensure_available_shipping_rates).and_return(true)
+ end
+
+ it 'invokes set_shipment_cost' do
+ expect(order).to receive(:set_shipments_cost)
+ order.next!
+ end
+
+ it 'updates shipment_total' do
+ expect { order.next! }.to change(order, :shipment_total).by(10.00)
+ end
+ end
+ end
+
+ context 'from delivery' do
+ before do
+ order.state = 'delivery'
+ allow(order).to receive(:apply_free_shipping_promotions)
+ end
+
+ it 'attempts to apply free shipping promotions' do
+ expect(order).to receive(:apply_free_shipping_promotions)
+ order.next!
+ end
+
+ context 'with payment required' do
+ before do
+ allow(order).to receive_messages payment_required?: true
+ end
+
+ it 'transitions to payment' do
+ expect(order).to receive(:set_shipments_cost)
+ order.next!
+ assert_state_changed(order, 'delivery', 'payment')
+ expect(order.state).to eq('payment')
+ end
+ end
+
+ context 'without payment required' do
+ before do
+ allow(order).to receive_messages payment_required?: false
+ end
+
+ it 'transitions to complete' do
+ order.next!
+ expect(order.state).to eq('complete')
+ end
+ end
+
+ context 'correctly determining payment required based on shipping information' do
+ let(:shipment) do
+ FactoryBot.create(:shipment)
+ end
+
+ before do
+ # Needs to be set here because we're working with a persisted order object
+ order.email = 'test@example.com'
+ order.save!
+ order.shipments << shipment
+ end
+
+ context 'with a shipment that has a price' do
+ before do
+ shipment.shipping_rates.first.update_column(:cost, 10)
+ order.set_shipments_cost
+ end
+
+ it 'transitions to payment' do
+ order.next!
+ expect(order.state).to eq('payment')
+ end
+ end
+
+ context 'with a shipment that is free' do
+ before do
+ shipment.shipping_rates.first.update_column(:cost, 0)
+ order.set_shipments_cost
+ end
+
+ it 'skips payment, transitions to complete' do
+ order.next!
+ expect(order.state).to eq('complete')
+ end
+ end
+ end
+ end
+
+ context 'from payment' do
+ before do
+ order.state = 'payment'
+ end
+
+ context 'with confirmation required' do
+ before do
+ allow(order).to receive_messages confirmation_required?: true
+ end
+
+ it 'transitions to confirm' do
+ order.next!
+ assert_state_changed(order, 'payment', 'confirm')
+ expect(order.state).to eq('confirm')
+ end
+ end
+
+ context 'without confirmation required' do
+ before do
+ order.email = 'spree@example.com'
+ allow(order).to receive_messages confirmation_required?: false
+ allow(order).to receive_messages payment_required?: true
+ order.payments << FactoryBot.create(:payment, state: payment_state, order: order)
+ end
+
+ context 'when there is at least one valid payment' do
+ let(:payment_state) { 'checkout' }
+
+ context 'line_items are in stock' do
+ before do
+ expect(order).to receive(:process_payments!).once.and_return(true)
+ end
+
+ it 'transitions to complete' do
+ order.next!
+ assert_state_changed(order, 'payment', 'complete')
+ expect(order.state).to eq('complete')
+ end
+ end
+
+ context 'line_items are not in stock' do
+ before do
+ expect(order).to receive(:ensure_line_items_are_in_stock).once.and_return(false)
+ end
+
+ it 'does not receive process_payments!' do
+ expect(order).not_to receive(:process_payments!)
+ order.next
+ end
+
+ it 'does not transition to complete' do
+ order.next
+ expect(order.state).to eq('payment')
+ end
+ end
+ end
+
+ context 'when there is only an invalid payment' do
+ let(:payment_state) { 'failed' }
+
+ it 'raises a StateMachine::InvalidTransition' do
+ expect do
+ order.next!
+ end.to raise_error(StateMachines::InvalidTransition, /#{Spree.t(:no_payment_found)}/)
+
+ expect(order.errors[:base]).to include(Spree.t(:no_payment_found))
+ end
+ end
+ end
+
+ # Regression test for #2028
+ context 'when payment is not required' do
+ before do
+ allow(order).to receive_messages payment_required?: false
+ end
+
+ it 'does not call process payments' do
+ expect(order).not_to receive(:process_payments!)
+ order.next!
+ assert_state_changed(order, 'payment', 'complete')
+ expect(order.state).to eq('complete')
+ end
+ end
+ end
+ end
+
+ context 'to complete' do
+ before do
+ order.state = 'confirm'
+ order.save!
+ end
+
+ context 'default credit card' do
+ before do
+ order.user = FactoryBot.create(:user)
+ order.email = 'spree@example.org'
+ order.payments << FactoryBot.create(:payment)
+
+ # make sure we will actually capture a payment
+ allow(order).to receive_messages(payment_required?: true)
+ order.line_items << FactoryBot.create(:line_item)
+ Spree::OrderUpdater.new(order).update
+
+ order.save!
+ end
+
+ it "makes the current credit card a user's default credit card" do
+ order.next!
+ expect(order.state).to eq 'complete'
+ expect(order.user.reload.default_credit_card.try(:id)).to eq(order.credit_cards.first.id)
+ end
+
+ it 'does not assign a default credit card if temporary_credit_card is set' do
+ order.temporary_credit_card = true
+ order.next!
+ expect(order.user.reload.default_credit_card).to be_nil
+ end
+ end
+ end
+
+ context 'subclassed order' do
+ # This causes another test above to fail, but fixing this test should make
+ # the other test pass
+ class SubclassedOrder < Spree::Order
+ checkout_flow do
+ go_to_state :payment
+ go_to_state :complete
+ end
+ end
+
+ skip 'should only call default transitions once when checkout_flow is redefined' do
+ order = SubclassedOrder.new
+ allow(order).to receive_messages payment_required?: true
+ expect(order).to receive(:process_payments!).once
+ order.state = 'payment'
+ order.next!
+ assert_state_changed(order, 'payment', 'complete')
+ expect(order.state).to eq('complete')
+ end
+ end
+
+ context 're-define checkout flow' do
+ before do
+ @old_checkout_flow = Spree::Order.checkout_flow
+ Spree::Order.class_eval do
+ checkout_flow do
+ go_to_state :payment
+ go_to_state :complete
+ end
+ end
+ end
+
+ after do
+ Spree::Order.checkout_flow(&@old_checkout_flow)
+ end
+
+ it 'does not keep old event transitions when checkout_flow is redefined' do
+ expect(Spree::Order.next_event_transitions).to eq([{ cart: :payment }, { payment: :complete }])
+ end
+
+ it 'does not keep old events when checkout_flow is redefined' do
+ state_machine = Spree::Order.state_machine
+ expect(state_machine.states.any? { |s| s.name == :address }).to be false
+ known_states = state_machine.events[:next].branches.map(&:known_states).flatten
+ expect(known_states).not_to include(:address)
+ expect(known_states).not_to include(:delivery)
+ expect(known_states).not_to include(:confirm)
+ end
+ end
+
+ # Regression test for #3665
+ context 'with only a complete step' do
+ before do
+ @old_checkout_flow = Spree::Order.checkout_flow
+ Spree::Order.class_eval do
+ checkout_flow do
+ go_to_state :complete
+ end
+ end
+ end
+
+ after do
+ Spree::Order.checkout_flow(&@old_checkout_flow)
+ end
+
+ it 'does not attempt to process payments' do
+ allow(order).to receive_message_chain(:line_items, :present?) { true }
+ allow(order).to receive(:ensure_line_items_are_in_stock).and_return(true)
+ allow(order).to receive(:ensure_line_item_variants_are_not_discontinued).and_return(true)
+ expect(order).not_to receive(:payment_required?)
+ expect(order).not_to receive(:process_payments!)
+ order.next!
+ assert_state_changed(order, 'cart', 'complete')
+ end
+ end
+
+ context 'insert checkout step' do
+ before do
+ @old_checkout_flow = Spree::Order.checkout_flow
+ Spree::Order.class_eval do
+ insert_checkout_step :new_step, before: :address
+ end
+ end
+
+ after do
+ Spree::Order.checkout_flow(&@old_checkout_flow)
+ end
+
+ it 'maintains removed transitions' do
+ transition = Spree::Order.find_transition(from: :delivery, to: :confirm)
+ expect(transition).to be_nil
+ end
+
+ context 'before' do
+ before do
+ Spree::Order.class_eval do
+ insert_checkout_step :before_address, before: :address
+ end
+ end
+
+ specify do
+ order = Spree::Order.new
+ expect(order.checkout_steps).to eq(%w(new_step before_address address delivery complete))
+ end
+
+ it 'goes through checkout without raising error' do
+ expect { OrderWalkthrough.up_to(:complete) }.not_to raise_error
+ end
+ end
+
+ context 'after' do
+ before do
+ Spree::Order.class_eval do
+ insert_checkout_step :after_address, after: :address
+ end
+ end
+
+ specify do
+ order = Spree::Order.new
+ expect(order.checkout_steps).to eq(%w(new_step address after_address delivery complete))
+ end
+
+ it 'goes through checkout without raising error' do
+ expect { OrderWalkthrough.up_to(:complete) }.not_to raise_error
+ end
+ end
+ end
+
+ context 'remove checkout step' do
+ before do
+ @old_checkout_flow = Spree::Order.checkout_flow
+ Spree::Order.class_eval do
+ remove_checkout_step :address
+ end
+ end
+
+ after do
+ Spree::Order.checkout_flow(&@old_checkout_flow)
+ end
+
+ it 'maintains removed transitions' do
+ transition = Spree::Order.find_transition(from: :delivery, to: :confirm)
+ expect(transition).to be_nil
+ end
+
+ specify do
+ order = Spree::Order.new
+ expect(order.checkout_steps).to eq(%w(delivery complete))
+ end
+ end
+
+ describe 'update_from_params' do
+ let(:permitted_params) { {} }
+ let(:params) { {} }
+
+ it 'calls update_atributes without order params' do
+ expect(order).to receive(:update_attributes).with({})
+ order.update_from_params(params, permitted_params)
+ end
+
+ it 'runs the callbacks' do
+ expect(order).to receive(:run_callbacks).with(:updating_from_params)
+ order.update_from_params(params, permitted_params)
+ end
+
+ context 'passing a credit card' do
+ let(:permitted_params) do
+ Spree::PermittedAttributes.checkout_attributes +
+ [payments_attributes: Spree::PermittedAttributes.payment_attributes]
+ end
+
+ let(:credit_card) { create(:credit_card, user_id: order.user_id) }
+
+ let(:params) do
+ ActionController::Parameters.new(
+ order: { payments_attributes: [{ payment_method_id: 1 }], existing_card: credit_card.id },
+ cvc_confirm: '737',
+ payment_source: {
+ '1' => { name: 'Luis Braga',
+ number: '4111 1111 1111 1111',
+ expiry: '06 / 2016',
+ verification_value: '737',
+ cc_type: '' }
+ }
+ )
+ end
+
+ before { order.user_id = 3 }
+
+ it 'sets confirmation value when its available via :cvc_confirm' do
+ allow(Spree::CreditCard).to receive_messages find: credit_card
+ expect(credit_card).to receive(:verification_value=)
+ order.update_from_params(params, permitted_params)
+ end
+
+ it 'sets existing card as source for new payment' do
+ expect do
+ order.update_from_params(params, permitted_params)
+ end.to change { Spree::Payment.count }.by(1)
+
+ expect(Spree::Payment.last.source).to eq credit_card
+ end
+
+ it 'sets request_env on payment' do
+ request_env = { 'USER_AGENT' => 'Firefox' }
+
+ order.update_from_params(params, permitted_params, request_env)
+ expect(order.payments[0].request_env).to eq request_env
+ end
+
+ it 'dont let users mess with others users cards' do
+ credit_card.update_column :user_id, 5
+
+ expect do
+ order.update_from_params(params, permitted_params)
+ end.to raise_error(Spree.t(:invalid_credit_card))
+ end
+ end
+
+ context 'has params' do
+ let(:permitted_params) { [:good_param] }
+ let(:params) { ActionController::Parameters.new(order: { bad_param: 'okay' }) }
+
+ it 'does not let through unpermitted attributes' do
+ expect(order).to receive(:update_attributes).with(ActionController::Parameters.new.permit!)
+ order.update_from_params(params, permitted_params)
+ end
+
+ context 'has existing_card param' do
+ let(:permitted_params) do
+ Spree::PermittedAttributes.checkout_attributes +
+ [payments_attributes: Spree::PermittedAttributes.payment_attributes]
+ end
+ let(:credit_card) { create(:credit_card, user_id: order.user_id) }
+ let(:params) do
+ ActionController::Parameters.new(
+ order: { payments_attributes: [{ payment_method_id: 1 }], existing_card: credit_card.id }
+ )
+ end
+
+ before do
+ Dummy::Application.config.action_controller.action_on_unpermitted_parameters = :raise
+ order.user_id = 3
+ end
+
+ after do
+ Dummy::Application.config.action_controller.action_on_unpermitted_parameters = :log
+ end
+
+ it 'does not attempt to permit existing_card' do
+ expect do
+ order.update_from_params(params, permitted_params)
+ end.not_to raise_error
+ end
+ end
+
+ context 'has allowed params' do
+ let(:params) { ActionController::Parameters.new(order: { good_param: 'okay' }) }
+
+ it 'accepts permitted attributes' do
+ expect(order).to receive(:assign_attributes).with(ActionController::Parameters.new('good_param' => 'okay').permit!)
+ order.update_from_params(params, permitted_params)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/currency_updater_spec.rb b/core/spec/models/spree/order/currency_updater_spec.rb
new file mode 100644
index 00000000000..8bcc226b25a
--- /dev/null
+++ b/core/spec/models/spree/order/currency_updater_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Spree::Order, type: :model do
+ context 'CurrencyUpdater' do
+ context 'when changing order currency' do
+ let!(:line_item) { create(:line_item) }
+ let!(:euro_price) { create(:price, variant: line_item.variant, amount: 8, currency: 'EUR') }
+
+ context '#homogenize_line_item_currencies' do
+ it 'succeeds without error' do
+ expect { line_item.order.update_attributes!(currency: 'EUR') }.not_to raise_error
+ end
+
+ it 'changes the line_item currencies' do
+ expect { line_item.order.update_attributes!(currency: 'EUR') }.to change { line_item.reload.currency }.from('USD').to('EUR')
+ end
+
+ it 'changes the line_item amounts' do
+ expect { line_item.order.update_attributes!(currency: 'EUR') }.to change { line_item.reload.amount }.to(8)
+ end
+
+ it 'fails to change the order currency when no prices are available in that currency' do
+ expect { line_item.order.update_attributes!(currency: 'GBP') }.to raise_error("no GBP price found for #{line_item.product.name} (#{line_item.variant.sku})")
+ end
+
+ it 'calculates the item total in the order.currency' do
+ expect { line_item.order.update_attributes!(currency: 'EUR') }.to change { line_item.order.item_total }.to(8)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/finalizing_spec.rb b/core/spec/models/spree/order/finalizing_spec.rb
new file mode 100644
index 00000000000..f2e5bad73fc
--- /dev/null
+++ b/core/spec/models/spree/order/finalizing_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Spree::Order, type: :model do
+ let(:order) { stub_model('Spree::Order') }
+
+ before { create(:store) }
+
+ context '#finalize!' do
+ let(:order) { Spree::Order.create(email: 'test@example.com') }
+
+ before do
+ order.update_column :state, 'complete'
+ end
+
+ after { Spree::Config.set track_inventory_levels: true }
+
+ it 'sets completed_at' do
+ expect(order).to receive(:touch).with(:completed_at)
+ order.finalize!
+ end
+
+ it 'sells inventory units' do
+ order.shipments.each do |shipment| # rubocop:disable RSpec/IteratedExpectation
+ expect(shipment).to receive(:update!)
+ expect(shipment).to receive(:finalize!)
+ end
+ order.finalize!
+ end
+
+ it 'decreases the stock for each variant in the shipment' do
+ order.shipments.each do |shipment|
+ expect(shipment.stock_location).to receive(:decrease_stock_for_variant)
+ end
+ order.finalize!
+ end
+
+ it 'changes the shipment state to ready if order is paid' do
+ Spree::Shipment.create(order: order, stock_location: create(:stock_location))
+ order.shipments.reload
+
+ allow(order).to receive_messages(paid?: true, complete?: true)
+ order.finalize!
+ order.reload # reload so we're sure the changes are persisted
+ expect(order.shipment_state).to eq('ready')
+ end
+
+ it 'does not sell inventory units if track_inventory_levels is false' do
+ Spree::Config.set track_inventory_levels: false
+ expect(Spree::InventoryUnit).not_to receive(:sell_units)
+ order.finalize!
+ end
+
+ it 'sends an order confirmation email' do
+ mail_message = double 'Mail::Message'
+ expect(Spree::OrderMailer).to receive(:confirm_email).with(order.id).and_return mail_message
+ expect(mail_message).to receive :deliver_later
+ order.finalize!
+ end
+
+ it 'sets confirmation delivered when finalizing' do
+ expect(order.confirmation_delivered?).to be false
+ order.finalize!
+ expect(order.confirmation_delivered?).to be true
+ end
+
+ it 'does not send duplicate confirmation emails' do
+ allow(order).to receive_messages(confirmation_delivered?: true)
+ expect(Spree::OrderMailer).not_to receive(:confirm_email)
+ order.finalize!
+ end
+
+ it 'freezes all adjustments' do
+ allow(Spree::OrderMailer).to receive_message_chain :confirm_email, :deliver_later
+ adjustments = [double]
+ expect(order).to receive(:all_adjustments).and_return(adjustments)
+ expect(adjustments).to all(receive(:close))
+ order.finalize!
+ end
+
+ context 'order is considered risky' do
+ before do
+ allow(order).to receive_messages is_risky?: true
+ end
+
+ it 'changes state to risky' do
+ expect(order).to receive(:considered_risky!)
+ order.finalize!
+ end
+
+ context 'and order is approved' do
+ before do
+ allow(order).to receive_messages approved?: true
+ end
+
+ it 'leaves order in complete state' do
+ order.finalize!
+ expect(order.state).to eq 'complete'
+ end
+ end
+ end
+
+ context 'order is not considered risky' do
+ before do
+ allow(order).to receive_messages is_risky?: false
+ end
+
+ it 'sets completed_at' do
+ order.finalize!
+ expect(order.completed_at).to be_present
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/payment_spec.rb b/core/spec/models/spree/order/payment_spec.rb
new file mode 100644
index 00000000000..35c3f0bc2bf
--- /dev/null
+++ b/core/spec/models/spree/order/payment_spec.rb
@@ -0,0 +1,253 @@
+require 'spec_helper'
+
+module Spree
+ describe Spree::Order, type: :model do
+ let(:order) { stub_model(Spree::Order) }
+ let(:updater) { Spree::OrderUpdater.new(order) }
+
+ context 'processing payments' do
+ before do
+ # So that Payment#purchase! is called during processing
+ Spree::Config[:auto_capture] = true
+
+ allow(order).to receive_message_chain(:line_items, :empty?).and_return(false)
+ allow(order).to receive_messages total: 100
+ end
+
+ it 'processes all checkout payments' do
+ payment_1 = create(:payment, amount: 50)
+ payment_2 = create(:payment, amount: 50)
+ allow(order).to receive(:unprocessed_payments).and_return([payment_1, payment_2])
+
+ order.process_payments!
+ updater.update_payment_state
+ expect(order.payment_state).to eq('paid')
+
+ expect(payment_1).to be_completed
+ expect(payment_2).to be_completed
+ end
+
+ it 'does not go over total for order' do
+ payment_1 = create(:payment, amount: 50)
+ payment_2 = create(:payment, amount: 50)
+ payment_3 = create(:payment, amount: 50)
+ allow(order).to receive(:unprocessed_payments).and_return([payment_1, payment_2, payment_3])
+
+ order.process_payments!
+ updater.update_payment_state
+ expect(order.payment_state).to eq('paid')
+
+ expect(payment_1).to be_completed
+ expect(payment_2).to be_completed
+ expect(payment_3).to be_checkout
+ end
+
+ it 'does not use failed payments' do
+ payment_1 = create(:payment, amount: 50)
+ payment_2 = create(:payment, amount: 50, state: 'failed')
+ allow(order).to receive(:pending_payments).and_return([payment_1])
+
+ expect(payment_2).not_to receive(:process!)
+
+ order.process_payments!
+ end
+ end
+
+ context 'ensure source attributes stick around' do
+ # For the reason of this test, please see spree/spree_gateway#132
+ it 'does not have inverse_of defined' do
+ expect(Spree::Order.reflections['payments'].options[:inverse_of]).to be_nil
+ end
+
+ it 'keeps source attributes after updating' do
+ persisted_order = Spree::Order.create
+ credit_card_payment_method = create(:credit_card_payment_method)
+ attributes = {
+ payments_attributes: [
+ {
+ payment_method_id: credit_card_payment_method.id,
+ source_attributes: {
+ name: 'Ryan Bigg',
+ number: '41111111111111111111',
+ expiry: '01 / 15',
+ verification_value: '123'
+ }
+ }
+ ]
+ }
+
+ persisted_order.update_attributes(attributes)
+ expect(persisted_order.unprocessed_payments.last.source.number).to be_present
+ end
+ end
+
+ context 'checking if order is paid' do
+ context 'payment_state is paid' do
+ before { allow(order).to receive_messages payment_state: 'paid' }
+
+ it { expect(order).to be_paid }
+ end
+
+ context 'payment_state is credit_owned' do
+ before { allow(order).to receive_messages payment_state: 'credit_owed' }
+
+ it { expect(order).to be_paid }
+ end
+ end
+
+ context '#process_payments!' do
+ let!(:order) { create(:order_with_line_items) }
+ let!(:payment) do
+ payment = create(:payment, amount: 10, order: order)
+ order.payments << payment
+ payment
+ end
+
+ before { allow(order).to receive_messages unprocessed_payments: [payment], total: 10 }
+
+ it 'processes the payments' do
+ expect(payment).to receive(:process!)
+ expect(order.process_payments!).to be_truthy
+ end
+
+ # Regression spec for https://github.com/spree/spree/issues/5436
+ it 'raises an error if there are no payments to process' do
+ allow(order).to receive_messages unprocessed_payments: []
+ expect(payment).not_to receive(:process!)
+ expect(order.process_payments!).to be_falsey
+ end
+
+ context 'when a payment raises a GatewayError' do
+ before { expect(payment).to receive(:process!).and_raise(Spree::Core::GatewayError) }
+
+ it 'returns true when configured to allow checkout on gateway failures' do
+ Spree::Config.set allow_checkout_on_gateway_error: true
+ expect(order.process_payments!).to be true
+ end
+
+ it 'returns false when not configured to allow checkout on gateway failures' do
+ Spree::Config.set allow_checkout_on_gateway_error: false
+ expect(order.process_payments!).to be false
+ end
+ end
+
+ # Regression spec for https://github.com/spree/spree/issues/8148
+
+ it 'updates order with correct payment total' do
+ Spree::Config[:auto_capture] = true
+ order.process_payments!
+
+ expect(payment).to be_completed
+ expect(order.payment_total).to eq payment.amount
+ end
+ end
+
+ context '#authorize_payments!' do
+ subject { order.authorize_payments! }
+
+ let(:payment) { stub_model(Spree::Payment) }
+
+ before { allow(order).to receive_messages unprocessed_payments: [payment], total: 10 }
+
+ it 'processes payments with attempt_authorization!' do
+ expect(payment).to receive(:authorize!)
+ subject
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context '#capture_payments!' do
+ subject { order.capture_payments! }
+
+ let(:payment) { stub_model(Spree::Payment) }
+
+ before { allow(order).to receive_messages unprocessed_payments: [payment], total: 10 }
+
+ it 'processes payments with attempt_authorization!' do
+ expect(payment).to receive(:purchase!)
+ subject
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context '#outstanding_balance' do
+ it 'returns positive amount when payment_total is less than total' do
+ order.payment_total = 20.20
+ order.total = 30.30
+ expect(order.outstanding_balance).to eq(10.10)
+ end
+ it 'returns negative amount when payment_total is greater than total' do
+ order.total = 8.20
+ order.payment_total = 10.20
+ expect(order.outstanding_balance).to be_within(0.001).of(-2.00)
+ end
+ it 'incorporates refund reimbursements' do
+ # Creates an order w/total 10
+ reimbursement = create :reimbursement
+ # Set the payment amount to actually be the order total of 10
+ reimbursement.order.payments.first.update_column :amount, 10
+ # Creates a refund of 10
+ create :refund, amount: 10,
+ payment: reimbursement.order.payments.first,
+ reimbursement: reimbursement
+ order = reimbursement.order.reload
+ # Update the order totals so payment_total goes to 0 reflecting the refund..
+ order.update_with_updater!
+ # Order Total - (Payment Total + Reimbursed)
+ # 10 - (0 + 10) = 0
+ expect(order.outstanding_balance).to eq 0
+ end
+
+ it 'incorporates refunds' do
+ order = create(:completed_order_with_totals)
+ calculator = order.shipments.first.shipping_method.calculator
+
+ calculator.set_preference(:amount, order.shipments.first.cost)
+ calculator.save!
+
+ order.payments << create(:payment, state: :completed, order: order, amount: order.total)
+
+ create(:refund, amount: 10, payment: order.payments.first)
+ order.update_with_updater!
+
+ expect(order.outstanding_balance).to eq 0
+ end
+ end
+
+ context '#outstanding_balance?' do
+ it 'is true when total greater than payment_total' do
+ order.total = 10.10
+ order.payment_total = 9.50
+ expect(order.outstanding_balance?).to be true
+ end
+
+ it 'is true when total less than payment_total' do
+ order.total = 8.25
+ order.payment_total = 10.44
+ expect(order.outstanding_balance?).to be true
+ end
+
+ it 'is false when total equals payment_total' do
+ order.total = 10.10
+ order.payment_total = 10.10
+ expect(order.outstanding_balance?).to be false
+ end
+ end
+
+ context 'payment required?' do
+ context 'total is zero' do
+ before { allow(order).to receive_messages(total: 0) }
+
+ it { expect(order.payment_required?).to be false }
+ end
+
+ context 'total > zero' do
+ before { allow(order).to receive_messages(total: 1) }
+
+ it { expect(order.payment_required?).to be true }
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/risk_assessment_spec.rb b/core/spec/models/spree/order/risk_assessment_spec.rb
new file mode 100644
index 00000000000..309057e2ce1
--- /dev/null
+++ b/core/spec/models/spree/order/risk_assessment_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe Spree::Order, type: :model do
+ let(:order) { stub_model('Spree::Order') }
+
+ describe '.is_risky?' do
+ context 'Not risky order' do
+ let(:order) { FactoryBot.create(:order, payments: [payment]) }
+
+ context 'with avs_response == D' do
+ let(:payment) { FactoryBot.create(:payment, avs_response: 'D') }
+
+ it 'is not considered risky' do
+ expect(order.is_risky?).to eq(false)
+ end
+ end
+
+ context 'with avs_response == M' do
+ let(:payment) { FactoryBot.create(:payment, avs_response: 'M') }
+
+ it 'is not considered risky' do
+ expect(order.is_risky?).to eq(false)
+ end
+ end
+
+ context "with avs_response == ''" do
+ let(:payment) { FactoryBot.create(:payment, avs_response: '') }
+
+ it 'is not considered risky' do
+ expect(order.is_risky?).to eq(false)
+ end
+ end
+
+ context 'with cvv_response_code == M' do
+ let(:payment) { FactoryBot.create(:payment, cvv_response_code: 'M') }
+
+ it 'is not considered risky' do
+ expect(order.is_risky?).to eq(false)
+ end
+ end
+
+ context "with cvv_response_message == ''" do
+ let(:payment) { FactoryBot.create(:payment, cvv_response_message: '') }
+
+ it 'is not considered risky' do
+ expect(order.is_risky?).to eq(false)
+ end
+ end
+ end
+
+ context 'Risky order' do
+ context 'AVS response message' do
+ let(:order) { FactoryBot.create(:order, payments: [FactoryBot.create(:payment, avs_response: 'A')]) }
+
+ it 'returns true if the order has an avs_response' do
+ expect(order.is_risky?).to eq(true)
+ end
+ end
+
+ context 'CVV response code' do
+ let(:order) { FactoryBot.create(:order, payments: [FactoryBot.create(:payment, cvv_response_code: 'N')]) }
+
+ it 'returns true if the order has an cvv_response_code' do
+ expect(order.is_risky?).to eq(true)
+ end
+ end
+
+ context "state == 'failed'" do
+ let(:order) { FactoryBot.create(:order, payments: [FactoryBot.create(:payment, state: 'failed')]) }
+
+ it "returns true if the order has state == 'failed'" do
+ expect(order.is_risky?).to eq(true)
+ end
+ end
+ end
+ end
+
+ context 'is considered risky' do
+ let(:order) do
+ order = FactoryBot.create(:completed_order_with_pending_payment)
+ order.considered_risky!
+ order
+ end
+
+ it 'can be approved by a user' do
+ expect(order).to receive(:approve!)
+ order.approved_by(stub_model(Spree::LegacyUser, id: 1))
+ expect(order.approver_id).to eq(1)
+ expect(order.approved_at).to be_present
+ expect(order.approved?).to be true
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/shipments_spec.rb b/core/spec/models/spree/order/shipments_spec.rb
new file mode 100644
index 00000000000..d161b7ffbb6
--- /dev/null
+++ b/core/spec/models/spree/order/shipments_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Spree::Order, type: :model do
+ let(:order) { create(:order_with_totals) }
+
+ context 'ensure shipments will be updated' do
+ before { Spree::Shipment.create!(order: order, stock_location: create(:stock_location)) }
+
+ it 'destroys current shipments' do
+ order.ensure_updated_shipments
+ expect(order.shipments).to be_empty
+ end
+
+ it 'puts order back in address state' do
+ order.ensure_updated_shipments
+ expect(order.state).to eq 'address'
+ end
+
+ it 'resets shipment_total' do
+ order.update_column(:shipment_total, 5)
+ order.ensure_updated_shipments
+ expect(order.shipment_total).to eq(0)
+ end
+
+ context "except when order is completed, that's OrderInventory job" do
+ it "doesn't touch anything" do
+ allow(order).to receive_messages completed?: true
+ order.update_column(:shipment_total, 5)
+ order.shipments.create!(stock_location: create(:stock_location))
+
+ expect do
+ order.ensure_updated_shipments
+ end.not_to change(order, :shipment_total)
+
+ expect do
+ order.ensure_updated_shipments
+ end.not_to change(order, :shipments)
+
+ expect do
+ order.ensure_updated_shipments
+ end.not_to change(order, :state)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/state_machine_spec.rb b/core/spec/models/spree/order/state_machine_spec.rb
new file mode 100644
index 00000000000..e7b3807299f
--- /dev/null
+++ b/core/spec/models/spree/order/state_machine_spec.rb
@@ -0,0 +1,193 @@
+require 'spec_helper'
+
+describe Spree::Order, type: :model do
+ let(:order) { build(:order) }
+
+ before do
+ # Ensure state machine has been re-defined correctly
+ Spree::Order.define_state_machine!
+ # We don't care about this validation here
+ allow(order).to receive(:require_email)
+ end
+
+ context '#next!' do
+ context 'when current state is confirm' do
+ before do
+ order.state = 'confirm'
+ order.run_callbacks(:create)
+ allow(order).to receive_messages payment_required?: true
+ allow(order).to receive_messages process_payments!: true
+ end
+
+ context 'when payment processing succeeds' do
+ before do
+ order.payments << FactoryBot.create(:payment, state: 'checkout', order: order)
+ allow(order).to receive_messages process_payments: true
+ end
+
+ it 'finalizes order when transitioning to complete state' do
+ expect(order).to receive(:finalize!)
+ order.next!
+ end
+
+ context 'when credit card processing fails' do
+ before { allow(order).to receive_messages process_payments!: false }
+
+ it 'does not complete the order' do
+ order.next
+ expect(order.state).to eq('confirm')
+ end
+ end
+ end
+
+ context 'when payment processing fails' do
+ before { allow(order).to receive_messages process_payments!: false }
+
+ it 'cannot transition to complete' do
+ order.next
+ expect(order.state).to eq('confirm')
+ end
+ end
+ end
+
+ context 'when current state is delivery' do
+ before do
+ allow(order).to receive_messages payment_required?: true
+ allow(order).to receive :apply_free_shipping_promotions
+ order.state = 'delivery'
+ end
+
+ it 'adjusts tax rates when transitioning to delivery' do
+ # Once for the line items
+ expect(Spree::TaxRate).to receive(:adjust).once
+ allow(order).to receive :set_shipments_cost
+ order.next!
+ end
+
+ it 'adjusts tax rates twice if there are any shipments' do
+ # Once for the line items, once for the shipments
+ order.shipments.build stock_location: create(:stock_location)
+ expect(Spree::TaxRate).to receive(:adjust).twice
+ allow(order).to receive :set_shipments_cost
+ order.next!
+ end
+ end
+ end
+
+ context '#can_cancel?' do
+ %w(pending backorder ready).each do |shipment_state|
+ it "should be true if shipment_state is #{shipment_state}" do
+ allow(order).to receive_messages completed?: true
+ order.shipment_state = shipment_state
+ expect(order.can_cancel?).to be true
+ end
+ end
+
+ (Spree::Shipment.state_machine.states.keys - [:pending, :backorder, :ready]).each do |shipment_state|
+ it "should be false if shipment_state is #{shipment_state}" do
+ allow(order).to receive_messages completed?: true
+ order.shipment_state = shipment_state
+ expect(order.can_cancel?).to be false
+ end
+ end
+ end
+
+ context '#cancel' do
+ let!(:variant) { stub_model(Spree::Variant) }
+ let!(:inventory_units) do
+ [stub_model(Spree::InventoryUnit, variant: variant),
+ stub_model(Spree::InventoryUnit, variant: variant)]
+ end
+ let!(:shipment) do
+ shipment = stub_model(Spree::Shipment)
+ allow(shipment).to receive_messages inventory_units: inventory_units, order: order
+ allow(order).to receive_messages shipments: [shipment]
+ shipment
+ end
+
+ before do
+ create_list(:line_item, 2, order: order, price: 10)
+
+ allow(order.line_items).to receive(:find_by).with(hash_including(:variant_id)) { line_items.first }
+
+ allow(order).to receive_messages completed?: true
+ allow(order).to receive_messages allow_cancel?: true
+
+ shipments = [shipment]
+ allow(order).to receive_messages shipments: shipments
+ allow(shipments).to receive_messages states: []
+ allow(shipments).to receive_messages ready: []
+ allow(shipments).to receive_messages pending: []
+ allow(shipments).to receive_messages shipped: []
+ allow(shipments).to receive(:sum).with(:cost).and_return(shipment.cost)
+
+ allow_any_instance_of(Spree::OrderUpdater).to receive(:update_adjustment_total).and_return(10)
+ end
+
+ it 'sends a cancel email' do
+ # Stub methods that cause side-effects in this test
+ allow(shipment).to receive(:cancel!)
+ allow(order).to receive :restock_items!
+ mail_message = double 'Mail::Message'
+ order_id = nil
+ expect(Spree::OrderMailer).to receive(:cancel_email) { |*args|
+ order_id = args[0]
+ mail_message
+ }
+ expect(mail_message).to receive :deliver_later
+ order.cancel!
+ expect(order_id).to eq(order.id)
+ end
+
+ context 'resets payment state' do
+ let(:payment) { create(:payment, amount: order.total) }
+
+ before do
+ # TODO: This is ugly :(
+ # Stubs methods that cause unwanted side effects in this test
+ allow(Spree::OrderMailer).to receive(:cancel_email).and_return(mail_message = double)
+ allow(mail_message).to receive :deliver_later
+ allow(order).to receive :restock_items!
+ allow(shipment).to receive(:cancel!)
+ allow(payment).to receive(:cancel!)
+ allow(order).to receive_message_chain(:payments, :valid, :empty?).and_return(false)
+ allow(order).to receive_message_chain(:payments, :completed).and_return([payment])
+ allow(order).to receive_message_chain(:payments, :completed, :includes).and_return([payment])
+ allow(order).to receive_message_chain(:payments, :last).and_return(payment)
+ allow(order).to receive_message_chain(:payments, :store_credits, :pending).and_return([])
+ end
+
+ context 'without shipped items' do
+ it "sets payment state to 'void'" do
+ expect { order.cancel! }.to change { order.reload.payment_state }.to('void')
+ end
+ end
+
+ context 'with shipped items' do
+ before do
+ allow(order).to receive_messages shipment_state: 'partial'
+ allow(order).to receive_messages outstanding_balance?: false
+ allow(order).to receive_messages payment_state: 'paid'
+ end
+
+ it 'does not alter the payment state' do
+ order.cancel!
+ expect(order.payment_state).to eql 'paid'
+ end
+ end
+
+ context 'with payments' do
+ let(:payment) { create(:payment) }
+
+ it 'automatically refunds all payments' do
+ allow(order).to receive_message_chain(:payments, :valid, :size).and_return(1)
+ allow(order).to receive_message_chain(:payments, :completed).and_return([payment])
+ allow(order).to receive_message_chain(:payments, :completed, :includes).and_return([payment])
+ allow(order).to receive_message_chain(:payments, :last).and_return(payment)
+ expect(payment).to receive(:cancel!)
+ order.cancel!
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/store_credit_spec.rb b/core/spec/models/spree/order/store_credit_spec.rb
new file mode 100644
index 00000000000..e4e7d7ac0ce
--- /dev/null
+++ b/core/spec/models/spree/order/store_credit_spec.rb
@@ -0,0 +1,338 @@
+require 'spec_helper'
+
+shared_examples 'check total store credit from payments' do
+ context 'with valid payments' do
+ subject { order }
+
+ let(:order) { payment.order }
+ let!(:payment) { create(:store_credit_payment) }
+ let!(:second_payment) { create(:store_credit_payment, order: order) }
+
+ it 'returns the sum of the payment amounts' do
+ expect(subject.total_applicable_store_credit).to eq (payment.amount + second_payment.amount)
+ end
+ end
+
+ context 'without valid payments' do
+ subject { order }
+
+ let(:order) { create(:order) }
+
+ it 'returns 0' do
+ expect(subject.total_applicable_store_credit).to be_zero
+ end
+ end
+end
+
+describe 'Order' do
+ describe '#covered_by_store_credit' do
+ context "order doesn't have an associated user" do
+ subject { create(:store_credits_order_without_user) }
+
+ it 'returns false' do
+ expect(subject.covered_by_store_credit).to be false
+ end
+ end
+
+ context 'order has an associated user' do
+ subject { create(:order, user: user) }
+
+ let(:user) { create(:user) }
+
+ context 'user has enough store credit to pay for the order' do
+ before do
+ allow(user).to receive(:total_available_store_credit).and_return(10.0)
+ allow(subject).to receive(:total).and_return(5.0)
+ end
+
+ it 'returns true' do
+ expect(subject.covered_by_store_credit).to be true
+ end
+ end
+
+ context 'user does not have enough store credit to pay for the order' do
+ before do
+ allow(user).to receive(:total_available_store_credit).and_return(0.0)
+ allow(subject).to receive(:total).and_return(5.0)
+ end
+
+ it 'returns false' do
+ expect(subject.covered_by_store_credit).to be false
+ end
+ end
+ end
+ end
+
+ describe '#total_available_store_credit' do
+ context 'order does not have an associated user' do
+ subject { create(:store_credits_order_without_user) }
+
+ it 'returns 0' do
+ expect(subject.total_available_store_credit).to be_zero
+ end
+ end
+
+ context 'order has an associated user' do
+ subject { create(:order, user: user) }
+
+ let(:user) { create(:user) }
+ let(:available_store_credit) { 25.0 }
+
+ before do
+ allow(user).to receive(:total_available_store_credit).and_return(available_store_credit)
+ end
+
+ it "returns the user's available store credit" do
+ expect(subject.total_available_store_credit).to eq available_store_credit
+ end
+ end
+ end
+
+ describe '#could_use_store_credit?' do
+ context 'order does not have an associated user' do
+ subject { create(:store_credits_order_without_user) }
+
+ it { expect(subject.could_use_store_credit?).to be false }
+ end
+
+ context 'order has an associated user' do
+ subject { create(:order, user: user) }
+
+ let(:user) { create(:user) }
+
+ context 'without store credit' do
+ it { expect(subject.could_use_store_credit?).to be false }
+ end
+
+ context 'with store credit' do
+ let(:available_store_credit) { 25.0 }
+
+ before do
+ allow(user).to receive(:total_available_store_credit).and_return(available_store_credit)
+ end
+
+ it { expect(subject.could_use_store_credit?).to be true }
+ end
+ end
+ end
+
+ describe '#order_total_after_store_credit' do
+ subject { create(:order, total: order_total) }
+
+ let(:order_total) { 100.0 }
+
+ before do
+ allow(subject).to receive(:total_applicable_store_credit).and_return(applicable_store_credit)
+ end
+
+ context "order's user has store credits" do
+ let(:applicable_store_credit) { 10.0 }
+
+ it 'deducts the applicable store credit' do
+ expect(subject.order_total_after_store_credit).to eq (order_total - applicable_store_credit)
+ end
+ end
+
+ context "order's user does not have any store credits" do
+ let(:applicable_store_credit) { 0.0 }
+
+ it 'returns the order total' do
+ expect(subject.order_total_after_store_credit).to eq order_total
+ end
+ end
+ end
+
+ describe '#total_applicable_store_credit' do
+ context 'order is in the confirm state' do
+ before { order.update_attributes(state: 'confirm') }
+
+ include_examples 'check total store credit from payments'
+ end
+
+ context 'order is completed' do
+ before { order.update_attributes(state: 'complete') }
+
+ include_examples 'check total store credit from payments'
+ end
+
+ context 'order is in any state other than confirm or complete' do
+ context 'the associated user has store credits' do
+ subject { order }
+
+ let(:store_credit) { create(:store_credit) }
+ let(:order) { create(:order, user: store_credit.user) }
+
+ context 'the store credit is more than the order total' do
+ let(:order_total) { store_credit.amount - 1 }
+
+ before { order.update_attributes(total: order_total) }
+
+ it 'returns the order total' do
+ expect(subject.total_applicable_store_credit).to eq order_total
+ end
+ end
+
+ context 'the store credit is less than the order total' do
+ let(:order_total) { store_credit.amount * 10 }
+
+ before { order.update_attributes(total: order_total) }
+
+ it 'returns the store credit amount' do
+ expect(subject.total_applicable_store_credit).to eq store_credit.amount
+ end
+ end
+ end
+
+ context 'the associated user does not have store credits' do
+ subject { order }
+
+ let(:order) { create(:order) }
+
+ it 'returns 0' do
+ expect(subject.total_applicable_store_credit).to be_zero
+ end
+ end
+
+ context 'the order does not have an associated user' do
+ subject { create(:store_credits_order_without_user) }
+
+ it 'returns 0' do
+ expect(subject.total_applicable_store_credit).to be_zero
+ end
+ end
+ end
+ end
+
+ describe '#total_applied_store_credit' do
+ context 'with valid payments' do
+ subject { order }
+
+ let(:order) { payment.order }
+ let!(:payment) { create(:store_credit_payment) }
+ let!(:second_payment) { create(:store_credit_payment, order: order) }
+
+ it 'returns the sum of the payment amounts' do
+ expect(subject.total_applied_store_credit).to eq (payment.amount + second_payment.amount)
+ end
+ end
+
+ context 'without valid payments' do
+ subject { order }
+
+ let(:order) { create(:order) }
+
+ it 'returns 0' do
+ expect(subject.total_applied_store_credit).to be_zero
+ end
+ end
+ end
+
+ describe '#using_store_credit?' do
+ subject { create(:order) }
+
+ context 'order has store credit payment' do
+ before { allow(subject).to receive(:total_applied_store_credit).and_return(10.0) }
+
+ it { expect(subject.using_store_credit?).to be true }
+ end
+
+ context 'order has no store credit payments' do
+ before { allow(subject).to receive(:total_applied_store_credit).and_return(0.0) }
+
+ it { expect(subject.using_store_credit?).to be false }
+ end
+ end
+
+ describe '#display_total_applicable_store_credit' do
+ subject { create(:order) }
+
+ let(:total_applicable_store_credit) { 10.00 }
+
+ before do
+ allow(subject).to receive(:total_applicable_store_credit).and_return(total_applicable_store_credit)
+ end
+
+ it 'returns a money instance' do
+ expect(subject.display_total_applicable_store_credit).to be_a(Spree::Money)
+ end
+
+ it 'returns a negative amount' do
+ expect(subject.display_total_applicable_store_credit.amount_in_cents).to eq (total_applicable_store_credit * -100.0)
+ end
+ end
+
+ describe '#display_total_applied_store_credit' do
+ subject { create(:order) }
+
+ let(:total_applied_store_credit) { 10.00 }
+
+ before do
+ allow(subject).to receive(:total_applied_store_credit).and_return(total_applied_store_credit)
+ end
+
+ it 'returns a money instance' do
+ expect(subject.display_total_applied_store_credit).to be_a(Spree::Money)
+ end
+
+ it 'returns a negative amount' do
+ expect(subject.display_total_applied_store_credit.amount_in_cents).to eq (total_applied_store_credit * -100.0)
+ end
+ end
+
+ describe '#display_order_total_after_store_credit' do
+ subject { create(:order) }
+
+ let(:order_total_after_store_credit) { 10.00 }
+
+ before do
+ allow(subject).to receive(:order_total_after_store_credit).and_return(order_total_after_store_credit)
+ end
+
+ it 'returns a money instance' do
+ expect(subject.display_order_total_after_store_credit).to be_a(Spree::Money)
+ end
+
+ it 'returns the order_total_after_store_credit amount' do
+ expect(subject.display_order_total_after_store_credit.amount_in_cents).to eq (order_total_after_store_credit * 100.0)
+ end
+ end
+
+ describe '#display_total_available_store_credit' do
+ subject { create(:order) }
+
+ let(:total_available_store_credit) { 10.00 }
+
+ before do
+ allow(subject).to receive(:total_available_store_credit).and_return(total_available_store_credit)
+ end
+
+ it 'returns a money instance' do
+ expect(subject.display_total_available_store_credit).to be_a(Spree::Money)
+ end
+
+ it 'returns the total_available_store_credit amount' do
+ expect(subject.display_total_available_store_credit.amount_in_cents).to eq (total_available_store_credit * 100.0)
+ end
+ end
+
+ describe '#display_store_credit_remaining_after_capture' do
+ subject { create(:order) }
+
+ let(:total_available_store_credit) { 10.00 }
+ let(:total_applicable_store_credit) { 5.00 }
+
+ before do
+ allow(subject).to receive(:total_available_store_credit).and_return(total_available_store_credit)
+ allow(subject).to receive(:total_applicable_store_credit).and_return(total_applicable_store_credit)
+ end
+
+ it 'returns a money instance' do
+ expect(subject.display_store_credit_remaining_after_capture).to be_a(Spree::Money)
+ end
+
+ it "returns all of the user's available store credit minus what's applied to the order amount" do
+ amount_remaining = total_available_store_credit - total_applicable_store_credit
+ expect(subject.display_store_credit_remaining_after_capture.amount_in_cents).to eq (amount_remaining * 100.0)
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/tax_spec.rb b/core/spec/models/spree/order/tax_spec.rb
new file mode 100644
index 00000000000..7134f6b01df
--- /dev/null
+++ b/core/spec/models/spree/order/tax_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+module Spree
+ describe Spree::Order, type: :model do
+ let(:order) { stub_model(Spree::Order) }
+
+ context '#tax_zone' do
+ let(:bill_address) { create :address }
+ let(:ship_address) { create :address }
+ let(:order) { Spree::Order.create(ship_address: ship_address, bill_address: bill_address) }
+ let(:zone) { create :zone }
+
+ context 'when no zones exist' do
+ it 'returns nil' do
+ expect(order.tax_zone).to be_nil
+ end
+ end
+
+ context 'when tax_using_ship_address: true' do
+ before { Spree::Config.set(tax_using_ship_address: true) }
+
+ it 'calculates using ship_address' do
+ expect(Spree::Zone).to receive(:match).at_least(:once).with(ship_address)
+ expect(Spree::Zone).not_to receive(:match).with(bill_address)
+ order.tax_zone
+ end
+ end
+
+ context 'when tax_using_ship_address: false' do
+ before { Spree::Config.set(tax_using_ship_address: false) }
+
+ it 'calculates using bill_address' do
+ expect(Spree::Zone).to receive(:match).at_least(:once).with(bill_address)
+ expect(Spree::Zone).not_to receive(:match).with(ship_address)
+ order.tax_zone
+ end
+ end
+
+ context 'when there is a default tax zone' do
+ before do
+ @default_zone = create(:zone, name: 'foo_zone')
+ allow(Spree::Zone).to receive_messages default_tax: @default_zone
+ end
+
+ context 'when there is a matching zone' do
+ before { allow(Spree::Zone).to receive_messages(match: zone) }
+
+ it 'returns the matching zone' do
+ expect(order.tax_zone).to eq(zone)
+ end
+ end
+
+ context 'when there is no matching zone' do
+ before { allow(Spree::Zone).to receive_messages(match: nil) }
+
+ it 'returns the default tax zone' do
+ expect(order.tax_zone).to eq(@default_zone)
+ end
+ end
+ end
+
+ context 'when no default tax zone' do
+ before { allow(Spree::Zone).to receive_messages default_tax: nil }
+
+ context 'when there is a matching zone' do
+ before { allow(Spree::Zone).to receive_messages(match: zone) }
+
+ it 'returns the matching zone' do
+ expect(order.tax_zone).to eq(zone)
+ end
+ end
+
+ context 'when there is no matching zone' do
+ before { allow(Spree::Zone).to receive_messages(match: nil) }
+
+ it 'returns nil' do
+ expect(order.tax_zone).to be_nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/totals_spec.rb b/core/spec/models/spree/order/totals_spec.rb
new file mode 100644
index 00000000000..438791ed43a
--- /dev/null
+++ b/core/spec/models/spree/order/totals_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+module Spree
+ describe Order, type: :model do
+ let(:order) { Order.create }
+ let(:shirt) { create(:variant) }
+
+ context 'adds item to cart and activates promo' do
+ let(:promotion) { Promotion.create name: 'Huhu' }
+ let(:calculator) { Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) }
+ let!(:action) { Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) }
+
+ before { Spree::Cart::AddItem.call(order: order, variant: shirt).value }
+
+ context 'item quantity changes' do
+ it 'recalculates order adjustments' do
+ expect do
+ Spree::Cart::AddItem.call(order: order, variant: shirt, quantity: 3)
+ end.to change { order.adjustments.eligible.pluck(:amount) }
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/updating_spec.rb b/core/spec/models/spree/order/updating_spec.rb
new file mode 100644
index 00000000000..da6564e0668
--- /dev/null
+++ b/core/spec/models/spree/order/updating_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Spree::Order, type: :model do
+ let(:order) { create(:order) }
+
+ context '#update_with_updater!' do
+ let(:line_items) { [mock_model(Spree::LineItem, amount: 5)] }
+
+ context 'when there are update hooks' do
+ before { Spree::Order.register_update_hook :foo }
+
+ after { Spree::Order.update_hooks.clear }
+
+ it 'calls each of the update hooks' do
+ expect(order).to receive :foo
+ order.update_with_updater!
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order/validations_spec.rb b/core/spec/models/spree/order/validations_spec.rb
new file mode 100644
index 00000000000..6a112f8060d
--- /dev/null
+++ b/core/spec/models/spree/order/validations_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+module Spree
+ describe Spree::Order, type: :model do
+ context 'validations' do
+ # Regression test for #2214
+ it 'does not return two error messages when email is blank' do
+ order = Spree::Order.new
+ allow(order).to receive_messages(require_email: true)
+ order.valid?
+ expect(order.errors[:email]).to eq(["can't be blank"])
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order_contents_spec.rb b/core/spec/models/spree/order_contents_spec.rb
new file mode 100644
index 00000000000..0539b12df5a
--- /dev/null
+++ b/core/spec/models/spree/order_contents_spec.rb
@@ -0,0 +1,332 @@
+require 'spec_helper'
+
+describe Spree::OrderContents, type: :model do
+ subject { described_class.new(order) }
+
+ let(:order) { Spree::Order.create }
+ let(:variant) { create(:variant) }
+
+ context '#add' do
+ context 'given quantity is not explicitly provided' do
+ it 'adds one line item' do
+ line_item = subject.add(variant)
+ expect(line_item.quantity).to eq(1)
+ expect(order.line_items.size).to eq(1)
+ end
+ end
+
+ context 'given a shipment' do
+ it 'ensure shipment calls update_amounts instead of order calling ensure_updated_shipments' do
+ shipment = create(:shipment)
+ expect(subject.order).not_to receive(:ensure_updated_shipments)
+ expect(subject.order).to receive(:refresh_shipment_rates).with(Spree::ShippingMethod::DISPLAY_ON_BACK_END)
+ expect(shipment).to receive(:update_amounts)
+ subject.add(variant, 1, shipment: shipment)
+ end
+ end
+
+ context 'not given a shipment' do
+ it 'ensures updated shipments' do
+ expect(subject.order).to receive(:ensure_updated_shipments)
+ subject.add(variant)
+ end
+ end
+
+ it 'adds line item if one does not exist' do
+ line_item = subject.add(variant, 1)
+ expect(line_item.quantity).to eq(1)
+ expect(order.line_items.size).to eq(1)
+ end
+
+ it 'updates line item if one exists' do
+ subject.add(variant, 1)
+ line_item = subject.add(variant, 1)
+ expect(line_item.quantity).to eq(2)
+ expect(order.line_items.size).to eq(1)
+ end
+
+ it 'updates order totals' do
+ expect(order.item_total.to_f).to eq(0.00)
+ expect(order.total.to_f).to eq(0.00)
+
+ subject.add(variant, 1)
+
+ expect(order.item_total.to_f).to eq(19.99)
+ expect(order.total.to_f).to eq(19.99)
+ end
+
+ context 'when store_credits payment' do
+ let!(:payment) { create(:store_credit_payment, order: order) }
+
+ it { expect { subject.add(variant, 1) }.to change { order.payments.store_credits.count }.by(-1) }
+ end
+
+ context 'running promotions' do
+ let(:promotion) { create(:promotion) }
+ let(:calculator) { Spree::Calculator::FlatRate.new(preferred_amount: 10) }
+
+ shared_context 'discount changes order total' do
+ before { subject.add(variant, 1) }
+
+ it { expect(subject.order.total).not_to eq variant.price }
+ end
+
+ context 'one active order promotion' do
+ let!(:action) { Spree::Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) }
+
+ it 'creates valid discount on order' do
+ subject.add(variant, 1)
+ expect(subject.order.adjustments.to_a.sum(&:amount)).not_to eq 0
+ end
+
+ include_context 'discount changes order total'
+ end
+
+ context 'one active line item promotion' do
+ let!(:action) { Spree::Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) }
+
+ it 'creates valid discount on order' do
+ subject.add(variant, 1)
+ expect(subject.order.line_item_adjustments.to_a.sum(&:amount)).not_to eq 0
+ end
+
+ include_context 'discount changes order total'
+ end
+
+ context 'VAT for variant with percent promotion' do
+ let!(:category) { Spree::TaxCategory.create name: 'Taxable Foo' }
+ let!(:rate) do
+ Spree::TaxRate.create(
+ amount: 0.25,
+ included_in_price: true,
+ calculator: Spree::Calculator::DefaultTax.create,
+ tax_category: category,
+ zone: create(:zone_with_country, default_tax: true)
+ )
+ end
+ let(:variant) { create(:variant, price: 1000) }
+ let(:calculator) { Spree::Calculator::PercentOnLineItem.new(preferred_percent: 50) }
+ let!(:action) { Spree::Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) }
+
+ it 'updates included_tax_total' do
+ expect(order.included_tax_total.to_f).to eq(0.00)
+ subject.add(variant, 1)
+ expect(order.included_tax_total.to_f).to eq(100)
+ end
+
+ it 'updates included_tax_total after adding two line items' do
+ subject.add(variant, 1)
+ expect(order.included_tax_total.to_f).to eq(100)
+ subject.add(variant, 1)
+ expect(order.included_tax_total.to_f).to eq(200)
+ end
+ end
+ end
+ end
+
+ context '#remove' do
+ context 'given an invalid variant' do
+ it 'raises an exception' do
+ expect do
+ subject.remove(variant, 1)
+ end.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'given quantity is not explicitly provided' do
+ it 'removes one line item' do
+ line_item = subject.add(variant, 3)
+ subject.remove(variant)
+
+ expect(line_item.quantity).to eq(2)
+ end
+ end
+
+ context 'given a shipment' do
+ it 'ensure shipment calls update_amounts instead of order calling ensure_updated_shipments' do
+ subject.add(variant, 1) # line item
+ shipment = create(:shipment)
+ expect(subject.order).not_to receive(:ensure_updated_shipments)
+ expect(shipment).to receive(:update_amounts)
+ subject.remove(variant, 1, shipment: shipment)
+ end
+ end
+
+ context 'not given a shipment' do
+ it 'ensures updated shipments' do
+ subject.add(variant, 1) # line item
+ expect(subject.order).to receive(:ensure_updated_shipments)
+ subject.remove(variant)
+ end
+ end
+
+ it 'reduces line_item quantity if quantity is less the line_item quantity' do
+ line_item = subject.add(variant, 3)
+ subject.remove(variant, 1)
+
+ expect(line_item.quantity).to eq(2)
+ end
+
+ context 'when store_credits payment' do
+ let(:payment) { create(:store_credit_payment, order: order) }
+
+ before do
+ subject.add(variant, 1)
+ payment
+ end
+
+ it { expect { subject.remove(variant, 1) }.to change { order.payments.store_credits.count }.by(-1) }
+ end
+
+ it 'removes line_item if quantity matches line_item quantity' do
+ subject.add(variant, 1)
+ removed_line_item = subject.remove(variant, 1)
+
+ # Should reflect the change already in Order#line_item
+ expect(order.line_items).not_to include(removed_line_item)
+ end
+
+ it 'updates order totals' do
+ expect(order.item_total.to_f).to eq(0.00)
+ expect(order.total.to_f).to eq(0.00)
+
+ subject.add(variant, 2)
+
+ expect(order.item_total.to_f).to eq(39.98)
+ expect(order.total.to_f).to eq(39.98)
+
+ subject.remove(variant, 1)
+ expect(order.item_total.to_f).to eq(19.99)
+ expect(order.total.to_f).to eq(19.99)
+ end
+ end
+
+ context '#remove_line_item' do
+ context 'given a shipment' do
+ it 'ensure shipment calls update_amounts instead of order calling ensure_updated_shipments' do
+ line_item = subject.add(variant, 1)
+ shipment = create(:shipment)
+ expect(subject.order).not_to receive(:ensure_updated_shipments)
+ expect(shipment).to receive(:update_amounts)
+ subject.remove_line_item(line_item, shipment: shipment)
+ end
+ end
+
+ context 'not given a shipment' do
+ it 'ensures updated shipments' do
+ line_item = subject.add(variant, 1)
+ expect(subject.order).to receive(:ensure_updated_shipments)
+ subject.remove_line_item(line_item)
+ end
+ end
+
+ context 'when store_credits payment' do
+ let(:payment) { create(:store_credit_payment, order: order) }
+
+ before do
+ @line_item = subject.add(variant, 1)
+ payment
+ end
+
+ it { expect { subject.remove_line_item(@line_item) }.to change { order.payments.store_credits.count }.by(-1) }
+ end
+
+ it 'removes line_item' do
+ line_item = subject.add(variant, 1)
+ subject.remove_line_item(line_item)
+
+ expect(order.reload.line_items).not_to include(line_item)
+ end
+
+ it 'updates order totals' do
+ expect(order.item_total.to_f).to eq(0.00)
+ expect(order.total.to_f).to eq(0.00)
+
+ line_item = subject.add(variant, 2)
+
+ expect(order.item_total.to_f).to eq(39.98)
+ expect(order.total.to_f).to eq(39.98)
+
+ subject.remove_line_item(line_item)
+ expect(order.item_total.to_f).to eq(0.00)
+ expect(order.total.to_f).to eq(0.00)
+ end
+ end
+
+ context 'update cart' do
+ let!(:shirt) { subject.add variant, 1 }
+
+ let(:params) do
+ { line_items_attributes: {
+ '0' => { id: shirt.id, quantity: 3 }
+ } }
+ end
+
+ it 'changes item quantity' do
+ subject.update_cart params
+ expect(shirt.quantity).to eq 3
+ end
+
+ it 'updates order totals' do
+ expect do
+ subject.update_cart params
+ end.to change { subject.order.total }
+ end
+
+ context 'when store_credits payment' do
+ let!(:payment) { create(:store_credit_payment, order: order) }
+
+ it { expect { subject.update_cart params }.to change { order.payments.store_credits.count }.by(-1) }
+ end
+
+ context 'submits item quantity 0' do
+ let(:params) do
+ { line_items_attributes: {
+ '0' => { id: shirt.id, quantity: 0 },
+ '1' => { id: '666', quantity: 0 }
+ } }
+ end
+
+ it 'removes item from order' do
+ expect do
+ subject.update_cart params
+ end.to change { subject.order.line_items.count }
+ end
+
+ it 'doesnt try to update unexistent items' do
+ filtered_params = { line_items_attributes: {
+ '0' => { id: shirt.id, quantity: 0 }
+ } }
+ expect(subject.order).to receive(:update_attributes).with(filtered_params)
+ subject.update_cart params
+ end
+
+ it 'does not filter if there is only one line item' do
+ single_line_item_params = { line_items_attributes: { id: shirt.id, quantity: 0 } }
+ expect(subject.order).to receive(:update_attributes).with(single_line_item_params)
+ subject.update_cart single_line_item_params
+ end
+ end
+
+ it 'ensures updated shipments' do
+ expect(subject.order).to receive(:ensure_updated_shipments)
+ subject.update_cart params
+ end
+ end
+
+ context 'completed order' do
+ let(:order) { create(:order, state: 'complete', completed_at: Time.current) }
+
+ before { order.shipments.create! stock_location_id: variant.stock_location_ids.first }
+
+ it 'updates order payment state' do
+ expect do
+ subject.add variant
+ end.to change(order, :payment_state)
+
+ expect do
+ subject.remove variant
+ end.to change(order, :payment_state)
+ end
+ end
+end
diff --git a/core/spec/models/spree/order_inventory_spec.rb b/core/spec/models/spree/order_inventory_spec.rb
new file mode 100644
index 00000000000..970bbe533af
--- /dev/null
+++ b/core/spec/models/spree/order_inventory_spec.rb
@@ -0,0 +1,249 @@
+require 'spec_helper'
+
+describe Spree::OrderInventory, type: :model do
+ subject { described_class.new(order, line_item) }
+
+ let(:order) { create :completed_order_with_totals }
+ let(:line_item) { order.line_items.first }
+
+ context 'when order is missing inventory units' do
+ before { line_item.update_column(:quantity, 2) }
+
+ it 'creates the proper number of inventory units' do
+ subject.verify
+ expect(subject.inventory_units.reload.sum(&:quantity)).to eq 2
+ end
+ end
+
+ context '#add_to_shipment' do
+ let(:shipment) { order.shipments.first }
+
+ context 'order is not completed' do
+ before { allow(order).to receive_messages completed?: false }
+
+ it "doesn't unstock items" do
+ expect(shipment.stock_location).not_to receive(:unstock)
+ expect(subject.send(:add_to_shipment, shipment, 5)).to eq(5)
+ end
+ end
+
+ context 'inventory units state' do
+ before { shipment.inventory_units.destroy_all }
+
+ it 'sets inventory_units state as per stock location availability' do
+ expect(shipment.stock_location).to receive(:fill_status).with(subject.variant, 5).and_return([3, 2])
+
+ expect(subject.send(:add_to_shipment, shipment, 5)).to eq(5)
+
+ units = shipment.inventory_units_for(subject.variant).group_by(&:state)
+ expect(units['backordered'].sum(&:quantity)).to eq(2)
+ expect(units['on_hand'].sum(&:quantity)).to eq(3)
+ end
+ end
+
+ context 'store doesnt track inventory' do
+ let(:variant) { create(:variant) }
+
+ before { Spree::Config.track_inventory_levels = false }
+
+ it 'creates only on hand inventory units' do
+ variant.stock_items.destroy_all
+
+ # The before_save callback in LineItem would verify inventory
+ line_item = Spree::Cart::AddItem.call(order: order, variant: variant, options: { shipment: shipment }).value
+
+ units = shipment.inventory_units_for(line_item.variant)
+ expect(units.sum(&:quantity)).to eq 1
+ expect(units.first).to be_on_hand
+ end
+ end
+
+ context 'variant doesnt track inventory' do
+ let(:variant) { create(:variant) }
+
+ before { variant.track_inventory = false }
+
+ it 'creates only on hand inventory units' do
+ variant.stock_items.destroy_all
+
+ line_item = Spree::Cart::AddItem.call(order: order, variant: variant).value
+ subject.verify(shipment)
+
+ units = shipment.inventory_units_for(line_item.variant)
+ expect(units.sum(&:quantity)).to eq 1
+ expect(units.first).to be_on_hand
+ end
+ end
+
+ it 'creates stock_movement' do
+ expect(subject.send(:add_to_shipment, shipment, 5)).to eq(5)
+
+ stock_item = shipment.stock_location.stock_item(subject.variant)
+ movement = stock_item.stock_movements.last
+ # movement.originator.should == shipment
+ expect(movement.quantity).to eq(-5)
+ end
+ end
+
+ context '#determine_target_shipment' do
+ let(:stock_location) { create :stock_location }
+ let(:variant) { line_item.variant }
+
+ before do
+ allow(line_item).to receive(:changed?).and_return(true)
+ subject.verify
+
+ order.shipments.create(stock_location_id: stock_location.id, cost: 5)
+
+ shipped = order.shipments.create(stock_location_id: order.shipments.first.stock_location.id, cost: 10)
+ shipped.update_column(:state, 'shipped')
+ end
+
+ it 'selects first non-shipped shipment that already contains given variant' do
+ shipment = subject.send(:determine_target_shipment)
+ expect(shipment.shipped?).to be false
+ expect(shipment.inventory_units_for(variant)).not_to be_empty
+
+ expect(variant.stock_location_ids.include?(shipment.stock_location_id)).to be true
+ end
+
+ context 'when no shipments already contain this varint' do
+ before do
+ subject.line_item.reload
+ subject.inventory_units.destroy_all
+ end
+
+ it 'selects first non-shipped shipment that leaves from same stock_location' do
+ shipment = subject.send(:determine_target_shipment)
+ shipment.reload
+ expect(shipment.shipped?).to be false
+ expect(shipment.inventory_units_for(variant)).to be_empty
+ expect(variant.stock_location_ids.include?(shipment.stock_location_id)).to be true
+ end
+ end
+ end
+
+ context 'when order has too many inventory units' do
+ before do
+ line_item.quantity = 3
+ line_item.save!
+
+ line_item.update_column(:quantity, 2)
+ subject.line_item.reload
+ end
+
+ it 'is a messed up order' do
+ expect(order.shipments.first.inventory_units_for(line_item.variant).sum(&:quantity)).to eq(3)
+ expect(line_item.quantity).to eq(2)
+ end
+
+ it 'decreases the number of inventory units' do
+ subject.verify
+ expect(subject.inventory_units.reload.sum(:quantity)).to eq 2
+ end
+
+ context '#remove_from_shipment' do
+ let(:shipment) { order.shipments.first }
+ let(:variant) { subject.variant }
+
+ context 'order is not completed' do
+ before { allow(order).to receive_messages completed?: false }
+
+ it "doesn't restock items" do
+ expect(shipment.stock_location).not_to receive(:restock)
+ expect(subject.send(:remove_from_shipment, shipment, 1)).to eq(1)
+ end
+ end
+
+ it 'destroys backordered units first' do
+ allow(shipment).to receive_messages(
+ inventory_units_for_item: [
+ mock_model(
+ Spree::InventoryUnit, variant_id: variant.id, quantity: 2, state: 'backordered', shipped?: false, backordered?: true
+ ),
+ mock_model(
+ Spree::InventoryUnit, variant_id: variant.id, quantity: 1, state: 'on_hand', shipped?: false, backordered?: false
+ )
+ ]
+ )
+
+ expect(shipment.inventory_units_for_item[0]).to receive(:quantity).and_return(2)
+ expect(shipment.inventory_units_for_item[0]).to receive(:decrement)
+ expect(shipment.inventory_units_for_item[0]).to receive(:save!)
+ expect(shipment.inventory_units_for_item[0]).to receive(:quantity).and_return(1)
+ expect(shipment.inventory_units_for_item[0]).to receive(:destroy)
+ expect(shipment.inventory_units_for_item[1]).to receive(:save!)
+ expect(shipment.inventory_units_for_item[1]).not_to receive(:decrement)
+ expect(shipment.inventory_units_for_item[1]).not_to receive(:destroy)
+ # expect(shipment.inventory_units_for_item[2]).to receive(:destroy)
+
+ expect(subject.send(:remove_from_shipment, shipment, 2)).to eq(2)
+ end
+
+ it 'destroys unshipped units first' do
+ allow(shipment).to receive_messages(
+ inventory_units_for_item: [
+ mock_model(Spree::InventoryUnit, variant_id: variant.id, quantity: 1, state: 'shipped', shipped?: true, backordered?: false),
+ mock_model(Spree::InventoryUnit, variant_id: variant.id, quantity: 1, state: 'on_hand', shipped?: false, backordered?: false)
+ ]
+ )
+
+ allow(shipment.inventory_units_for_item[0]).to receive(:save!)
+ allow(shipment.inventory_units_for_item[1]).to receive(:save!)
+ expect(shipment.inventory_units_for_item[0]).not_to receive(:destroy)
+ expect(shipment.inventory_units_for_item[1]).to receive(:destroy)
+
+ expect(subject.send(:remove_from_shipment, shipment, 1)).to eq(1)
+ end
+
+ it 'only attempts to destroy as many units as are eligible, and return amount destroyed' do
+ allow(shipment).to receive_messages(
+ inventory_units_for_item: [
+ mock_model(Spree::InventoryUnit, variant_id: variant.id, quantity: 1, state: 'shipped', shipped?: true, backordered?: false),
+ mock_model(Spree::InventoryUnit, variant_id: variant.id, quantity: 1, state: 'on_hand', shipped?: false, backordered?: false)
+ ]
+ )
+
+ expect(shipment.inventory_units_for_item[0]).not_to receive(:destroy)
+ expect(shipment.inventory_units_for_item[1]).to receive(:destroy)
+ allow(shipment.inventory_units_for_item[0]).to receive(:save!)
+ allow(shipment.inventory_units_for_item[1]).to receive(:save!)
+
+ expect(subject.send(:remove_from_shipment, shipment, 1)).to eq(1)
+ end
+
+ it 'destroys self if not inventory units remain' do
+ allow(shipment).to receive(:inventory_units).and_return(shipment.inventory_units)
+ allow(shipment.inventory_units).to receive_messages(sum: 0)
+ expect(shipment).to receive(:destroy)
+
+ expect(subject.send(:remove_from_shipment, shipment, 1)).to eq(1)
+ end
+
+ context 'inventory unit line item and variant points to different products' do
+ let(:different_line_item) { create(:line_item) }
+
+ let!(:different_inventory) do
+ shipment.set_up_inventory('on_hand', variant, order, different_line_item)
+ end
+
+ context 'completed order' do
+ before { order.touch :completed_at }
+
+ it 'removes only units that match both line item and variant' do
+ subject.send(:remove_from_shipment, shipment, shipment.inventory_units.sum(&:quantity))
+ expect(different_inventory.reload).to be_persisted
+ end
+ end
+ end
+
+ context 'backordered items are removed' do
+ it 'doesn\'t create on_hand items from backordered items' do
+ shipment.set_up_inventory('backordered', variant, order, line_item)
+
+ expect { subject.send(:remove_from_shipment, shipment, 3) }.to change { line_item.variant.stock_items.sum(:count_on_hand) }.from(-2).to(0)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order_merger_spec.rb b/core/spec/models/spree/order_merger_spec.rb
new file mode 100644
index 00000000000..b0810aa922e
--- /dev/null
+++ b/core/spec/models/spree/order_merger_spec.rb
@@ -0,0 +1,135 @@
+require 'spec_helper'
+
+# Regression tests for #2179
+module Spree
+ describe OrderMerger, type: :model do
+ let(:variant) { create(:variant) }
+ let(:order_1) { Spree::Order.create }
+ let(:order_2) { Spree::Order.create }
+ let(:user) { stub_model(Spree::LegacyUser, email: 'spree@example.com') }
+ let(:subject) { Spree::OrderMerger.new(order_1) }
+
+ it 'destroys the other order' do
+ subject.merge!(order_2)
+ expect { order_2.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'persist the merge' do
+ expect(subject).to receive(:persist_merge)
+ subject.merge!(order_2)
+ end
+
+ context 'user is provided' do
+ it 'assigns user to new order' do
+ subject.merge!(order_2, user)
+ expect(order_1.user).to eq user
+ end
+ end
+
+ context 'merging together two orders with line items for the same variant' do
+ before do
+ order_1.contents.add(variant, 1)
+ order_2.contents.add(variant, 1)
+ end
+
+ specify do
+ subject.merge!(order_2, user)
+ expect(order_1.line_items.count).to eq(1)
+
+ line_item = order_1.line_items.first
+ expect(line_item.quantity).to eq(2)
+ expect(line_item.variant_id).to eq(variant.id)
+ end
+ end
+
+ context 'merging using extension-specific line_item_comparison_hooks' do
+ before do
+ Rails.application.config.spree.line_item_comparison_hooks << :foos_match
+ allow(Spree::Variant).to receive(:price_modifier_amount).and_return(0.00)
+ end
+
+ after do
+ # reset to avoid test pollution
+ Rails.application.config.spree.line_item_comparison_hooks = Set.new
+ end
+
+ context '2 equal line items' do
+ before do
+ @line_item_1 = order_1.contents.add(variant, 1, foos: {})
+ @line_item_2 = order_2.contents.add(variant, 1, foos: {})
+ end
+
+ specify do
+ expect(order_1).to receive(:foos_match).with(@line_item_1, kind_of(Hash)).and_return(true)
+ subject.merge!(order_2)
+ expect(order_1.line_items.count).to eq(1)
+
+ line_item = order_1.line_items.first
+ expect(line_item.quantity).to eq(2)
+ expect(line_item.variant_id).to eq(variant.id)
+ end
+ end
+
+ context '2 different line items' do
+ before do
+ allow(order_1).to receive(:foos_match).and_return(false)
+
+ order_1.contents.add(variant, 1, foos: {})
+ order_2.contents.add(variant, 1, foos: { bar: :zoo })
+ end
+
+ specify do
+ subject.merge!(order_2)
+ expect(order_1.line_items.count).to eq(2)
+
+ line_item = order_1.line_items.first
+ expect(line_item.quantity).to eq(1)
+ expect(line_item.variant_id).to eq(variant.id)
+
+ line_item = order_1.line_items.last
+ expect(line_item.quantity).to eq(1)
+ expect(line_item.variant_id).to eq(variant.id)
+ end
+ end
+ end
+
+ context 'merging together two orders with different line items' do
+ let(:variant_2) { create(:variant) }
+
+ before do
+ order_1.contents.add(variant, 1)
+ order_2.contents.add(variant_2, 1)
+ end
+
+ specify do
+ subject.merge!(order_2)
+ line_items = order_1.line_items.reload
+ expect(line_items.count).to eq(2)
+
+ expect(order_1.item_count).to eq 2
+ expect(order_1.item_total).to eq line_items.map(&:amount).sum
+
+ # No guarantee on ordering of line items, so we do this:
+ expect(line_items.pluck(:quantity)).to match_array([1, 1])
+ expect(line_items.pluck(:variant_id)).to match_array([variant.id, variant_2.id])
+ end
+ end
+
+ context 'merging together orders with invalid line items' do
+ let(:variant_2) { create(:variant) }
+
+ before do
+ order_1.contents.add(variant, 1)
+ order_2.contents.add(variant_2, 1)
+ end
+
+ it 'creates errors with invalid line items' do
+ # we cannot use .destroy here as it will be halted by
+ # :ensure_no_line_items callback
+ variant_2.really_destroy!
+ subject.merge!(order_2)
+ expect(order_1.errors.full_messages).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order_promotion_spec.rb b/core/spec/models/spree/order_promotion_spec.rb
new file mode 100644
index 00000000000..a9abf315a75
--- /dev/null
+++ b/core/spec/models/spree/order_promotion_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Spree::OrderPromotion, type: :model do
+ subject { create(:order_promotion, order: order, promotion: promotion) }
+
+ let(:order) { create(:order_with_line_items) }
+ let(:promotion) { create(:promotion_with_item_adjustment, code: 'test') }
+
+ shared_context 'apply promo' do
+ before do
+ order.coupon_code = promotion.code
+ Spree::PromotionHandler::Coupon.new(order).apply
+ order.save!
+ order.all_adjustments.promotion.update_all(amount: -5.0)
+ end
+ end
+
+ context '#name' do
+ it 'returns the same value as Promotion name' do
+ expect(subject.name).to eq(promotion.name)
+ end
+ end
+
+ context '#description' do
+ it 'returns the same value as Promotion description' do
+ expect(subject.description).to eq(promotion.description)
+ end
+ end
+
+ context '#amount' do
+ include_context 'apply promo'
+
+ it 'equals sum of adjustments created by promotion' do
+ expect(subject.amount).to eq(-5.0)
+ end
+ end
+
+ context '#display_amount' do
+ include_context 'apply promo'
+
+ it 'returns Spree::Money instance with amount value and proper currency' do
+ expect(subject.display_amount.to_s).to eq('-$5.00')
+ end
+
+ context 'different currency' do
+ before { order.currency = 'EUR' }
+
+ it 'return same currency as order' do
+ expect(subject.currency).to eq('EUR')
+ expect(subject.display_amount.currency).to eq('EUR')
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/order_spec.rb b/core/spec/models/spree/order_spec.rb
new file mode 100644
index 00000000000..9afbfd205f1
--- /dev/null
+++ b/core/spec/models/spree/order_spec.rb
@@ -0,0 +1,1148 @@
+require 'spec_helper'
+
+class FakeCalculator < Spree::Calculator
+ def compute(_computable)
+ 5
+ end
+end
+
+describe Spree::Order, type: :model do
+ let(:user) { stub_model(Spree::LegacyUser, email: 'spree@example.com') }
+ let(:order) { stub_model(Spree::Order, user: user) }
+
+ before do
+ create(:store)
+ allow(Spree::LegacyUser).to receive_messages(current: mock_model(Spree::LegacyUser, id: 123))
+ end
+
+ describe '.scopes' do
+ let!(:user) { FactoryBot.create(:user) }
+ let!(:completed_order) { FactoryBot.create(:order, user: user, completed_at: Time.current) }
+ let!(:incompleted_order) { FactoryBot.create(:order, user: user, completed_at: nil) }
+
+ describe '.complete' do
+ it { expect(Spree::Order.complete).to include completed_order }
+ it { expect(Spree::Order.complete).not_to include incompleted_order }
+ end
+
+ describe '.incomplete' do
+ it { expect(Spree::Order.incomplete).to include incompleted_order }
+ it { expect(Spree::Order.incomplete).not_to include completed_order }
+ end
+ end
+
+ describe '#update_with_updater!' do
+ let(:updater) { Spree::OrderUpdater.new(order) }
+
+ before do
+ allow(order).to receive(:updater).and_return(updater)
+ allow(updater).to receive(:update).and_return(true)
+ end
+
+ after { order.update_with_updater! }
+
+ it 'expects to update order with order updater' do
+ expect(updater).to receive(:update).and_return(true)
+ end
+ end
+
+ context '#cancel' do
+ let(:order) { create(:completed_order_with_totals) }
+ let!(:payment) do
+ create(
+ :payment,
+ order: order,
+ amount: order.total,
+ state: 'completed'
+ )
+ end
+ let(:payment_method) { double }
+
+ it 'marks the payments as void' do
+ allow_any_instance_of(Spree::Shipment).to receive(:refresh_rates).and_return(true)
+ order.cancel
+ order.reload
+
+ expect(order.payments.first).to be_void
+ end
+ end
+
+ context '#canceled_by' do
+ subject { order.canceled_by(admin_user) }
+
+ let(:admin_user) { create :admin_user }
+ let(:order) { create :order }
+
+ before do
+ allow(order).to receive(:cancel!)
+ end
+
+ it 'cancels the order' do
+ expect(order).to receive(:cancel!)
+ subject
+ end
+
+ it 'saves canceler_id' do
+ subject
+ expect(order.reload.canceler_id).to eq(admin_user.id)
+ end
+
+ it 'saves canceled_at' do
+ subject
+ expect(order.reload.canceled_at).not_to be_nil
+ end
+
+ it 'has canceler' do
+ subject
+ expect(order.reload.canceler).to eq(admin_user)
+ end
+ end
+
+ context '#create' do
+ let(:order) { Spree::Order.create }
+
+ it 'assigns an order number' do
+ expect(order.number).not_to be_nil
+ end
+
+ it 'creates a randomized 35 character token' do
+ expect(order.token.size).to eq(35)
+ end
+ end
+
+ context 'creates shipments cost' do
+ let(:shipment) { double }
+
+ before { allow(order).to receive_messages shipments: [shipment] }
+
+ it 'update and persist totals' do
+ expect(shipment).to receive :update_amounts
+ expect(order.updater).to receive :update_shipment_total
+ expect(order.updater).to receive :persist_totals
+
+ order.set_shipments_cost
+ end
+ end
+
+ context '#finalize!' do
+ let(:order) { Spree::Order.create(email: 'test@example.com') }
+
+ before do
+ order.update_column :state, 'complete'
+ end
+
+ after { Spree::Config.set track_inventory_levels: true }
+
+ it 'sets completed_at' do
+ expect(order).to receive(:touch).with(:completed_at)
+ order.finalize!
+ end
+
+ it 'sells inventory units' do
+ order.shipments.each do |shipment| # rubocop:disable RSpec/IteratedExpectation
+ expect(shipment).to receive(:update!)
+ expect(shipment).to receive(:finalize!)
+ end
+ order.finalize!
+ end
+
+ it 'decreases the stock for each variant in the shipment' do
+ order.shipments.each do |shipment|
+ expect(shipment.stock_location).to receive(:decrease_stock_for_variant)
+ end
+ order.finalize!
+ end
+
+ it 'changes the shipment state to ready if order is paid' do
+ Spree::Shipment.create(order: order, stock_location: create(:stock_location))
+ order.shipments.reload
+
+ allow(order).to receive_messages(paid?: true, complete?: true)
+ order.finalize!
+ order.reload # reload so we're sure the changes are persisted
+ expect(order.shipment_state).to eq('ready')
+ end
+
+ it 'does not sell inventory units if track_inventory_levels is false' do
+ Spree::Config.set track_inventory_levels: false
+ expect(Spree::InventoryUnit).not_to receive(:sell_units)
+ order.finalize!
+ end
+
+ it 'sends an order confirmation email' do
+ mail_message = double 'Mail::Message'
+ expect(Spree::OrderMailer).to receive(:confirm_email).with(order.id).and_return mail_message
+ expect(mail_message).to receive :deliver_later
+ order.finalize!
+ end
+
+ it 'sets confirmation delivered when finalizing' do
+ expect(order.confirmation_delivered?).to be false
+ order.finalize!
+ expect(order.confirmation_delivered?).to be true
+ end
+
+ it 'does not send duplicate confirmation emails' do
+ allow(order).to receive_messages(confirmation_delivered?: true)
+ expect(Spree::OrderMailer).not_to receive(:confirm_email)
+ order.finalize!
+ end
+
+ it 'freezes all adjustments' do
+ allow(Spree::OrderMailer).to receive_message_chain :confirm_email, :deliver_later
+ adjustments = [double]
+ expect(order).to receive(:all_adjustments).and_return(adjustments)
+ expect(adjustments).to all(receive(:close))
+ order.finalize!
+ end
+
+ context 'order is considered risky' do
+ before do
+ allow(order).to receive_messages is_risky?: true
+ end
+
+ it 'changes state to risky' do
+ expect(order).to receive(:considered_risky!)
+ order.finalize!
+ end
+
+ context 'and order is approved' do
+ before do
+ allow(order).to receive_messages approved?: true
+ end
+
+ it 'leaves order in complete state' do
+ order.finalize!
+ expect(order.state).to eq 'complete'
+ end
+ end
+ end
+ end
+
+ context 'insufficient_stock_lines' do
+ let(:line_item) { mock_model Spree::LineItem, insufficient_stock?: true }
+
+ before { allow(order).to receive_messages(line_items: [line_item]) }
+
+ it 'returns line_item that has insufficient stock on hand' do
+ expect(order.insufficient_stock_lines.size).to eq(1)
+ expect(order.insufficient_stock_lines.include?(line_item)).to be true
+ end
+ end
+
+ describe '#ensure_line_item_variants_are_not_discontinued' do
+ subject { order.ensure_line_item_variants_are_not_discontinued }
+
+ let(:order) { create :order_with_line_items }
+
+ context 'when variant is destroyed' do
+ before do
+ order.line_items.first.variant.discontinue!
+ end
+
+ it 'restarts checkout flow' do
+ expect(order).to receive(:restart_checkout_flow).once
+ subject
+ end
+
+ it 'has error message' do
+ subject
+ expect(order.errors[:base]).to include(Spree.t(:discontinued_variants_present))
+ end
+
+ it 'is false' do
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when no variants are destroyed' do
+ it 'does not restart checkout' do
+ expect(order).not_to receive(:restart_checkout_flow)
+ subject
+ end
+
+ it 'is true' do
+ expect(subject).to be_truthy
+ end
+ end
+ end
+
+ describe '#ensure_line_items_are_in_stock' do
+ subject { order.ensure_line_items_are_in_stock }
+
+ let(:line_item) { create(:line_item, order: order) }
+
+ before do
+ allow(order).to receive(:insufficient_stock_lines).and_return([true])
+ end
+
+ it 'restarts checkout flow' do
+ allow(order).to receive(:restart_checkout_flow)
+ expect(order).to receive(:restart_checkout_flow).once
+ subject
+ end
+
+ it 'has error message' do
+ subject
+ expect(order.errors[:base]).to include(Spree.t(:insufficient_stock_lines_present))
+ end
+
+ it 'is false' do
+ allow(order).to receive(:restart_checkout_flow)
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'empty!' do
+ let(:order) { Spree::Order.create(email: 'test@example.com') }
+ let(:promotion) { create :promotion, code: '10off' }
+
+ before do
+ promotion.orders << order
+ end
+
+ context 'completed order' do
+ before do
+ order.update_columns(state: 'complete', completed_at: Time.current)
+ end
+
+ it 'raises an exception' do
+ expect { order.empty! }.to raise_error(RuntimeError, Spree.t(:cannot_empty_completed_order))
+ end
+ end
+
+ context 'incomplete order' do
+ before do
+ order.empty!
+ end
+
+ it 'clears out line items, adjustments and update totals' do
+ expect(order.line_items.count).to be_zero
+ expect(order.adjustments.count).to be_zero
+ expect(order.shipments.count).to be_zero
+ expect(order.order_promotions.count).to be_zero
+ expect(order.promo_total).to be_zero
+ expect(order.item_total).to be_zero
+ expect(order.empty!).to eq(order)
+ end
+ end
+ end
+
+ context '#display_outstanding_balance' do
+ it 'returns the value as a spree money' do
+ allow(order).to receive(:outstanding_balance).and_return(10.55)
+ expect(order.display_outstanding_balance).to eq(Spree::Money.new(10.55))
+ end
+ end
+
+ context '#display_item_total' do
+ it 'returns the value as a spree money' do
+ allow(order).to receive(:item_total).and_return(10.55)
+ expect(order.display_item_total).to eq(Spree::Money.new(10.55))
+ end
+ end
+
+ context '#display_adjustment_total' do
+ it 'returns the value as a spree money' do
+ order.adjustment_total = 10.55
+ expect(order.display_adjustment_total).to eq(Spree::Money.new(10.55))
+ end
+ end
+
+ context '#display_promo_total' do
+ it 'returns the value as a spree money' do
+ order.promo_total = 10.55
+ expect(order.display_promo_total).to eq(Spree::Money.new(10.55))
+ end
+ end
+
+ context '#display_total' do
+ it 'returns the value as a spree money' do
+ order.total = 10.55
+ expect(order.display_total).to eq(Spree::Money.new(10.55))
+ end
+ end
+
+ context '#currency' do
+ context 'when object currency is ABC' do
+ before { order.currency = 'ABC' }
+
+ it 'returns the currency from the object' do
+ expect(order.currency).to eq('ABC')
+ end
+ end
+ end
+
+ context '#confirmation_required?' do
+ # Regression test for #4117
+ it "is required if the state is currently 'confirm'" do
+ order = Spree::Order.new
+ assert !order.confirmation_required?
+ order.state = 'confirm'
+ assert order.confirmation_required?
+ end
+
+ context 'Spree::Config[:always_include_confirm_step] == true' do
+ before do
+ Spree::Config[:always_include_confirm_step] = true
+ end
+
+ it 'returns true if payments empty' do
+ order = Spree::Order.new
+ assert order.confirmation_required?
+ end
+ end
+
+ context 'Spree::Config[:always_include_confirm_step] == false' do
+ it 'returns false if payments empty and Spree::Config[:always_include_confirm_step] == false' do
+ order = Spree::Order.new
+ assert !order.confirmation_required?
+ end
+
+ it 'does not bomb out when an order has an unpersisted payment' do
+ order = Spree::Order.new
+ order.payments.build
+ assert !order.confirmation_required?
+ end
+ end
+ end
+
+ context 'add_update_hook' do
+ before do
+ Spree::Order.class_eval do
+ register_update_hook :add_awesome_sauce
+ end
+ end
+
+ after do
+ Spree::Order.update_hooks = Set.new
+ end
+
+ it 'calls hook during update' do
+ order = create(:order)
+ expect(order).to receive(:add_awesome_sauce)
+ order.update_with_updater!
+ end
+
+ it 'calls hook during finalize' do
+ order = create(:order)
+ expect(order).to receive(:add_awesome_sauce)
+ order.finalize!
+ end
+ end
+
+ describe '#tax_address' do
+ subject { order.tax_address }
+
+ before { Spree::Config[:tax_using_ship_address] = tax_using_ship_address }
+
+ context 'when tax_using_ship_address is true' do
+ let(:tax_using_ship_address) { true }
+
+ it 'returns ship_address' do
+ expect(subject).to eq(order.ship_address)
+ end
+ end
+
+ context 'when tax_using_ship_address is not true' do
+ let(:tax_using_ship_address) { false }
+
+ it 'returns bill_address' do
+ expect(subject).to eq(order.bill_address)
+ end
+ end
+ end
+
+ describe '#restart_checkout_flow' do
+ it 'updates the state column to the first checkout_steps value' do
+ order = create(:order_with_totals, state: 'delivery')
+ expect(order.checkout_steps).to eql ['address', 'delivery', 'complete']
+ expect { order.restart_checkout_flow }.to change { order.state }.from('delivery').to('address')
+ end
+
+ context 'without line items' do
+ it 'updates the state column to cart' do
+ order = create(:order, state: 'delivery')
+ expect { order.restart_checkout_flow }.to change { order.state }.from('delivery').to('cart')
+ end
+ end
+ end
+
+ # Regression tests for #4072
+ context '#state_changed' do
+ let(:order) { FactoryBot.create(:order) }
+
+ it 'logs state changes' do
+ order.update_column(:payment_state, 'balance_due')
+ order.payment_state = 'paid'
+ expect(order.state_changes).to be_empty
+ order.state_changed('payment')
+ state_change = order.state_changes.find_by(name: 'payment')
+ expect(state_change.previous_state).to eq('balance_due')
+ expect(state_change.next_state).to eq('paid')
+ end
+
+ it 'does not do anything if state does not change' do
+ order.update_column(:payment_state, 'balance_due')
+ expect(order.state_changes).to be_empty
+ order.state_changed('payment')
+ expect(order.state_changes).to be_empty
+ end
+ end
+
+ # Regression test for #4199
+ context '#available_payment_methods' do
+ let(:ok_method) { double :payment_method, available_for_order?: true }
+ let(:no_method) { double :payment_method, available_for_order?: false }
+ let(:methods) { [ok_method, no_method] }
+
+ it 'includes frontend payment methods' do
+ payment_method = Spree::PaymentMethod.create!(name: 'Fake',
+ active: true,
+ display_on: 'front_end')
+ expect(order.available_payment_methods).to include(payment_method)
+ end
+
+ it "includes 'both' payment methods" do
+ payment_method = Spree::PaymentMethod.create!(name: 'Fake',
+ active: true,
+ display_on: 'both')
+ expect(order.available_payment_methods).to include(payment_method)
+ end
+
+ it 'does not include a payment method twice if display_on is blank' do
+ payment_method = Spree::PaymentMethod.create!(name: 'Fake',
+ active: true,
+ display_on: 'both')
+ expect(order.available_payment_methods.count).to eq(1)
+ expect(order.available_payment_methods).to include(payment_method)
+ end
+
+ it 'does not include a payment method that is not suitable for this order' do
+ allow(Spree::PaymentMethod).to receive(:available_on_front_end).and_return(methods)
+ expect(order.available_payment_methods).to match_array [ok_method]
+ end
+ end
+
+ context '#apply_free_shipping_promotions' do
+ it 'calls out to the FreeShipping promotion handler' do
+ shipment = double('Shipment')
+ allow(order).to receive_messages shipments: [shipment]
+ expect(Spree::PromotionHandler::FreeShipping).to receive(:new).and_return(handler = double)
+ expect(handler).to receive(:activate)
+
+ expect(Spree::Adjustable::AdjustmentsUpdater).to receive(:update).with(shipment)
+
+ expect(order.updater).to receive(:update)
+ order.apply_free_shipping_promotions
+ end
+ end
+
+ context '#products' do
+ before do
+ @variant1 = mock_model(Spree::Variant, product: 'product1')
+ @variant2 = mock_model(Spree::Variant, product: 'product2')
+ @line_items = [mock_model(Spree::LineItem, product: 'product1', variant: @variant1, variant_id: @variant1.id, quantity: 1),
+ mock_model(Spree::LineItem, product: 'product2', variant: @variant2, variant_id: @variant2.id, quantity: 2)]
+ allow(order).to receive_messages(line_items: @line_items)
+ end
+
+ it 'gets the quantity of a given variant' do
+ expect(order.quantity_of(@variant1)).to eq(1)
+
+ @variant3 = mock_model(Spree::Variant, product: 'product3')
+ expect(order.quantity_of(@variant3)).to eq(0)
+ end
+
+ it 'can find a line item matching a given variant' do
+ expect(order.find_line_item_by_variant(@variant1)).not_to be_nil
+ expect(order.find_line_item_by_variant(mock_model(Spree::Variant))).to be_nil
+ end
+
+ context 'match line item with options' do
+ before do
+ Rails.application.config.spree.line_item_comparison_hooks << :foos_match
+ end
+
+ after do
+ # reset to avoid test pollution
+ Rails.application.config.spree.line_item_comparison_hooks = Set.new
+ end
+
+ it 'matches line item when options match' do
+ allow(order).to receive(:foos_match).and_return(true)
+ expect(order.line_item_options_match(@line_items.first, foos: { bar: :zoo })).to be true
+ end
+
+ it 'does not match line item without options' do
+ allow(order).to receive(:foos_match).and_return(false)
+ expect(order.line_item_options_match(@line_items.first, {})).to be false
+ end
+ end
+ end
+
+ describe '#associate_user!' do
+ let(:user) { FactoryBot.create(:user_with_addreses) }
+ let(:email) { user.email }
+ let(:created_by) { user }
+ let(:bill_address) { user.bill_address }
+ let(:ship_address) { user.ship_address }
+ let(:override_email) { true }
+
+ let(:order) { FactoryBot.build(:order, order_attributes) }
+
+ let(:order_attributes) do
+ {
+ user: nil,
+ email: nil,
+ created_by: nil,
+ bill_address: nil,
+ ship_address: nil
+ }
+ end
+
+ def assert_expected_order_state
+ expect(order.user).to eql(user)
+ expect(order.user_id).to eql(user.id)
+
+ expect(order.email).to eql(email)
+
+ expect(order.created_by).to eql(created_by)
+ expect(order.created_by_id).to eql(created_by.id)
+
+ expect(order.bill_address == bill_address).to be(true) if order.bill_address
+
+ expect(order.ship_address == ship_address).to be(true) if order.ship_address
+ end
+
+ shared_examples_for '#associate_user!' do |persisted = false|
+ it 'associates a user to an order' do
+ order.associate_user!(user, override_email)
+ assert_expected_order_state
+ end
+
+ unless persisted
+ it 'does not persist the order' do
+ expect { order.associate_user!(user) }.
+ not_to change(order, :persisted?).
+ from(false)
+ end
+ end
+ end
+
+ context 'when email is set' do
+ let(:order_attributes) { super().merge(email: 'test@example.com') }
+
+ context 'when email should be overridden' do
+ it_behaves_like '#associate_user!'
+ end
+
+ context 'when email should not be overridden' do
+ let(:override_email) { false }
+ let(:email) { 'test@example.com' }
+
+ it_behaves_like '#associate_user!'
+ end
+ end
+
+ context 'when created_by is set' do
+ let(:order_attributes) { super().merge(created_by: created_by) }
+ let(:created_by) { create(:user_with_addreses) }
+
+ it_behaves_like '#associate_user!'
+ end
+
+ context 'when bill_address is set' do
+ let(:order_attributes) { super().merge(bill_address: bill_address) }
+ let(:bill_address) { FactoryBot.build(:address) }
+
+ it_behaves_like '#associate_user!'
+ end
+
+ context 'when ship_address is set' do
+ let(:order_attributes) { super().merge(ship_address: ship_address) }
+ let(:ship_address) { FactoryBot.build(:address) }
+
+ it_behaves_like '#associate_user!'
+ end
+
+ context 'when the user is not persisted' do
+ let(:user) { FactoryBot.build(:user_with_addreses) }
+
+ it 'does not persist the user' do
+ expect { order.associate_user!(user) }.
+ not_to change(user, :persisted?).
+ from(false)
+ end
+
+ it_behaves_like '#associate_user!'
+ end
+
+ context 'when the order is persisted' do
+ let(:order) { FactoryBot.create(:order, order_attributes) }
+
+ it 'associates a user to a persisted order' do
+ order.associate_user!(user)
+ order.reload
+ assert_expected_order_state
+ end
+
+ it 'does not persist other changes to the order' do
+ order.state = 'complete'
+ order.associate_user!(user)
+ order.reload
+ expect(order.state).to eql('cart')
+ end
+
+ it 'does not change any other orders' do
+ other = FactoryBot.create(:order)
+ order.associate_user!(user)
+ expect(other.reload.user).not_to eql(user)
+ end
+
+ it 'is not affected by scoping' do
+ order.class.where.not(id: order).scoping do
+ order.associate_user!(user)
+ end
+ order.reload
+ assert_expected_order_state
+ end
+
+ it_behaves_like '#associate_user!', true
+ end
+ end
+
+ context '#can_ship?' do
+ let(:order) { Spree::Order.create }
+
+ it "is true for order in the 'complete' state" do
+ allow(order).to receive_messages(complete?: true)
+ expect(order.can_ship?).to be true
+ end
+
+ it "is true for order in the 'resumed' state" do
+ allow(order).to receive_messages(resumed?: true)
+ expect(order.can_ship?).to be true
+ end
+
+ it "is true for an order in the 'awaiting return' state" do
+ allow(order).to receive_messages(awaiting_return?: true)
+ expect(order.can_ship?).to be true
+ end
+
+ it "is true for an order in the 'returned' state" do
+ allow(order).to receive_messages(returned?: true)
+ expect(order.can_ship?).to be true
+ end
+
+ it "is false if the order is neither in the 'complete' nor 'resumed' state" do
+ allow(order).to receive_messages(resumed?: false, complete?: false)
+ expect(order.can_ship?).to be false
+ end
+ end
+
+ context '#completed?' do
+ it 'indicates if order is completed' do
+ order.completed_at = nil
+ expect(order.completed?).to be false
+
+ order.completed_at = Time.current
+ expect(order.completed?).to be true
+ end
+ end
+
+ context '#allow_checkout?' do
+ it 'is true if there are line_items in the order' do
+ allow(order).to receive_message_chain(:line_items, :exists?).and_return(true)
+ expect(order.checkout_allowed?).to be true
+ end
+ it 'is false if there are no line_items in the order' do
+ allow(order).to receive_message_chain(:line_items, :exists?).and_return(false)
+ expect(order.checkout_allowed?).to be false
+ end
+ end
+
+ context '#amount' do
+ before do
+ @order = create(:order, user: user)
+ @order.line_items = [create(:line_item, price: 1.0, quantity: 2),
+ create(:line_item, price: 1.0, quantity: 1)]
+ end
+
+ it 'returns the correct lum sum of items' do
+ expect(@order.amount).to eq(3.0)
+ end
+ end
+
+ context '#backordered?' do
+ it 'is backordered if one of the shipments is backordered' do
+ allow(order).to receive_messages(shipments: [mock_model(Spree::Shipment, backordered?: false),
+ mock_model(Spree::Shipment, backordered?: true)])
+ expect(order).to be_backordered
+ end
+ end
+
+ context '#can_cancel?' do
+ it 'is false for completed order in the canceled state' do
+ order.state = 'canceled'
+ order.shipment_state = 'ready'
+ order.completed_at = Time.current
+ expect(order.can_cancel?).to be false
+ end
+
+ it 'is true for completed order with no shipment' do
+ order.state = 'complete'
+ order.shipment_state = nil
+ order.completed_at = Time.current
+ expect(order.can_cancel?).to be true
+ end
+ end
+
+ context '#tax_total' do
+ it 'adds included tax and additional tax' do
+ allow(order).to receive_messages(additional_tax_total: 10, included_tax_total: 20)
+
+ expect(order.tax_total).to eq 30
+ end
+ end
+
+ # Regression test for #4923
+ context 'locking' do
+ let(:order) { Spree::Order.create } # need a persisted in order to test locking
+
+ it 'can lock' do
+ expect { order.with_lock {} }.not_to raise_error
+ end
+ end
+
+ describe '#pre_tax_item_amount' do
+ it "sums all of the line items' pre tax amounts" do
+ subject.line_items = [
+ Spree::LineItem.new(price: 10, quantity: 2, pre_tax_amount: 5.0),
+ Spree::LineItem.new(price: 30, quantity: 1, pre_tax_amount: 14.0)
+ ]
+
+ expect(subject.pre_tax_item_amount).to eq 19.0
+ end
+ end
+
+ describe '#quantity' do
+ # Uses a persisted record, as the quantity is retrieved via a DB count
+ let(:order) { create :order_with_line_items, line_items_count: 3 }
+
+ it 'sums the quantity of all line items' do
+ expect(order.quantity).to eq 3
+ end
+ end
+
+ describe '#has_non_reimbursement_related_refunds?' do
+ subject do
+ order.has_non_reimbursement_related_refunds?
+ end
+
+ context 'no refunds exist' do
+ it { is_expected.to eq false }
+ end
+
+ context 'a non-reimbursement related refund exists' do
+ let(:order) { refund.payment.order }
+ let(:refund) { create(:refund, reimbursement_id: nil, amount: 5) }
+
+ it { is_expected.to eq true }
+ end
+
+ context 'an old-style refund exists' do
+ let(:order) { create(:order_ready_to_ship) }
+ let(:payment) { order.payments.first.tap { |p| allow(p).to receive_messages(profiles_supported: false) } }
+ let!(:refund_payment) do
+ build(:payment, amount: -1, order: order, state: 'completed', source: payment).tap do |p|
+ allow(p).to receive_messages(profiles_supported?: false)
+ p.save!
+ end
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'a reimbursement related refund exists' do
+ let(:order) { refund.payment.order }
+ let(:refund) { create(:refund, reimbursement_id: 123, amount: 5) }
+
+ it { is_expected.to eq false }
+ end
+ end
+
+ describe '#create_proposed_shipments' do
+ context 'has unassociated inventory units' do
+ let!(:inventory_unit) { create(:inventory_unit, order: subject) }
+
+ before do
+ inventory_unit.update_column(:shipment_id, nil)
+ end
+
+ context 'when shipped' do
+ before do
+ inventory_unit.update_column(:state, 'shipped')
+ end
+
+ it 'does not delete inventory_unit' do
+ subject.create_proposed_shipments
+ expect(inventory_unit.reload).to eq inventory_unit
+ end
+ end
+
+ context 'when returned' do
+ before do
+ inventory_unit.update_column(:state, 'returned')
+ end
+
+ it 'does not delete inventory_unit' do
+ subject.create_proposed_shipments
+ expect(inventory_unit.reload).to eq inventory_unit
+ end
+ end
+
+ context 'when on_hand' do
+ before do
+ inventory_unit.update_column(:state, 'on_hand')
+ end
+
+ it 'deletes inventory_unit' do
+ subject.create_proposed_shipments
+ expect { inventory_unit.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'when backordered' do
+ before do
+ inventory_unit.update_column(:state, 'backordered')
+ end
+
+ it 'deletes inventory_unit' do
+ subject.create_proposed_shipments
+ expect { inventory_unit.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+
+ it 'assigns the coordinator returned shipments to its shipments' do
+ shipment = build(:shipment)
+ allow_any_instance_of(Spree::Stock::Coordinator).to receive(:shipments).and_return([shipment])
+ subject.create_proposed_shipments
+ expect(subject.shipments).to eq [shipment]
+ end
+ end
+
+ describe '#all_inventory_units_returned?' do
+ subject { order.all_inventory_units_returned? }
+
+ let(:order) { create(:order_with_line_items, line_items_count: 3) }
+
+ context 'all inventory units are returned' do
+ before { order.inventory_units.update_all(state: 'returned') }
+
+ it 'is true' do
+ expect(subject).to eq true
+ end
+ end
+
+ context 'some inventory units are returned' do
+ before do
+ order.inventory_units.first.update_attribute(:state, 'returned')
+ end
+
+ it 'is false' do
+ expect(subject).to eq false
+ end
+ end
+
+ context 'no inventory units are returned' do
+ it 'is false' do
+ expect(subject).to eq false
+ end
+ end
+ end
+
+ describe '#fully_discounted?' do
+ let(:line_item) { Spree::LineItem.new(price: 10, quantity: 1) }
+ let(:shipment) { Spree::Shipment.new(cost: 10) }
+ let(:payment) { Spree::Payment.new(amount: 10) }
+
+ before do
+ allow(order).to receive(:line_items) { [line_item] }
+ allow(order).to receive(:shipments) { [shipment] }
+ allow(order).to receive(:payments) { [payment] }
+ end
+
+ context 'the order had no inventory-related cost' do
+ before do
+ # discount the cost of the line items
+ allow(order).to receive(:adjustment_total).and_return(-5)
+ allow(line_item).to receive(:adjustment_total).and_return(-5)
+
+ # but leave some shipment payment amount
+ allow(shipment).to receive(:adjustment_total).and_return(0)
+ end
+
+ it { expect(order.fully_discounted?).to eq true }
+ end
+
+ context 'the order had inventory-related cost' do
+ before do
+ # partially discount the cost of the line item
+ allow(order).to receive(:adjustment_total).and_return(0)
+ allow(line_item).to receive(:adjustment_total).and_return(-5)
+
+ # and partially discount the cost of the shipment so the total
+ # discount matches the item total for test completeness
+ allow(shipment).to receive(:adjustment_total).and_return(-5)
+ end
+
+ it { expect(order.fully_discounted?).to eq false }
+ end
+ end
+
+ describe '#promo_code' do
+ context 'without promo_code applied' do
+ it { expect(order.promo_code).to eq nil }
+ end
+
+ context 'with_promo_code applied' do
+ let(:promo_code) { '10off' }
+ let(:promotion) { create :promotion, code: promo_code }
+
+ before do
+ promotion.orders << order
+ end
+
+ it 'returns applied promo_code' do
+ expect(order.promo_code).to eq promo_code
+ end
+ end
+ end
+
+ describe '#validate_payments_attributes' do
+ let(:payment_method) { create(:credit_card_payment_method) }
+ let(:attributes) do
+ [{ amount: 50, payment_method_id: payment_method.id }]
+ end
+
+ context 'with existing payment method' do
+ it "doesn't raise error and returns collection" do
+ expect(order.validate_payments_attributes(attributes)).to eq attributes
+ end
+ end
+
+ context 'not existing payment method' do
+ let(:payment_method) { create(:credit_card_payment_method, display_on: 'backend') }
+
+ it 'raises RecordNotFound' do
+ expect { order.validate_payments_attributes(attributes) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+
+ describe 'order transit to returned state from resumed state' do
+ let!(:resumed_order) { create(:order_with_line_items, line_items_count: 3, state: :resumed) }
+
+ context 'when all inventory_units returned' do
+ before do
+ resumed_order.inventory_units.update_all(state: 'returned')
+ resumed_order.return
+ end
+
+ it { expect(resumed_order).to be_returned }
+ end
+
+ context 'when some inventory_units returned' do
+ before do
+ resumed_order.inventory_units.first.update_attribute(:state, 'returned')
+ resumed_order.return
+ end
+
+ it { expect(resumed_order).to be_resumed }
+ end
+ end
+
+ describe 'credit_card_nil_payment' do
+ let!(:order) { create(:order_with_line_items, line_items_count: 2) }
+ let!(:credit_card_payment_method) { create(:simple_credit_card_payment_method) }
+ let!(:store_credits) { create(:store_credit_payment, order: order) }
+
+ def attributes(amount = 0)
+ { payments_attributes: [{ amount: amount, payment_method_id: credit_card_payment_method.id }] }
+ end
+ context 'when zero amount credit-card payment' do
+ it 'expect not to build a new payment' do
+ expect { order.assign_attributes(attributes) }.to change { order.payments.size }.by(0)
+ end
+ end
+
+ context 'when valid-amount(>0) creditcard payment' do
+ it 'expect not to build a new payment' do
+ expect { order.assign_attributes(attributes(10)) }.to change { order.payments.size }.by(1)
+ end
+ end
+ end
+
+ describe '#collect_backend_payment_methods' do
+ let!(:order) { create(:order_with_line_items, line_items_count: 2) }
+ let!(:credit_card_payment_method) { create(:simple_credit_card_payment_method, display_on: 'both') }
+ let!(:store_credit_payment_method) { create(:store_credit_payment_method, display_on: 'both') }
+
+ it { expect(order.collect_backend_payment_methods).to include(credit_card_payment_method) }
+ it { expect(order.collect_backend_payment_methods).not_to include(store_credit_payment_method) }
+ end
+
+ describe '#create_shipment_tax_charge!' do
+ let(:order_shipments) { double }
+
+ after { order.create_shipment_tax_charge! }
+
+ context 'when order has shipments' do
+ before do
+ allow(order).to receive(:shipments).and_return(order_shipments)
+ allow(order_shipments).to receive(:any?).and_return(true)
+ allow(Spree::TaxRate).to receive(:adjust).with(order, order_shipments)
+ end
+
+ it { expect(order_shipments).to receive(:any?).and_return(true) }
+ it { expect(order).to receive(:shipments).and_return(order_shipments) }
+ it { expect(Spree::TaxRate).to receive(:adjust).with(order, order_shipments) }
+ end
+
+ context 'when order has no shipments' do
+ before do
+ allow(order).to receive_message_chain(:shipments, :any?).and_return(false)
+ end
+
+ it { expect(order).to receive_message_chain(:shipments, :any?).and_return(false) }
+ end
+ end
+
+ describe '#shipping_eq_billing_address' do
+ let!(:order) { create(:order) }
+
+ context 'with only bill address' do
+ it { expect(order.shipping_eq_billing_address?).to eq(false) }
+ end
+
+ context 'blank addresses' do
+ before do
+ order.bill_address = Spree::Address.new
+ order.ship_address = Spree::Address.new
+ end
+
+ it { expect(order.shipping_eq_billing_address?).to eq(true) }
+ end
+
+ context 'no addresses' do
+ before do
+ order.bill_address = nil
+ order.ship_address = nil
+ end
+
+ it { expect(order.shipping_eq_billing_address?).to eq(true) }
+ end
+ end
+end
diff --git a/core/spec/models/spree/order_updater_spec.rb b/core/spec/models/spree/order_updater_spec.rb
new file mode 100644
index 00000000000..939ca253e47
--- /dev/null
+++ b/core/spec/models/spree/order_updater_spec.rb
@@ -0,0 +1,319 @@
+require 'spec_helper'
+require 'spree/testing_support/order_walkthrough'
+
+module Spree
+ describe OrderUpdater, type: :model do
+ let(:order) { Spree::Order.create }
+ let(:updater) { Spree::OrderUpdater.new(order) }
+
+ context 'order totals' do
+ before do
+ create_list(:line_item, 2, order: order, price: 10)
+ end
+
+ it 'updates payment totals' do
+ create(:payment_with_refund, order: order)
+ Spree::OrderUpdater.new(order).update_payment_total
+ expect(order.payment_total).to eq(40.75)
+ end
+
+ it 'update item total' do
+ updater.update_item_total
+ expect(order.item_total).to eq(20)
+ end
+
+ it 'update shipment total' do
+ create(:shipment, order: order, cost: 10)
+ updater.update_shipment_total
+ expect(order.shipment_total).to eq(10)
+ end
+
+ context 'with order promotion followed by line item addition' do
+ let(:promotion) { Spree::Promotion.create!(name: '10% off') }
+ let(:calculator) { Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) }
+
+ let(:promotion_action) do
+ Promotion::Actions::CreateAdjustment.create!(calculator: calculator,
+ promotion: promotion)
+ end
+
+ before do
+ updater.update
+ create(:adjustment, source: promotion_action, adjustable: order, order: order)
+ create(:line_item, order: order, price: 10) # in addition to the two already created
+ updater.update
+ end
+
+ it 'updates promotion total' do
+ expect(order.promo_total).to eq(-3)
+ end
+ end
+
+ it 'update order adjustments' do
+ # A line item will not have both additional and included tax,
+ # so please just humour me for now.
+ order.line_items.first.update_columns(adjustment_total: 10.05,
+ additional_tax_total: 0.05,
+ included_tax_total: 0.05)
+ updater.update_adjustment_total
+ expect(order.adjustment_total).to eq(10.05)
+ expect(order.additional_tax_total).to eq(0.05)
+ expect(order.included_tax_total).to eq(0.05)
+ end
+ end
+
+ describe '#update_with_updater!' do
+ it 'updates item count' do
+ create(:line_item, order: order)
+ create(:line_item, order: order)
+
+ order.update_with_updater!
+
+ expect(order.item_count).to eq(2)
+ end
+ end
+
+ context 'updating shipment state' do
+ before do
+ allow(order).to receive_messages backordered?: false
+ allow(order).to receive_message_chain(:shipments, :shipped, :count).and_return(0)
+ allow(order).to receive_message_chain(:shipments, :ready, :count).and_return(0)
+ allow(order).to receive_message_chain(:shipments, :pending, :count).and_return(0)
+ end
+
+ it 'is backordered' do
+ allow(order).to receive_messages backordered?: true
+ updater.update_shipment_state
+
+ expect(order.shipment_state).to eq('backorder')
+ end
+
+ it 'is nil' do
+ allow(order).to receive_message_chain(:shipments, :states).and_return([])
+ allow(order).to receive_message_chain(:shipments, :count).and_return(0)
+
+ updater.update_shipment_state
+ expect(order.shipment_state).to be_nil
+ end
+
+ ['shipped', 'ready', 'pending'].each do |state|
+ it "is #{state}" do
+ allow(order).to receive_message_chain(:shipments, :states).and_return([state])
+ updater.update_shipment_state
+ expect(order.shipment_state).to eq(state.to_s)
+ end
+ end
+
+ it 'is partial' do
+ allow(order).to receive_message_chain(:shipments, :states).and_return(['pending', 'ready'])
+ updater.update_shipment_state
+ expect(order.shipment_state).to eq('partial')
+ end
+ end
+
+ context 'updating payment state' do
+ let(:order) { Order.new }
+ let(:updater) { order.updater }
+
+ it 'is failed if no valid payments' do
+ allow(order).to receive_message_chain(:payments, :valid, :empty?).and_return(true)
+
+ updater.update_payment_state
+ expect(order.payment_state).to eq('failed')
+ end
+
+ context 'payment total is greater than order total' do
+ it 'is credit_owed' do
+ order.payment_total = 2
+ order.total = 1
+
+ expect do
+ updater.update_payment_state
+ end.to change(order, :payment_state).to 'credit_owed'
+ end
+ end
+
+ context 'order total is greater than payment total' do
+ it 'is balance_due' do
+ order.payment_total = 1
+ order.total = 2
+
+ expect do
+ updater.update_payment_state
+ end.to change(order, :payment_state).to 'balance_due'
+ end
+ end
+
+ context 'order total equals payment total' do
+ it 'is paid' do
+ order.payment_total = 30
+ order.total = 30
+
+ expect do
+ updater.update_payment_state
+ end.to change(order, :payment_state).to 'paid'
+ end
+ end
+
+ context 'order is canceled' do
+ before do
+ order.state = 'canceled'
+ end
+
+ context 'and is still unpaid' do
+ it 'is void' do
+ order.payment_total = 0
+ order.total = 30
+ expect do
+ updater.update_payment_state
+ end.to change(order, :payment_state).to 'void'
+ end
+ end
+
+ context 'and is paid' do
+ it 'is credit_owed' do
+ order.payment_total = 30
+ order.total = 30
+ allow(order).to receive_message_chain(:payments, :valid, :empty?).and_return(false)
+ allow(order).to receive_message_chain(:payments, :completed, :size).and_return(1)
+ expect do
+ updater.update_payment_state
+ end.to change(order, :payment_state).to 'credit_owed'
+ end
+ end
+
+ context 'and payment is refunded' do
+ it 'is void' do
+ order.payment_total = 0
+ order.total = 30
+ expect do
+ updater.update_payment_state
+ end.to change(order, :payment_state).to 'void'
+ end
+ end
+ end
+ end
+
+ it 'state change' do
+ order.shipment_state = 'shipped'
+ state_changes = double
+ allow(order).to receive_messages state_changes: state_changes
+ expect(state_changes).to receive(:create).with(
+ previous_state: nil,
+ next_state: 'shipped',
+ name: 'shipment',
+ user_id: nil
+ )
+
+ order.state_changed('shipment')
+ end
+
+ shared_context 'with original shipping method gone backend only' do
+ before do
+ order.shipments.first.shipping_method.update(display_on: :back_end)
+ create(:shipping_method) # create frontend available shipping method
+ end
+ end
+
+ context 'completed order' do
+ before { order.update(completed_at: Time.current) }
+
+ describe '#update' do
+ it 'updates payment state' do
+ expect(updater).to receive(:update_payment_state)
+ updater.update
+ end
+
+ it 'updates shipment state' do
+ expect(updater).to receive(:update_shipment_state)
+ updater.update
+ end
+
+ it 'updates shipments total again after updating shipments' do
+ expect(updater).to receive(:update_shipment_total).ordered
+ expect(updater).to receive(:update_shipments).ordered
+ expect(updater).to receive(:update_shipment_total).ordered
+ updater.update
+ end
+ end
+
+ describe '#update_shipments' do
+ it 'updates each shipment' do
+ shipment = stub_model(Spree::Shipment, order: order)
+ shipments = [shipment]
+ allow(order).to receive_messages shipments: shipments
+ allow(shipments).to receive_messages states: []
+ allow(shipments).to receive_messages ready: []
+ allow(shipments).to receive_messages pending: []
+ allow(shipments).to receive_messages shipped: []
+
+ expect(shipment).to receive(:update!).with(order)
+ updater.update_shipments
+ end
+
+ it 'refreshes shipment rates' do
+ shipment = stub_model(Spree::Shipment, order: order)
+ shipments = [shipment]
+ allow(order).to receive_messages shipments: shipments
+
+ expect(shipment).to receive(:refresh_rates)
+ updater.update_shipments
+ end
+
+ it 'updates the shipment amount' do
+ shipment = stub_model(Spree::Shipment, order: order)
+ shipments = [shipment]
+ allow(order).to receive_messages shipments: shipments
+
+ expect(shipment).to receive(:update_amounts)
+ updater.update_shipments
+ end
+
+ context 'refresh rates' do
+ include_context 'with original shipping method gone backend only'
+ let(:order) { create(:completed_order_with_totals) }
+
+ it 'keeps the original shipping method' do
+ expect { updater.update_shipments }.not_to change { order.shipments.first.shipping_method }
+ end
+ end
+ end
+ end
+
+ context 'incomplete order' do
+ it 'doesnt update payment state' do
+ expect(updater).not_to receive(:update_payment_state)
+ updater.update
+ end
+
+ it 'doesnt update shipment state' do
+ expect(updater).not_to receive(:update_shipment_state)
+ updater.update
+ end
+
+ it 'doesnt update each shipment' do
+ shipment = stub_model(Spree::Shipment)
+ shipments = [shipment]
+ allow(order).to receive_messages shipments: shipments
+ allow(shipments).to receive_messages states: []
+ allow(shipments).to receive_messages ready: []
+ allow(shipments).to receive_messages pending: []
+ allow(shipments).to receive_messages shipped: []
+
+ allow(updater).to receive(:update_totals) # Otherwise this gets called and causes a scene
+ expect(updater).not_to receive(:update_shipments).with(order)
+ updater.update
+ end
+
+ describe '#update_shipments' do
+ include_context 'with original shipping method gone backend only'
+ let(:order) { ::OrderWalkthrough.up_to(:delivery) }
+
+ it 'resets shipping method to frontend-available' do
+ order.updater.update_shipments
+ expect(order.shipments.first.shipping_method).to eq Spree::ShippingMethod.find_by(display_on: 'both')
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/payment/gateway_options_spec.rb b/core/spec/models/spree/payment/gateway_options_spec.rb
new file mode 100644
index 00000000000..881c0241411
--- /dev/null
+++ b/core/spec/models/spree/payment/gateway_options_spec.rb
@@ -0,0 +1,141 @@
+require 'spec_helper'
+
+RSpec.describe Spree::Payment::GatewayOptions, type: :model do
+ let(:options) { Spree::Payment::GatewayOptions.new(payment) }
+
+ let(:payment) do
+ double(
+ Spree::Payment,
+ order: order,
+ number: 'P1566',
+ currency: 'EUR',
+ payment_method: payment_method
+ )
+ end
+
+ let(:payment_method) do
+ double(
+ Spree::Gateway::Bogus,
+ exchange_multiplier: Spree::Gateway::FROM_DOLLAR_TO_CENT_RATE
+ )
+ end
+
+ let(:order) do
+ double(
+ Spree::Order,
+ email: 'test@email.com',
+ user_id: 144,
+ last_ip_address: '0.0.0.0',
+ number: 'R1444',
+ ship_total: '12.44'.to_d,
+ additional_tax_total: '1.53'.to_d,
+ item_total: '15.11'.to_d,
+ promo_total: '2.57'.to_d,
+ bill_address: bill_address,
+ ship_address: ship_address
+ )
+ end
+
+ let(:bill_address) do
+ double Spree::Address, active_merchant_hash: { bill: :address }
+ end
+
+ let(:ship_address) do
+ double Spree::Address, active_merchant_hash: { ship: :address }
+ end
+
+ describe '#email' do
+ subject { options.email }
+
+ it { is_expected.to eq 'test@email.com' }
+ end
+
+ describe '#customer' do
+ subject { options.customer }
+
+ it { is_expected.to eq 'test@email.com' }
+ end
+
+ describe '#customer_id' do
+ subject { options.customer_id }
+
+ it { is_expected.to eq 144 }
+ end
+
+ describe '#ip' do
+ subject { options.ip }
+
+ it { is_expected.to eq '0.0.0.0' }
+ end
+
+ describe '#order_id' do
+ subject { options.order_id }
+
+ it { is_expected.to eq 'R1444-P1566' }
+ end
+
+ describe '#shipping' do
+ subject { options.shipping }
+
+ it { is_expected.to eq 1244 }
+ end
+
+ describe '#tax' do
+ subject { options.tax }
+
+ it { is_expected.to eq 153 }
+ end
+
+ describe '#subtotal' do
+ subject { options.subtotal }
+
+ it { is_expected.to eq 1511 }
+ end
+
+ describe '#discount' do
+ subject { options.discount }
+
+ it { is_expected.to eq 257 }
+ end
+
+ describe '#currency' do
+ subject { options.currency }
+
+ it { is_expected.to eq 'EUR' }
+ end
+
+ describe '#billing_address' do
+ subject { options.billing_address }
+
+ it { is_expected.to eq(bill: :address) }
+ end
+
+ describe '#shipping_address' do
+ subject { options.shipping_address }
+
+ it { is_expected.to eq(ship: :address) }
+ end
+
+ describe '#to_hash' do
+ subject { options.to_hash }
+
+ let(:expected) do
+ {
+ email: 'test@email.com',
+ customer: 'test@email.com',
+ customer_id: 144,
+ ip: '0.0.0.0',
+ order_id: 'R1444-P1566',
+ shipping: '1244'.to_d,
+ tax: '153'.to_d,
+ subtotal: '1511'.to_d,
+ discount: '257'.to_d,
+ currency: 'EUR',
+ billing_address: { bill: :address },
+ shipping_address: { ship: :address }
+ }
+ end
+
+ it { is_expected.to eq expected }
+ end
+end
diff --git a/core/spec/models/spree/payment/store_credit_spec.rb b/core/spec/models/spree/payment/store_credit_spec.rb
new file mode 100644
index 00000000000..352f58a3428
--- /dev/null
+++ b/core/spec/models/spree/payment/store_credit_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe 'Payment' do
+ context '#cancel!' do
+ subject { payment.cancel! }
+
+ context 'a store credit' do
+ let(:store_credit) { create(:store_credit, amount_used: captured_amount) }
+ let(:auth_code) { '1-SC-20141111111111' }
+ let(:captured_amount) { 10.0 }
+ let(:payment) { create(:store_credit_payment, response_code: auth_code) }
+
+ let!(:capture_event) do
+ create(:store_credit_auth_event,
+ action: Spree::StoreCredit::CAPTURE_ACTION,
+ authorization_code: auth_code,
+ amount: captured_amount,
+ store_credit: store_credit)
+ end
+
+ let(:successful_response) do
+ ActiveMerchant::Billing::Response.new(
+ true,
+ Spree.t('store_credit_payment_method.successful_action', action: :cancel),
+ {},
+ authorization: payment.response_code
+ )
+ end
+
+ let(:failed_response) do
+ ActiveMerchant::Billing::Response.new(
+ false,
+ Spree.t('store_credit_payment_method.unable_to_find_for_action', action: :cancel,
+ auth_code: payment.response_code),
+ {},
+ {}
+ )
+ end
+
+ it 'attemps to cancels the payment' do
+ expect(payment.payment_method).to receive(:cancel).with(payment.response_code) { successful_response }
+ subject
+ end
+
+ context 'cancels successfully' do
+ it 'voids the payment' do
+ expect { subject }.to change(payment, :state).to('void')
+ end
+ end
+
+ context 'does not cancel successfully' do
+ it 'does not change the payment state' do
+ expect(payment.payment_method).to receive(:cancel).with(payment.response_code) { failed_response }
+ expect { subject }.to raise_error(Spree::Core::GatewayError)
+ expect(payment.reload.state).not_to eq 'void'
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/payment_method/store_credit_spec.rb b/core/spec/models/spree/payment_method/store_credit_spec.rb
new file mode 100644
index 00000000000..77ad57c641c
--- /dev/null
+++ b/core/spec/models/spree/payment_method/store_credit_spec.rb
@@ -0,0 +1,310 @@
+require 'spec_helper'
+
+describe Spree::PaymentMethod::StoreCredit do
+ let(:order) { create(:order) }
+ let(:payment) { create(:payment, order: order) }
+ let(:gateway_options) { payment.gateway_options }
+
+ context '#authorize' do
+ subject do
+ described_class.new.authorize(auth_amount, store_credit, gateway_options)
+ end
+
+ let(:auth_amount) { store_credit.amount_remaining * 100 }
+ let(:store_credit) { create(:store_credit) }
+ let(:gateway_options) { super().merge(originator: originator) }
+ let(:originator) { nil }
+
+ context 'without an invalid store credit' do
+ let(:store_credit) { nil }
+ let(:auth_amount) { 10 }
+
+ it 'declines an unknown store credit' do
+ expect(subject.success?).to be false
+ expect(subject.message).to include Spree.t('store_credit_payment_method.unable_to_find')
+ end
+ end
+
+ context 'with insuffient funds' do
+ let(:auth_amount) { (store_credit.amount_remaining * 100) + 1 }
+
+ it 'declines a store credit' do
+ expect(subject.success?).to be false
+ expect(subject.message).to include Spree.t('store_credit_payment_method.insufficient_funds')
+ end
+ end
+
+ context 'when the currency does not match the order currency' do
+ let(:store_credit) { create(:store_credit, currency: 'AUD') }
+
+ it 'declines the credit' do
+ expect(subject.success?).to be false
+ expect(subject.message).to include Spree.t('store_credit_payment_method.currency_mismatch')
+ end
+ end
+
+ context 'with a valid request' do
+ it 'authorizes a valid store credit' do
+ expect(subject.success?).to be true
+ expect(subject.authorization).not_to be_nil
+ end
+
+ context 'with an originator' do
+ let(:originator) { double('originator') }
+
+ it 'passes the originator' do
+ expect_any_instance_of(Spree::StoreCredit).to receive(:authorize).
+ with(anything, anything, action_originator: originator)
+ subject
+ end
+ end
+ end
+ end
+
+ context '#capture' do
+ subject do
+ described_class.new.capture(capture_amount, auth_code, gateway_options)
+ end
+
+ let(:capture_amount) { 10_00 }
+ let(:auth_code) { auth_event.authorization_code }
+ let(:gateway_options) { super().merge(originator: originator) }
+
+ let(:authorized_amount) { capture_amount / 100.0 }
+ let(:auth_event) { create(:store_credit_auth_event, store_credit: store_credit, amount: authorized_amount) }
+ let(:store_credit) { create(:store_credit, amount_authorized: authorized_amount) }
+ let(:originator) { nil }
+
+ context 'with an invalid auth code' do
+ let(:auth_code) { -1 }
+
+ it 'declines an unknown store credit' do
+ expect(subject.success?).to be false
+ expect(subject.message).to include Spree.t('store_credit_payment_method.unable_to_find')
+ end
+ end
+
+ context 'when unable to authorize the amount' do
+ let(:authorized_amount) { (capture_amount - 1) / 100 }
+
+ before do
+ allow_any_instance_of(Spree::StoreCredit).to receive_messages(authorize: true)
+ end
+
+ it 'declines a store credit' do
+ expect(subject.success?).to be false
+ expect(subject.message).to include Spree.t('store_credit_payment_method.insufficient_authorized_amount')
+ end
+ end
+
+ context 'when the currency does not match the order currency' do
+ let(:store_credit) { create(:store_credit, currency: 'AUD', amount_authorized: authorized_amount) }
+
+ it 'declines the credit' do
+ expect(subject.success?).to be false
+ expect(subject.message).to include Spree.t('store_credit_payment_method.currency_mismatch')
+ end
+ end
+
+ context 'with a valid request' do
+ it 'captures the store credit' do
+ expect(subject.message).to include Spree.t('store_credit_payment_method.successful_action',
+ action: Spree::StoreCredit::CAPTURE_ACTION)
+ expect(subject.success?).to be true
+ end
+
+ context 'with an originator' do
+ let(:originator) { double('originator') }
+
+ it 'passes the originator' do
+ expect_any_instance_of(Spree::StoreCredit).to receive(:capture).
+ with(anything, anything, anything, action_originator: originator)
+ subject
+ end
+ end
+ end
+ end
+
+ context '#void' do
+ subject do
+ described_class.new.void(auth_code, gateway_options)
+ end
+
+ let(:auth_code) { auth_event.authorization_code }
+ let(:gateway_options) { super().merge(originator: originator) }
+ let(:auth_event) { create(:store_credit_auth_event) }
+ let(:originator) { nil }
+
+ context 'with an invalid auth code' do
+ let(:auth_code) { 1 }
+
+ it 'declines an unknown store credit' do
+ expect(subject.success?).to be false
+ expect(subject.message).to include Spree.t('store_credit_payment_method.unable_to_find')
+ end
+ end
+
+ context 'when the store credit is not voided successfully' do
+ before { allow_any_instance_of(Spree::StoreCredit).to receive(:void).and_return false }
+
+ it 'returns an error response' do
+ expect(subject.success?).to be false
+ end
+ end
+
+ it 'voids a valid store credit void request' do
+ expect(subject.success?).to be true
+ expect(subject.message).to include Spree.t('store_credit_payment_method.successful_action',
+ action: Spree::StoreCredit::VOID_ACTION)
+ end
+
+ context 'with an originator' do
+ let(:originator) { double('originator') }
+
+ it 'passes the originator' do
+ expect_any_instance_of(Spree::StoreCredit).to receive(:void).with(anything, action_originator: originator)
+ subject
+ end
+ end
+ end
+
+ context '#purchase' do
+ it "declines a purchase if it can't find a pending credit for the correct amount" do
+ amount = 100.0
+ store_credit = create(:store_credit)
+ auth_code = store_credit.generate_authorization_code
+ store_credit.store_credit_events.create!(action: Spree::StoreCredit::ELIGIBLE_ACTION,
+ amount: amount,
+ authorization_code: auth_code)
+ store_credit.store_credit_events.create!(action: Spree::StoreCredit::CAPTURE_ACTION,
+ amount: amount, authorization_code: auth_code)
+
+ response = subject.purchase(amount * 100.0, store_credit, gateway_options)
+ expect(response.success?).to be false
+ expect(response.message).to include Spree.t('store_credit_payment_method.unable_to_find')
+ end
+
+ it 'captures a purchase if it can find a pending credit for the correct amount' do
+ amount = 100.0
+ store_credit = create(:store_credit)
+ auth_code = store_credit.generate_authorization_code
+ store_credit.store_credit_events.create!(action: Spree::StoreCredit::ELIGIBLE_ACTION,
+ amount: amount, authorization_code: auth_code)
+
+ response = subject.purchase(amount * 100.0, store_credit, gateway_options)
+ expect(response.success?).to be true
+ expect(response.message).to include Spree.t('store_credit_payment_method.successful_action',
+ action: Spree::StoreCredit::CAPTURE_ACTION)
+ end
+ end
+
+ context '#credit' do
+ subject do
+ described_class.new.credit(credit_amount, auth_code, gateway_options)
+ end
+
+ let(:credit_amount) { 100.0 }
+ let(:auth_code) { auth_event.authorization_code }
+ let(:gateway_options) { super().merge(originator: originator) }
+ let(:auth_event) { create(:store_credit_auth_event) }
+ let(:originator) { nil }
+
+ context 'with an invalid auth code' do
+ let(:auth_code) { 1 }
+
+ it 'declines an unknown store credit' do
+ expect(subject.success?).to be false
+ expect(subject.message).to include Spree.t('store_credit_payment_method.unable_to_find')
+ end
+ end
+
+ context "when the store credit isn't credited successfully" do
+ before { allow(Spree::StoreCredit).to receive_messages(credit: false) }
+
+ it 'returns an error response' do
+ expect(subject.success?).to be false
+ end
+ end
+
+ context 'with a valid credit request' do
+ before { allow_any_instance_of(Spree::StoreCredit).to receive_messages(credit: true) }
+
+ it 'credits a valid store credit credit request' do
+ expect(subject.success?).to be true
+ expect(subject.message).to include Spree.t('store_credit_payment_method.successful_action',
+ action: Spree::StoreCredit::CREDIT_ACTION)
+ end
+ end
+
+ context 'with an originator' do
+ let(:originator) { double('originator') }
+
+ it 'passes the originator' do
+ allow_any_instance_of(Spree::StoreCredit).to receive(:credit).
+ with(anything, anything, anything, action_originator: originator)
+ subject
+ end
+ end
+ end
+
+ context '#cancel' do
+ subject do
+ described_class.new.cancel(auth_code)
+ end
+
+ let(:store_credit) { create(:store_credit, amount_used: captured_amount) }
+ let(:auth_code) { '1-SC-20141111111111' }
+ let(:captured_amount) { 10.0 }
+
+ let!(:capture_event) do
+ create(:store_credit_auth_event,
+ action: Spree::StoreCredit::CAPTURE_ACTION,
+ authorization_code: auth_code,
+ amount: captured_amount,
+ store_credit: store_credit)
+ end
+
+ context 'store credit event found' do
+ it 'creates a store credit for the same amount that was captured' do
+ allow_any_instance_of(Spree::StoreCredit).to receive(:credit).
+ with(captured_amount, auth_code, store_credit.currency)
+ subject
+ end
+
+ it 'returns a valid store credit cancel request' do
+ expect(subject.success?).to be true
+ expect(subject.message).to include Spree.t('store_credit_payment_method.successful_action',
+ action: Spree::StoreCredit::CANCEL_ACTION)
+ end
+ end
+
+ context 'store credit event not found' do
+ subject do
+ described_class.new.cancel('INVALID')
+ end
+
+ it 'returns an error response' do
+ expect(subject.success?).to be false
+ end
+ end
+ end
+
+ describe '#available_for_order?' do
+ let!(:store_credit_payment_method) { create(:store_credit_payment_method, display_on: 'both') }
+
+ context 'when user have store credits' do
+ let!(:user_with_store_credits) { create(:user) }
+ let!(:store_credit) { create(:store_credit, user: user_with_store_credits) }
+ let!(:order_with_store_credit) { create(:order, user: user_with_store_credits) }
+
+ it { expect(store_credit_payment_method.available_for_order?(order_with_store_credit)).to be true }
+ end
+
+ context "when user don't store credits" do
+ let!(:user_without_store_credits) { create(:user) }
+ let!(:order_without_store_credit) { create(:order, user: user_without_store_credits) }
+
+ it { expect(store_credit_payment_method.available_for_order?(order_without_store_credit)).to be false }
+ end
+ end
+end
diff --git a/core/spec/models/spree/payment_method_spec.rb b/core/spec/models/spree/payment_method_spec.rb
new file mode 100644
index 00000000000..eee3ae40b7f
--- /dev/null
+++ b/core/spec/models/spree/payment_method_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe Spree::PaymentMethod, type: :model do
+ context 'visibility scopes' do
+ before do
+ [nil, '', 'both', 'front_end', 'back_end'].each do |display_on|
+ Spree::Gateway::Test.create(
+ name: 'Display Both',
+ display_on: display_on,
+ active: true,
+ description: 'foofah'
+ )
+ end
+ end
+
+ it 'has 5 total methods' do
+ expect(Spree::PaymentMethod.count).to eq(5)
+ end
+
+ describe '#available' do
+ it 'returns all methods available to front-end/back-end' do
+ methods = Spree::PaymentMethod.available
+ expect(methods.size).to eq(3)
+ expect(methods.pluck(:display_on)).to eq(['both', 'front_end', 'back_end'])
+ end
+ end
+
+ describe '#available_on_front_end' do
+ it 'returns all methods available to front-end' do
+ methods = Spree::PaymentMethod.available_on_front_end
+ expect(methods.size).to eq(2)
+ expect(methods.pluck(:display_on)).to eq(['both', 'front_end'])
+ end
+ end
+
+ describe '#available_on_back_end' do
+ it 'returns all methods available to back-end' do
+ methods = Spree::PaymentMethod.available_on_back_end
+ expect(methods.size).to eq(2)
+ expect(methods.pluck(:display_on)).to eq(['both', 'back_end'])
+ end
+ end
+ end
+
+ describe '#auto_capture?' do
+ class TestGateway < Spree::Gateway
+ def provider_class
+ Provider
+ end
+ end
+
+ subject { gateway.auto_capture? }
+
+ let(:gateway) { TestGateway.new }
+
+ context 'when auto_capture is nil' do
+ before do
+ expect(Spree::Config).to receive('[]').with(:auto_capture).and_return(auto_capture)
+ end
+
+ context 'and when Spree::Config[:auto_capture] is false' do
+ let(:auto_capture) { false }
+
+ it 'is false' do
+ expect(gateway.auto_capture).to be_nil
+ expect(subject).to be false
+ end
+ end
+
+ context 'and when Spree::Config[:auto_capture] is true' do
+ let(:auto_capture) { true }
+
+ it 'is true' do
+ expect(gateway.auto_capture).to be_nil
+ expect(subject).to be true
+ end
+ end
+ end
+
+ context 'when auto_capture is not nil' do
+ before do
+ gateway.auto_capture = auto_capture
+ end
+
+ context 'and is true' do
+ let(:auto_capture) { true }
+
+ it 'is true' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'and is false' do
+ let(:auto_capture) { false }
+
+ it 'is true' do
+ expect(subject).to be false
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/payment_spec.rb b/core/spec/models/spree/payment_spec.rb
new file mode 100644
index 00000000000..65a85db4597
--- /dev/null
+++ b/core/spec/models/spree/payment_spec.rb
@@ -0,0 +1,937 @@
+require 'spec_helper'
+
+describe Spree::Payment, type: :model do
+ let(:order) { Spree::Order.create }
+ let(:refund_reason) { create(:refund_reason) }
+
+ let(:gateway) do
+ gateway = Spree::Gateway::Bogus.new(active: true)
+ allow(gateway).to receive_messages source_required: true
+ gateway
+ end
+
+ let(:avs_code) { 'D' }
+ let(:cvv_code) { 'M' }
+
+ let(:card) { create :credit_card }
+
+ let(:payment) do
+ payment = Spree::Payment.new
+ payment.source = card
+ payment.order = order
+ payment.payment_method = gateway
+ payment.amount = 5
+ payment
+ end
+
+ let(:amount_in_cents) { (payment.amount * 100).round }
+
+ let!(:success_response) do
+ ActiveMerchant::Billing::Response.new(true, '', {}, authorization: '123',
+ cvv_result: cvv_code,
+ avs_result: { code: avs_code })
+ end
+
+ let(:failed_response) do
+ ActiveMerchant::Billing::Response.new(false, '', {}, {})
+ end
+
+ before do
+ # Rails >= 5.0.3 returns new object for association so we ugly mock it
+ allow(payment).to receive(:log_entries).and_return(payment.log_entries)
+
+ # So it doesn't create log entries every time a processing method is called
+ allow(payment.log_entries).to receive(:create!)
+ end
+
+ describe 'Constants' do
+ it { expect(Spree::Payment::INVALID_STATES).to eq(%w(failed invalid)) }
+ end
+
+ describe 'scopes' do
+ describe '.valid' do
+ subject { Spree::Payment.valid }
+
+ let!(:invalid_payment) do
+ create(:payment, avs_response: 'Y', cvv_response_code: 'M', cvv_response_message: '', state: 'invalid')
+ end
+
+ let!(:failed_payment) do
+ create(:payment, avs_response: 'Y', cvv_response_code: 'M', cvv_response_message: '', state: 'failed')
+ end
+
+ let!(:checkout_payment) do
+ create(:payment, avs_response: 'A', cvv_response_code: 'M', cvv_response_message: '', state: 'checkout')
+ end
+
+ let!(:completed_payment) do
+ create(:payment, avs_response: 'Y', cvv_response_code: 'N', cvv_response_message: '', state: 'completed')
+ end
+
+ it { is_expected.not_to include(invalid_payment) }
+ it { is_expected.not_to include(failed_payment) }
+ it { is_expected.to include(checkout_payment) }
+ it { is_expected.to include(completed_payment) }
+ end
+ end
+
+ context '.risky' do
+ let!(:payment_1) { create(:payment, avs_response: 'Y', cvv_response_code: 'M', cvv_response_message: 'Match') }
+ let!(:payment_2) { create(:payment, avs_response: 'Y', cvv_response_code: 'M', cvv_response_message: '') }
+ let!(:payment_3) { create(:payment, avs_response: 'A', cvv_response_code: 'M', cvv_response_message: 'Match') }
+ let!(:payment_4) { create(:payment, avs_response: 'Y', cvv_response_code: 'N', cvv_response_message: 'No Match') }
+
+ it 'does not return successful responses' do
+ expect(subject.class.risky.to_a).to match_array([payment_3, payment_4])
+ end
+ end
+
+ context '#captured_amount' do
+ context 'calculates based on capture events' do
+ it 'with 0 capture events' do
+ expect(payment.captured_amount).to eq(0)
+ end
+
+ it 'with some capture events' do
+ payment.save
+ payment.capture_events.create!(amount: 2.0)
+ payment.capture_events.create!(amount: 3.0)
+ expect(payment.captured_amount).to eq(5)
+ end
+ end
+ end
+
+ context '#uncaptured_amount' do
+ context 'calculates based on capture events' do
+ it 'with 0 capture events' do
+ expect(payment.uncaptured_amount).to eq(5.0)
+ end
+
+ it 'with some capture events' do
+ payment.save
+ payment.capture_events.create!(amount: 2.0)
+ payment.capture_events.create!(amount: 3.0)
+ expect(payment.uncaptured_amount).to eq(0)
+ end
+ end
+ end
+
+ context 'validations' do
+ context 'when payment source is not required' do
+ it 'do not validate source presence' do
+ allow_any_instance_of(Spree::PaymentMethod).to receive(:source_required?).and_return(false)
+
+ payment.source = nil
+ expect(payment).to be_valid
+ end
+ end
+
+ context 'with payment source required' do
+ it 'validate source presence' do
+ payment.source = nil
+ expect(payment).not_to be_valid
+ end
+ end
+
+ it 'returns useful error messages when source is invalid' do
+ payment.source = Spree::CreditCard.new
+ expect(payment).not_to be_valid
+ cc_errors = payment.errors['Credit Card']
+ expect(cc_errors).to include("Number can't be blank")
+ expect(cc_errors).to include('Month is not a number')
+ expect(cc_errors).to include('Year is not a number')
+ expect(cc_errors).to include("Verification Value can't be blank")
+ end
+ end
+
+ # Regression test for https://github.com/spree/spree/pull/2224
+ context 'failure' do
+ it 'transitions to failed from pending state' do
+ payment.state = 'pending'
+ payment.failure
+ expect(payment.state).to eql('failed')
+ end
+
+ it 'transitions to failed from processing state' do
+ payment.state = 'processing'
+ payment.failure
+ expect(payment.state).to eql('failed')
+ end
+ end
+
+ context 'invalidate' do
+ it 'transitions from checkout to invalid' do
+ payment.state = 'checkout'
+ payment.invalidate
+ expect(payment.state).to eq('invalid')
+ end
+ end
+
+ context 'processing' do
+ describe '#process!' do
+ it 'purchases if with auto_capture' do
+ expect(payment.payment_method).to receive(:auto_capture?).and_return(true)
+ payment.process!
+ expect(payment).to be_completed
+ end
+
+ it 'authorizes without auto_capture' do
+ expect(payment.payment_method).to receive(:auto_capture?).and_return(false)
+ payment.process!
+ expect(payment).to be_pending
+ end
+
+ it "makes the state 'processing'" do
+ expect(payment).to receive(:started_processing!)
+ payment.process!
+ end
+
+ it 'invalidates if payment method doesnt support source' do
+ expect(payment.payment_method).to receive(:supports?).with(payment.source).and_return(false)
+ expect { payment.process! }.to raise_error(Spree::Core::GatewayError)
+ expect(payment.state).to eq('invalid')
+ end
+
+ # Regression test for #4598
+ it 'allows payments with a gateway_customer_profile_id' do
+ allow(payment.source).to receive_messages gateway_customer_profile_id: 'customer_1'
+ expect(payment.payment_method).to receive(:supports?).with(payment.source).and_return(false)
+ expect(payment).to receive(:started_processing!)
+ payment.process!
+ end
+
+ # Another regression test for #4598
+ it 'allows payments with a gateway_payment_profile_id' do
+ allow(payment.source).to receive_messages gateway_payment_profile_id: 'customer_1'
+ expect(payment.payment_method).to receive(:supports?).with(payment.source).and_return(false)
+ expect(payment).to receive(:started_processing!)
+ payment.process!
+ end
+ end
+
+ describe '#authorize!' do
+ it 'calls authorize on the gateway with the payment amount' do
+ expect(payment.payment_method).to receive(:authorize).with(amount_in_cents,
+ card,
+ anything).and_return(success_response)
+ payment.authorize!
+ end
+
+ it 'calls authorize on the gateway with the currency code' do
+ allow(payment).to receive_messages currency: 'GBP'
+ expect(payment.payment_method).to receive(:authorize).with(amount_in_cents,
+ card,
+ hash_including(currency: 'GBP')).and_return(success_response)
+ payment.authorize!
+ end
+
+ it 'logs the response' do
+ payment.save!
+ expect(payment.log_entries).to receive(:create!).with(details: anything)
+ payment.authorize!
+ end
+
+ context 'if successful' do
+ before do
+ expect(payment.payment_method).to receive(:authorize).with(amount_in_cents,
+ card,
+ anything).and_return(success_response)
+ end
+
+ it 'stores the response_code, avs_response and cvv_response fields' do
+ payment.authorize!
+ expect(payment.response_code).to eq('123')
+ expect(payment.avs_response).to eq(avs_code)
+ expect(payment.cvv_response_code).to eq(cvv_code)
+ expect(payment.cvv_response_message).to eq(ActiveMerchant::Billing::CVVResult::MESSAGES[cvv_code])
+ end
+
+ it 'makes payment pending' do
+ expect(payment).to receive(:pend!)
+ payment.authorize!
+ end
+ end
+
+ context 'if unsuccessful' do
+ it 'marks payment as failed' do
+ allow(gateway).to receive(:authorize).and_return(failed_response)
+ expect(payment).to receive(:failure)
+ expect(payment).not_to receive(:pend)
+ expect do
+ payment.authorize!
+ end.to raise_error(Spree::Core::GatewayError)
+ end
+ end
+ end
+
+ describe '#purchase!' do
+ it 'calls purchase on the gateway with the payment amount' do
+ expect(gateway).to receive(:purchase).with(amount_in_cents, card, anything).and_return(success_response)
+ payment.purchase!
+ end
+
+ it 'logs the response' do
+ payment.save!
+ expect(payment.log_entries).to receive(:create!).with(details: anything)
+ payment.purchase!
+ end
+
+ context 'if successful' do
+ before do
+ expect(payment.payment_method).to receive(:purchase).with(amount_in_cents,
+ card,
+ anything).and_return(success_response)
+ end
+
+ it 'stores the response_code and avs_response' do
+ payment.purchase!
+ expect(payment.response_code).to eq('123')
+ expect(payment.avs_response).to eq(avs_code)
+ end
+
+ it 'makes payment complete' do
+ expect(payment).to receive(:complete!)
+ payment.purchase!
+ end
+
+ it 'logs a capture event' do
+ payment.purchase!
+ expect(payment.capture_events.count).to eq(1)
+ expect(payment.capture_events.first.amount).to eq(payment.amount)
+ end
+
+ it 'sets the uncaptured amount to 0' do
+ payment.purchase!
+ expect(payment.uncaptured_amount).to eq(0)
+ end
+ end
+
+ context 'if unsuccessful' do
+ before do
+ allow(gateway).to receive(:purchase).and_return(failed_response)
+ expect(payment).to receive(:failure)
+ expect(payment).not_to receive(:pend)
+ end
+
+ it 'makes payment failed' do
+ expect { payment.purchase! }.to raise_error(Spree::Core::GatewayError)
+ end
+
+ it 'does not log a capture event' do
+ expect { payment.purchase! }.to raise_error(Spree::Core::GatewayError)
+ expect(payment.capture_events.count).to eq(0)
+ end
+ end
+ end
+
+ describe '#capture!' do
+ context 'when payment is pending' do
+ before do
+ payment.amount = 100
+ payment.state = 'pending'
+ payment.response_code = '12345'
+ end
+
+ context 'if successful' do
+ context 'for entire amount' do
+ before do
+ expect(payment.payment_method).to receive(:capture).with(payment.display_amount.amount_in_cents, payment.response_code, anything).and_return(success_response)
+ end
+
+ it 'makes payment complete' do
+ expect(payment).to receive(:complete!)
+ payment.capture!
+ end
+
+ it 'logs capture events' do
+ payment.capture!
+ expect(payment.capture_events.count).to eq(1)
+ expect(payment.capture_events.first.amount).to eq(payment.amount)
+ end
+ end
+
+ context 'for partial amount' do
+ let(:original_amount) { payment.money.amount_in_cents }
+ let(:capture_amount) { original_amount - 100 }
+
+ before do
+ expect(payment.payment_method).to receive(:capture).with(capture_amount, payment.response_code, anything).and_return(success_response)
+ end
+
+ it 'makes payment complete & create pending payment for remaining amount' do
+ expect(payment).to receive(:complete!)
+ payment.capture!(capture_amount)
+ order = payment.order
+ payments = order.payments
+
+ expect(payments.size).to eq 2
+ expect(payments.pending.first.amount).to eq 1
+ # Payment stays processing for spec because of receive(:complete!) stub.
+ expect(payments.processing.first.amount).to eq(capture_amount / 100)
+ expect(payments.processing.first.source).to eq(payments.pending.first.source)
+ end
+
+ it 'logs capture events' do
+ payment.capture!(capture_amount)
+ expect(payment.capture_events.count).to eq(1)
+ expect(payment.capture_events.first.amount).to eq(capture_amount / 100)
+ end
+ end
+ end
+
+ context 'if unsuccessful' do
+ it 'does not make payment complete' do
+ allow(gateway).to receive_messages capture: failed_response
+ expect(payment).to receive(:failure)
+ expect(payment).not_to receive(:complete)
+ expect { payment.capture! }.to raise_error(Spree::Core::GatewayError)
+ end
+ end
+ end
+
+ # Regression test for #2119
+ context 'when payment is completed' do
+ before do
+ payment.state = 'completed'
+ end
+
+ it 'does nothing' do
+ expect(payment).not_to receive(:complete)
+ expect(payment.payment_method).not_to receive(:capture)
+ expect(payment.log_entries).not_to receive(:create!)
+ payment.capture!
+ end
+ end
+ end
+
+ describe '#void_transaction!' do
+ before do
+ payment.response_code = '123'
+ payment.state = 'pending'
+ end
+
+ context 'when profiles are supported' do
+ it "calls payment_gateway.void with the payment's response_code" do
+ allow(gateway).to receive_messages payment_profiles_supported?: true
+ expect(gateway).to receive(:void).with('123', card, anything).and_return(success_response)
+ payment.void_transaction!
+ end
+ end
+
+ context 'when profiles are not supported' do
+ it "calls payment_gateway.void with the payment's response_code" do
+ allow(gateway).to receive_messages payment_profiles_supported?: false
+ expect(gateway).to receive(:void).with('123', anything).and_return(success_response)
+ payment.void_transaction!
+ end
+ end
+
+ it 'logs the response' do
+ expect(payment.log_entries).to receive(:create!).with(details: anything)
+ payment.void_transaction!
+ end
+
+ context 'if successful' do
+ it 'updates the response_code with the authorization from the gateway' do
+ # Change it to something different
+ payment.response_code = 'abc'
+ payment.void_transaction!
+ expect(payment.response_code).to eq('12345')
+ end
+ end
+
+ context 'if unsuccessful' do
+ it 'does not void the payment' do
+ allow(gateway).to receive_messages void: failed_response
+ expect(payment).not_to receive(:void)
+ expect { payment.void_transaction! }.to raise_error(Spree::Core::GatewayError)
+ end
+ end
+
+ # Regression test for #2119
+ context 'if payment is already voided' do
+ before do
+ payment.state = 'void'
+ end
+
+ it 'does not void the payment' do
+ expect(payment.payment_method).not_to receive(:void)
+ payment.void_transaction!
+ end
+ end
+ end
+ end
+
+ context 'when already processing' do
+ it 'returns nil without trying to process the source' do
+ payment.state = 'processing'
+
+ expect(payment.process!).to be_nil
+ end
+ end
+
+ context 'with source required' do
+ context 'raises an error if no source is specified' do
+ before do
+ payment.source = nil
+ end
+
+ specify do
+ expect { payment.process! }.to raise_error(Spree::Core::GatewayError, Spree.t(:payment_processing_failed))
+ end
+ end
+ end
+
+ context 'with source optional' do
+ context 'raises no error if source is not specified' do
+ before do
+ payment.source = nil
+ allow(payment.payment_method).to receive_messages(source_required?: false)
+ end
+
+ specify do
+ expect { payment.process! }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#credit_allowed' do
+ # Regression test for #4403 & #4407
+ it 'is the difference between offsets total and payment amount' do
+ payment.amount = 100
+ allow(payment).to receive(:offsets_total).and_return(0)
+ expect(payment.credit_allowed).to eq(100)
+ allow(payment).to receive(:offsets_total).and_return(-80)
+ expect(payment.credit_allowed).to eq(20)
+ end
+ end
+
+ describe '#can_credit?' do
+ it 'is true if credit_allowed > 0' do
+ allow(payment).to receive(:credit_allowed).and_return(100)
+ expect(payment.can_credit?).to be true
+ end
+
+ it 'is false if credit_allowed is 0' do
+ allow(payment).to receive(:credit_allowed).and_return(0)
+ expect(payment.can_credit?).to be false
+ end
+ end
+
+ describe '#save' do
+ context 'captured payments' do
+ it 'update order payment total' do
+ payment = create(:payment, order: order, state: 'completed')
+ expect(order.payment_total).to eq payment.amount
+ end
+ end
+
+ context 'not completed payments' do
+ it "doesn't update order payment total" do
+ expect do
+ Spree::Payment.create(amount: 100, order: order)
+ end.not_to change(order, :payment_total)
+ end
+
+ it 'requires a payment method' do
+ expect(Spree::Payment.create(amount: 100, order: order)).to have(1).error_on(:payment_method)
+ end
+ end
+
+ context 'when the payment was completed but now void' do
+ let(:payment) do
+ Spree::Payment.create(
+ amount: 100,
+ order: order,
+ state: 'completed'
+ )
+ end
+
+ it 'updates order payment total' do
+ payment.void
+ expect(order.payment_total).to eq 0
+ end
+ end
+
+ context 'completed orders' do
+ before { allow(order).to receive_messages completed?: true }
+
+ it 'updates payment_state and shipments' do
+ expect(order.updater).to receive(:update_payment_state)
+ expect(order.updater).to receive(:update_shipment_state)
+ Spree::Payment.create(amount: 100, order: order, payment_method: gateway, source: card)
+ end
+ end
+
+ context 'when profiles are supported' do
+ before do
+ allow(gateway).to receive_messages payment_profiles_supported?: true
+ allow(payment.source).to receive_messages has_payment_profile?: false
+ end
+
+ context 'when there is an error connecting to the gateway' do
+ it 'calls gateway_error' do
+ message = double('gateway_error')
+ connection_error = ActiveMerchant::ConnectionError.new(message, nil)
+ expect(gateway).to receive(:create_profile).and_raise(connection_error)
+ expect do
+ Spree::Payment.create(
+ amount: 100,
+ order: order,
+ source: card,
+ payment_method: gateway
+ )
+ end.to raise_error(Spree::Core::GatewayError)
+ end
+ end
+
+ context 'with multiple payment attempts' do
+ let(:attributes) { attributes_for(:credit_card) }
+
+ it 'does not try to create profiles on old failed payment attempts' do
+ allow_any_instance_of(Spree::Payment).to receive(:payment_method) { gateway }
+
+ order.payments.create!(
+ source_attributes: attributes,
+ payment_method: gateway,
+ amount: 100
+ )
+ expect(gateway).to receive(:create_profile).exactly :once
+ expect(order.payments.count).to eq(1)
+ order.payments.create!(
+ source_attributes: attributes,
+ payment_method: gateway,
+ amount: 100
+ )
+ end
+ end
+
+ context 'when successfully connecting to the gateway' do
+ it 'creates a payment profile' do
+ expect(payment.payment_method).to receive :create_profile
+ Spree::Payment.create(
+ amount: 100,
+ order: order,
+ source: card,
+ payment_method: gateway
+ )
+ end
+ end
+ end
+
+ context 'when profiles are not supported' do
+ before { allow(gateway).to receive_messages payment_profiles_supported?: false }
+
+ it 'does not create a payment profile' do
+ expect(gateway).not_to receive :create_profile
+ Spree::Payment.create(
+ amount: 100,
+ order: order,
+ source: card,
+ payment_method: gateway
+ )
+ end
+ end
+ end
+
+ describe '#build_source' do
+ let(:params) do
+ {
+ amount: 100,
+ payment_method: gateway,
+ source_attributes: {
+ expiry: '01 / 99',
+ number: '1234567890123',
+ verification_value: '123',
+ name: 'Spree Commerce'
+ }
+ }
+ end
+
+ it "builds the payment's source" do
+ payment = Spree::Payment.new(params)
+ expect(payment).to be_valid
+ expect(payment.source).not_to be_nil
+ end
+
+ it 'assigns user and gateway to payment source' do
+ order = create(:order)
+ source = order.payments.new(params).source
+
+ expect(source.user_id).to eq order.user_id
+ expect(source.payment_method_id).to eq gateway.id
+ end
+
+ it 'errors when payment source not valid' do
+ params = { amount: 100, payment_method: gateway,
+ source_attributes: { expiry: '1 / 12' } }
+
+ payment = Spree::Payment.new(params)
+ expect(payment).not_to be_valid
+ expect(payment.source).not_to be_nil
+ expect(payment.source.error_on(:number).size).to eq(1)
+ expect(payment.source.error_on(:verification_value).size).to eq(1)
+ end
+
+ it 'does not build a new source when duplicating the model with source_attributes set' do
+ payment = create(:payment)
+ payment.source_attributes = params[:source_attributes]
+ expect { payment.dup }.not_to change { payment.source }
+ end
+ end
+
+ describe '#currency' do
+ before { allow(order).to receive(:currency).and_return('ABC') }
+
+ it 'returns the order currency' do
+ expect(payment.currency).to eq('ABC')
+ end
+ end
+
+ describe '#display_amount' do
+ it 'returns a Spree::Money for this amount' do
+ expect(payment.display_amount).to eq(Spree::Money.new(payment.amount))
+ end
+ end
+
+ # Regression test for #2216
+ describe '#gateway_options' do
+ before { allow(order).to receive_messages(last_ip_address: '192.168.1.1') }
+
+ it 'contains an IP' do
+ expect(payment.gateway_options[:ip]).to eq(order.last_ip_address)
+ end
+
+ it 'contains the email address from a persisted order' do
+ # Sets the payment's order to a different Ruby object entirely
+ payment.order = Spree::Order.find(payment.order_id)
+ email = 'foo@example.com'
+ order.update_attributes(email: email)
+ expect(payment.gateway_options[:email]).to eq(email)
+ end
+ end
+
+ describe '#amount=' do
+ before do
+ subject.amount = amount
+ end
+
+ context 'when the amount is a string' do
+ context 'amount is a decimal' do
+ let(:amount) { '2.99' }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('2.99'))
+ end
+ end
+
+ context 'amount is an integer' do
+ let(:amount) { '2' }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('2.0'))
+ end
+ end
+
+ context 'amount contains a dollar sign' do
+ let(:amount) { '$2.99' }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('2.99'))
+ end
+ end
+
+ context 'amount contains a comma' do
+ let(:amount) { '$2,999.99' }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('2999.99'))
+ end
+ end
+
+ context 'amount contains a negative sign' do
+ let(:amount) { '-2.99' }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('-2.99'))
+ end
+ end
+
+ context 'amount is invalid' do
+ let(:amount) { 'invalid' }
+
+ # this is a strange default for ActiveRecord
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('0'))
+ end
+ end
+
+ context 'amount is an empty string' do
+ let(:amount) { '' }
+
+ it '#amount' do
+ expect(subject.amount).to be_nil
+ end
+ end
+ end
+
+ context 'when the amount is a number' do
+ let(:amount) { 1.55 }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('1.55'))
+ end
+ end
+
+ context 'when the locale uses a coma as a decimal separator' do
+ before do
+ I18n.backend.store_translations(:fr, number: { currency: { format: { delimiter: ' ', separator: ',' } } })
+ I18n.locale = :fr
+ subject.amount = amount
+ end
+
+ after do
+ I18n.locale = I18n.default_locale
+ end
+
+ context 'amount is a decimal' do
+ let(:amount) { '2,99' }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('2.99'))
+ end
+ end
+
+ context 'amount contains a $ sign' do
+ let(:amount) { '2,99 $' }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('2.99'))
+ end
+ end
+
+ context 'amount is a number' do
+ let(:amount) { 2.99 }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('2.99'))
+ end
+ end
+
+ context 'amount contains a negative sign' do
+ let(:amount) { '-2,99 $' }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('-2.99'))
+ end
+ end
+
+ context 'amount uses a dot as a decimal separator' do
+ let(:amount) { '2.99' }
+
+ it '#amount' do
+ expect(subject.amount).to eql(BigDecimal('2.99'))
+ end
+ end
+ end
+ end
+
+ describe 'is_avs_risky?' do
+ it 'returns false if avs_response included in NON_RISKY_AVS_CODES' do
+ ('A'..'Z').reject { |x| subject.class::RISKY_AVS_CODES.include?(x) }.to_a.each do |char|
+ payment.update_attribute(:avs_response, char)
+ expect(payment.is_avs_risky?).to eq false
+ end
+ end
+
+ it 'returns false if avs_response.blank?' do
+ payment.update_attribute(:avs_response, nil)
+ expect(payment.is_avs_risky?).to eq false
+ payment.update_attribute(:avs_response, '')
+ expect(payment.is_avs_risky?).to eq false
+ end
+
+ it 'returns true if avs_response in RISKY_AVS_CODES' do
+ # should use avs_response_code helper
+ ('A'..'Z').reject { |x| subject.class::NON_RISKY_AVS_CODES.include?(x) }.to_a.each do |char|
+ payment.update_attribute(:avs_response, char)
+ expect(payment.is_avs_risky?).to eq true
+ end
+ end
+ end
+
+ describe 'is_cvv_risky?' do
+ it "returns false if cvv_response_code == 'M'" do
+ payment.update_attribute(:cvv_response_code, 'M')
+ expect(payment.is_cvv_risky?).to eq(false)
+ end
+
+ it 'returns false if cvv_response_code == nil' do
+ payment.update_attribute(:cvv_response_code, nil)
+ expect(payment.is_cvv_risky?).to eq(false)
+ end
+
+ it "returns false if cvv_response_message == ''" do
+ payment.update_attribute(:cvv_response_message, '')
+ expect(payment.is_cvv_risky?).to eq(false)
+ end
+
+ it 'returns true if cvv_response_code == [A-Z], omitting D' do
+ # should use cvv_response_code helper
+ (%w{N P S U} << '').each do |char|
+ payment.update_attribute(:cvv_response_code, char)
+ expect(payment.is_cvv_risky?).to eq(true)
+ end
+ end
+ end
+
+ describe '#editable?' do
+ subject { payment }
+
+ before do
+ subject.state = state
+ end
+
+ context "when the state is 'checkout'" do
+ let(:state) { 'checkout' }
+
+ it 'returns true' do
+ expect(payment.editable?).to eq(true)
+ end
+ end
+
+ context "when the state is 'pending'" do
+ let(:state) { 'pending' }
+
+ it 'returns true' do
+ expect(payment.editable?).to eq(true)
+ end
+ end
+
+ %w[processing completed failed void invalid].each do |state|
+ context "when the state is '#{state}'" do
+ let(:state) { state }
+
+ it 'returns false' do
+ expect(payment.editable?).to eq(false)
+ end
+ end
+ end
+ end
+
+ # Regression test for #4072 (kinda)
+ # The need for this was discovered in the research for #4072
+ context 'state changes' do
+ it 'are logged to the database' do
+ expect(payment.state_changes).to be_empty
+ expect(payment.process!).to be true
+ expect(payment.state_changes.count).to eq(2)
+ changes = payment.state_changes.map { |change| { change.previous_state => change.next_state } }
+ expect(changes).to match_array([
+ { 'checkout' => 'processing' },
+ { 'processing' => 'pending' }
+ ])
+ end
+ end
+end
diff --git a/core/spec/models/spree/preference_spec.rb b/core/spec/models/spree/preference_spec.rb
new file mode 100644
index 00000000000..1ca935e879d
--- /dev/null
+++ b/core/spec/models/spree/preference_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Spree::Preference, type: :model do
+ it 'requires a key' do
+ @preference = Spree::Preference.new
+ @preference.key = :test
+ @preference.value = true
+ expect(@preference).to be_valid
+ end
+
+ describe 'type coversion for values' do
+ def round_trip_preference(key, value)
+ p = Spree::Preference.new
+ p.value = value
+ p.key = key
+ p.save
+
+ Spree::Preference.find_by(key: key)
+ end
+
+ it ':boolean' do
+ value = true
+ key = 'boolean_key'
+ pref = round_trip_preference(key, value)
+ expect(pref.value).to eq value
+ end
+
+ it 'false :boolean' do
+ value = false
+ key = 'boolean_key'
+ pref = round_trip_preference(key, value)
+ expect(pref.value).to eq value
+ end
+
+ it ':integer' do
+ value = 10
+ key = 'integer_key'
+ pref = round_trip_preference(key, value)
+ expect(pref.value).to eq value
+ end
+
+ it ':decimal' do
+ value = 1.5
+ key = 'decimal_key'
+ pref = round_trip_preference(key, value)
+ expect(pref.value).to eq value
+ end
+
+ it ':string' do
+ value = 'This is a string'
+ key = 'string_key'
+ pref = round_trip_preference(key, value)
+ expect(pref.value).to eq value
+ end
+
+ it ':text' do
+ value = 'This is a string stored as text'
+ key = 'text_key'
+ pref = round_trip_preference(key, value)
+ expect(pref.value).to eq value
+ end
+
+ it ':password' do
+ value = 'This is a password'
+ key = 'password_key'
+ pref = round_trip_preference(key, value)
+ expect(pref.value).to eq value
+ end
+
+ it ':any' do
+ value = [1, 2]
+ key = 'any_key'
+ pref = round_trip_preference(key, value)
+ expect(pref.value).to eq value
+ end
+ end
+end
diff --git a/core/spec/models/spree/preferences/configuration_spec.rb b/core/spec/models/spree/preferences/configuration_spec.rb
new file mode 100644
index 00000000000..10ed8ff9d5f
--- /dev/null
+++ b/core/spec/models/spree/preferences/configuration_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Spree::Preferences::Configuration, type: :model do
+ before :all do
+ class AppConfig < Spree::Preferences::Configuration
+ preference :color, :string, default: :blue
+ end
+ @config = AppConfig.new
+ end
+
+ it 'has named methods to access preferences' do
+ @config.color = 'orange'
+ expect(@config.color).to eq 'orange'
+ end
+
+ it 'uses [ ] to access preferences' do
+ @config[:color] = 'red'
+ expect(@config[:color]).to eq 'red'
+ end
+
+ it 'uses set/get to access preferences' do
+ @config.set :color, 'green'
+ expect(@config.get(:color)).to eq 'green'
+ end
+end
diff --git a/core/spec/models/spree/preferences/preferable_spec.rb b/core/spec/models/spree/preferences/preferable_spec.rb
new file mode 100644
index 00000000000..4accb91bf09
--- /dev/null
+++ b/core/spec/models/spree/preferences/preferable_spec.rb
@@ -0,0 +1,337 @@
+require 'spec_helper'
+
+describe Spree::Preferences::Preferable, type: :model do
+ before :all do
+ class A
+ include Spree::Preferences::Preferable
+ attr_reader :id
+
+ def initialize
+ @id = rand(999)
+ end
+
+ def preferences
+ @preferences ||= default_preferences
+ end
+
+ preference :color, :string, default: 'green'
+ end
+
+ class B < A
+ preference :flavor, :string
+ end
+ end
+
+ before do
+ @a = A.new
+ allow(@a).to receive_messages(persisted?: true)
+ @b = B.new
+ allow(@b).to receive_messages(persisted?: true)
+
+ # ensure we're persisting as that is the default
+ #
+ store = Spree::Preferences::Store.instance
+ store.persistence = true
+ end
+
+ describe 'preference definitions' do
+ it 'parent should not see child definitions' do
+ expect(@a.has_preference?(:color)).to be true
+ expect(@a.has_preference?(:flavor)).not_to be true
+ end
+
+ it 'child should have parent and own definitions' do
+ expect(@b.has_preference?(:color)).to be true
+ expect(@b.has_preference?(:flavor)).to be true
+ end
+
+ it 'instances have defaults' do
+ expect(@a.preferred_color).to eq 'green'
+ expect(@b.preferred_color).to eq 'green'
+ expect(@b.preferred_flavor).to be_nil
+ end
+
+ it 'can be asked if it has a preference definition' do
+ expect(@a.has_preference?(:color)).to be true
+ expect(@a.has_preference?(:bad)).to be false
+ end
+
+ it 'can be asked and raises' do
+ expect do
+ @a.has_preference! :flavor
+ end.to raise_error(NoMethodError, 'flavor preference not defined')
+ end
+
+ it 'has a type' do
+ expect(@a.preferred_color_type).to eq :string
+ expect(@a.preference_type(:color)).to eq :string
+ end
+
+ it 'has a default' do
+ expect(@a.preferred_color_default).to eq 'green'
+ expect(@a.preference_default(:color)).to eq 'green'
+ end
+
+ it 'raises if not defined' do
+ expect do
+ @a.get_preference :flavor
+ end.to raise_error(NoMethodError, 'flavor preference not defined')
+ end
+ end
+
+ describe 'preference access' do
+ it 'handles ghost methods for preferences' do
+ @a.preferred_color = 'blue'
+ expect(@a.preferred_color).to eq 'blue'
+ end
+
+ it 'parent and child instances have their own prefs' do
+ @a.preferred_color = 'red'
+ @b.preferred_color = 'blue'
+
+ expect(@a.preferred_color).to eq 'red'
+ expect(@b.preferred_color).to eq 'blue'
+ end
+
+ it 'raises when preference not defined' do
+ expect do
+ @a.set_preference(:bad, :bone)
+ end.to raise_exception(NoMethodError, 'bad preference not defined')
+ end
+
+ it 'builds a hash of preferences' do
+ @b.preferred_flavor = :strawberry
+ expect(@b.preferences[:flavor]).to eq 'strawberry'
+ expect(@b.preferences[:color]).to eq 'green' # default from A
+ end
+
+ it 'builds a hash of preference defaults' do
+ expect(@b.default_preferences).to eq(flavor: nil,
+ color: 'green')
+ end
+
+ context 'converts integer preferences to integer values' do
+ before do
+ A.preference :is_integer, :integer
+ end
+
+ it 'with strings' do
+ @a.set_preference(:is_integer, '3')
+ expect(@a.preferences[:is_integer]).to eq(3)
+
+ @a.set_preference(:is_integer, '')
+ expect(@a.preferences[:is_integer]).to eq(0)
+ end
+ end
+
+ context 'converts decimal preferences to BigDecimal values' do
+ before do
+ A.preference :if_decimal, :decimal
+ end
+
+ it 'returns a BigDecimal' do
+ @a.set_preference(:if_decimal, 3.3)
+ expect(@a.preferences[:if_decimal].class).to eq(BigDecimal)
+ end
+
+ it 'with strings' do
+ @a.set_preference(:if_decimal, '3.3')
+ expect(@a.preferences[:if_decimal]).to eq(3.3)
+
+ @a.set_preference(:if_decimal, '')
+ expect(@a.preferences[:if_decimal]).to eq(0.0)
+ end
+ end
+
+ context 'converts boolean preferences to boolean values' do
+ before do
+ A.preference :is_boolean, :boolean, default: true
+ end
+
+ it 'with strings' do
+ @a.set_preference(:is_boolean, '0')
+ expect(@a.preferences[:is_boolean]).to be false
+ @a.set_preference(:is_boolean, 'f')
+ expect(@a.preferences[:is_boolean]).to be false
+ @a.set_preference(:is_boolean, 't')
+ expect(@a.preferences[:is_boolean]).to be true
+ end
+
+ it 'with integers' do
+ @a.set_preference(:is_boolean, 0)
+ expect(@a.preferences[:is_boolean]).to be false
+ @a.set_preference(:is_boolean, 1)
+ expect(@a.preferences[:is_boolean]).to be true
+ end
+
+ it 'with an empty string' do
+ @a.set_preference(:is_boolean, '')
+ expect(@a.preferences[:is_boolean]).to be false
+ end
+
+ it 'with an empty hash' do
+ @a.set_preference(:is_boolean, [])
+ expect(@a.preferences[:is_boolean]).to be false
+ end
+ end
+
+ context 'converts array preferences to array values' do
+ before do
+ A.preference :is_array, :array, default: []
+ end
+
+ it 'with arrays' do
+ @a.set_preference(:is_array, [])
+ expect(@a.preferences[:is_array]).to be_is_a(Array)
+ end
+
+ it 'with string' do
+ @a.set_preference(:is_array, 'string')
+ expect(@a.preferences[:is_array]).to be_is_a(Array)
+ end
+
+ it 'with hash' do
+ @a.set_preference(:is_array, {})
+ expect(@a.preferences[:is_array]).to be_is_a(Array)
+ end
+ end
+
+ context 'converts hash preferences to hash values' do
+ before do
+ A.preference :is_hash, :hash, default: {}
+ end
+
+ it 'with hash' do
+ @a.set_preference(:is_hash, {})
+ expect(@a.preferences[:is_hash]).to be_is_a(Hash)
+ end
+
+ it 'with hash and keys are integers' do
+ @a.set_preference(:is_hash, 1 => 2, 3 => 4)
+ expect(@a.preferences[:is_hash]).to eql(1 => 2, 3 => 4)
+ end
+
+ it 'with string' do
+ @a.set_preference(:is_hash, '{"0"=>{"answer"=>"1", "value"=>"No"}}')
+ expect(@a.preferences[:is_hash]).to be_is_a(Hash)
+ end
+
+ it 'with boolean' do
+ @a.set_preference(:is_hash, false)
+ expect(@a.preferences[:is_hash]).to be_is_a(Hash)
+ @a.set_preference(:is_hash, true)
+ expect(@a.preferences[:is_hash]).to be_is_a(Hash)
+ end
+
+ it 'with simple array' do
+ @a.set_preference(:is_hash, ['key', 'value', 'another key', 'another value'])
+ expect(@a.preferences[:is_hash]).to be_is_a(Hash)
+ expect(@a.preferences[:is_hash]['key']).to eq('value')
+ expect(@a.preferences[:is_hash]['another key']).to eq('another value')
+ end
+
+ it 'with a nested array' do
+ @a.set_preference(:is_hash, [['key', 'value'], ['another key', 'another value']])
+ expect(@a.preferences[:is_hash]).to be_is_a(Hash)
+ expect(@a.preferences[:is_hash]['key']).to eq('value')
+ expect(@a.preferences[:is_hash]['another key']).to eq('another value')
+ end
+
+ it 'with single array' do
+ expect { @a.set_preference(:is_hash, ['key']) }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'converts any preferences to any values' do
+ before do
+ A.preference :product_ids, :any, default: []
+ A.preference :product_attributes, :any, default: {}
+ end
+
+ it 'with array' do
+ expect(@a.preferences[:product_ids]).to eq([])
+ @a.set_preference(:product_ids, [1, 2])
+ expect(@a.preferences[:product_ids]).to eq([1, 2])
+ end
+
+ it 'with hash' do
+ expect(@a.preferences[:product_attributes]).to eq({})
+ @a.set_preference(:product_attributes, id: 1, name: 2)
+ expect(@a.preferences[:product_attributes]).to eq(id: 1, name: 2)
+ end
+ end
+ end
+
+ describe 'persisted preferables' do
+ before(:all) do
+ class CreatePrefTest < ActiveRecord::Migration[4.2]
+ def self.up
+ create_table :pref_tests do |t|
+ t.string :col
+ t.text :preferences
+ end
+ end
+
+ def self.down
+ drop_table :pref_tests
+ end
+ end
+
+ @migration_verbosity = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+ CreatePrefTest.migrate(:up)
+
+ class PrefTest < Spree::Base
+ preference :pref_test_pref, :string, default: 'abc'
+ preference :pref_test_any, :any, default: []
+ end
+ end
+
+ after(:all) do
+ CreatePrefTest.migrate(:down)
+ ActiveRecord::Migration.verbose = @migration_verbosity
+ end
+
+ before do
+ # load PrefTest table
+ PrefTest.first
+ @pt = PrefTest.create
+ end
+
+ describe 'pending preferences for new activerecord objects' do
+ it 'saves preferences after record is saved' do
+ pr = PrefTest.new
+ pr.set_preference(:pref_test_pref, 'XXX')
+ expect(pr.get_preference(:pref_test_pref)).to eq('XXX')
+ pr.save!
+ expect(pr.get_preference(:pref_test_pref)).to eq('XXX')
+ end
+
+ it 'saves preferences for serialized object' do
+ pr = PrefTest.new
+ pr.set_preference(:pref_test_any, [1, 2])
+ expect(pr.get_preference(:pref_test_any)).to eq([1, 2])
+ pr.save!
+ expect(pr.get_preference(:pref_test_any)).to eq([1, 2])
+ end
+ end
+
+ it 'clear preferences' do
+ @pt.set_preference(:pref_test_pref, 'xyz')
+ expect(@pt.preferred_pref_test_pref).to eq('xyz')
+ @pt.clear_preferences
+ expect(@pt.preferred_pref_test_pref).to eq('abc')
+ end
+
+ it 'clear preferences when record is deleted' do
+ @pt.save!
+ @pt.preferred_pref_test_pref = 'lmn'
+ @pt.save!
+ @pt.destroy
+ @pt1 = PrefTest.new(col: 'aaaa')
+ @pt1.id = @pt.id
+ @pt1.save!
+ expect(@pt1.get_preference(:pref_test_pref)).to eq('abc')
+ end
+ end
+end
diff --git a/core/spec/models/spree/preferences/scoped_store_spec.rb b/core/spec/models/spree/preferences/scoped_store_spec.rb
new file mode 100644
index 00000000000..4b33d3ce8f3
--- /dev/null
+++ b/core/spec/models/spree/preferences/scoped_store_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Spree::Preferences::ScopedStore, type: :model do
+ subject { scoped_store }
+
+ let(:scoped_store) { described_class.new(prefix, suffix) }
+
+ let(:prefix) { nil }
+ let(:suffix) { nil }
+
+ describe '#store' do
+ subject { scoped_store.store }
+
+ it { is_expected.to be Spree::Preferences::Store.instance }
+ end
+
+ context 'stubbed store' do
+ let(:store) { double(:store) }
+
+ before do
+ allow(scoped_store).to receive(:store).and_return(store)
+ end
+
+ context 'with a prefix' do
+ let(:prefix) { 'my_class' }
+
+ it 'can fetch' do
+ expect(store).to receive(:fetch).with('my_class/attr')
+ scoped_store.fetch('attr') { 'default' }
+ end
+
+ it 'can assign' do
+ expect(store).to receive(:[]=).with('my_class/attr', 'val')
+ scoped_store['attr'] = 'val'
+ end
+
+ it 'can delete' do
+ expect(store).to receive(:delete).with('my_class/attr')
+ scoped_store.delete('attr')
+ end
+
+ context 'and suffix' do
+ let(:suffix) { 123 }
+
+ it 'can fetch' do
+ expect(store).to receive(:fetch).with('my_class/attr/123')
+ scoped_store.fetch('attr') { 'default' }
+ end
+
+ it 'can assign' do
+ expect(store).to receive(:[]=).with('my_class/attr/123', 'val')
+ scoped_store['attr'] = 'val'
+ end
+
+ it 'can delete' do
+ expect(store).to receive(:delete).with('my_class/attr/123')
+ scoped_store.delete('attr')
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/preferences/store_spec.rb b/core/spec/models/spree/preferences/store_spec.rb
new file mode 100644
index 00000000000..70f4dccab82
--- /dev/null
+++ b/core/spec/models/spree/preferences/store_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Spree::Preferences::Store, type: :model do
+ before do
+ @store = Spree::Preferences::StoreInstance.new
+ end
+
+ it 'sets and gets a key' do
+ @store.set :test, 1
+ expect(@store.exist?(:test)).to be true
+ expect(@store.get(:test)).to eq 1
+ end
+
+ it 'can set and get false values when cache return nil' do
+ @store.set :test, false
+ expect(@store.get(:test)).to be false
+ end
+
+ it 'will return db value when cache is emtpy and cache the db value' do
+ preference = Spree::Preference.where(key: 'test').first_or_initialize
+ preference.value = '123'
+ preference.save
+
+ Rails.cache.clear
+ expect(@store.get(:test)).to eq '123'
+ expect(Rails.cache.read(:test)).to eq '123'
+ end
+
+ it 'returns and cache fallback value when supplied' do
+ Rails.cache.clear
+ expect(@store.get(:test) { false }).to be false
+ expect(Rails.cache.read(:test)).to be false
+ end
+
+ it 'returns but not cache fallback value when persistence is disabled' do
+ Rails.cache.clear
+ allow(@store).to receive_messages(should_persist?: false)
+ expect(@store.get(:test) { true }).to be true
+ expect(Rails.cache.exist?(:test)).to be false
+ end
+
+ it "returns nil when key can't be found and fallback value is not supplied" do
+ expect(@store.get(:random_key) { nil }).to be_nil
+ end
+end
diff --git a/core/spec/models/spree/price_spec.rb b/core/spec/models/spree/price_spec.rb
new file mode 100644
index 00000000000..f26f9fff989
--- /dev/null
+++ b/core/spec/models/spree/price_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+describe Spree::Price, type: :model do
+ describe '#amount=' do
+ let(:price) { Spree::Price.new }
+ let(:amount) { '3,0A0' }
+
+ before do
+ price.amount = amount
+ end
+
+ it 'is expected to equal to localized number' do
+ expect(price.amount).to eq(Spree::LocalizedNumber.parse(amount))
+ end
+ end
+
+ describe '#price' do
+ let(:price) { Spree::Price.new }
+ let(:amount) { 3000.00 }
+
+ context 'when amount is changed' do
+ before do
+ price.amount = amount
+ end
+
+ it 'is expected to equal to price' do
+ expect(price.amount).to eq(price.price)
+ end
+ end
+ end
+
+ describe 'validations' do
+ subject { Spree::Price.new variant: variant, amount: amount }
+
+ let(:variant) { stub_model Spree::Variant }
+
+ context 'when the amount is nil' do
+ let(:amount) { nil }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when the amount is less than 0' do
+ let(:amount) { -1 }
+
+ it 'has 1 error_on' do
+ expect(subject.error_on(:amount).size).to eq(1)
+ end
+ it 'populates errors' do
+ subject.valid?
+ expect(subject.errors.messages[:amount].first).to eq 'must be greater than or equal to 0'
+ end
+ end
+
+ context 'when the amount is greater than maximum amount' do
+ let(:amount) { Spree::Price::MAXIMUM_AMOUNT + 1 }
+
+ it 'has 1 error_on' do
+ expect(subject.error_on(:amount).size).to eq(1)
+ end
+ it 'populates errors' do
+ subject.valid?
+ expect(subject.errors.messages[:amount].first).to eq "must be less than or equal to #{Spree::Price::MAXIMUM_AMOUNT}"
+ end
+ end
+
+ context 'when the amount is between 0 and the maximum amount' do
+ let(:amount) { Spree::Price::MAXIMUM_AMOUNT }
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ describe '#price_including_vat_for(zone)' do
+ subject(:price_with_vat) { price.price_including_vat_for(price_options) }
+
+ let(:variant) { stub_model Spree::Variant }
+ let(:default_zone) { Spree::Zone.new }
+ let(:zone) { Spree::Zone.new }
+ let(:amount) { 10 }
+ let(:tax_category) { Spree::TaxCategory.new }
+ let(:price) { Spree::Price.new variant: variant, amount: amount }
+ let(:price_options) { { tax_zone: zone } }
+
+ context 'when called with a non-default zone' do
+ before do
+ allow(variant).to receive(:tax_category).and_return(tax_category)
+ expect(price).to receive(:default_zone).at_least(:once).and_return(default_zone)
+ allow(price).to receive(:apply_foreign_vat?).and_return(true)
+ allow(price).to receive(:included_tax_amount).with(tax_zone: default_zone, tax_category: tax_category).and_return(0.19)
+ allow(price).to receive(:included_tax_amount).with(tax_zone: zone, tax_category: tax_category).and_return(0.25)
+ end
+
+ it 'returns the correct price including another VAT to two digits' do
+ expect(price_with_vat).to eq(10.50)
+ end
+ end
+
+ context 'when called from the default zone' do
+ before do
+ allow(variant).to receive(:tax_category).and_return(tax_category)
+ expect(price).to receive(:default_zone).at_least(:once).and_return(zone)
+ end
+
+ it 'returns the correct price' do
+ expect(price).to receive(:price).and_call_original
+ expect(price_with_vat).to eq(10.00)
+ end
+ end
+
+ context 'when no default zone is set' do
+ before do
+ allow(variant).to receive(:tax_category).and_return(tax_category)
+ expect(price).to receive(:default_zone).at_least(:once).and_return(nil)
+ end
+
+ it 'returns the correct price' do
+ expect(price).to receive(:price).and_call_original
+ expect(price.price_including_vat_for(tax_zone: zone)).to eq(10.00)
+ end
+ end
+ end
+
+ describe '#display_price_including_vat_for(zone)' do
+ subject { Spree::Price.new amount: 10 }
+
+ it 'calls #price_including_vat_for' do
+ expect(subject).to receive(:price_including_vat_for)
+ subject.display_price_including_vat_for(nil)
+ end
+ end
+end
diff --git a/core/spec/models/spree/product/scopes_spec.rb b/core/spec/models/spree/product/scopes_spec.rb
new file mode 100644
index 00000000000..103220df276
--- /dev/null
+++ b/core/spec/models/spree/product/scopes_spec.rb
@@ -0,0 +1,176 @@
+require 'spec_helper'
+
+describe 'Product scopes', type: :model do
+ let!(:product) { create(:product) }
+
+ describe '#available' do
+ context 'when discontinued' do
+ let!(:discontinued_product) { create(:product, discontinue_on: Time.current - 1.day) }
+
+ it { expect(Spree::Product.available).not_to include(discontinued_product) }
+ end
+
+ context 'when not discontinued' do
+ let!(:product_2) { create(:product, discontinue_on: Time.current + 1.day) }
+
+ it { expect(Spree::Product.available).to include(product_2) }
+ end
+
+ context 'when available' do
+ let!(:product_2) { create(:product, available_on: Time.current - 1.day) }
+
+ it { expect(Spree::Product.available).to include(product_2) }
+ end
+
+ context 'when not available' do
+ let!(:unavailable_product) { create(:product, available_on: Time.current + 1.day) }
+
+ it { expect(Spree::Product.available).not_to include(unavailable_product) }
+ end
+ end
+
+ context 'A product assigned to parent and child taxons' do
+ before do
+ @taxonomy = create(:taxonomy)
+ @root_taxon = @taxonomy.root
+
+ @parent_taxon = create(:taxon, name: 'Parent', taxonomy_id: @taxonomy.id, parent: @root_taxon)
+ @child_taxon = create(:taxon, name: 'Child 1', taxonomy_id: @taxonomy.id, parent: @parent_taxon)
+ @parent_taxon.reload # Need to reload for descendents to show up
+
+ product.taxons << @parent_taxon
+ product.taxons << @child_taxon
+ end
+
+ it 'calling Product.in_taxon returns products in child taxons' do
+ product.taxons -= [@child_taxon]
+ expect(product.taxons.count).to eq(1)
+
+ expect(Spree::Product.in_taxon(@parent_taxon)).to include(product)
+ end
+
+ it 'calling Product.in_taxon should not return duplicate records' do
+ expect(Spree::Product.in_taxon(@parent_taxon).to_a.size).to eq(1)
+ end
+
+ context 'orders products based on their ordering within the classifications' do
+ let(:other_taxon) { create(:taxon, products: [product]) }
+ let!(:product_2) { create(:product, taxons: [@child_taxon, other_taxon]) }
+
+ it 'by initial ordering' do
+ expect(Spree::Product.in_taxon(@child_taxon)).to eq([product, product_2])
+ expect(Spree::Product.in_taxon(other_taxon)).to eq([product, product_2])
+ end
+
+ it 'after ordering changed' do
+ [@child_taxon, other_taxon].each do |taxon|
+ Spree::Classification.find_by(taxon: taxon, product: product).insert_at(2)
+ expect(Spree::Product.in_taxon(taxon)).to eq([product_2, product])
+ end
+ end
+ end
+ end
+
+ context 'property scopes' do
+ let(:name) { 'A proper tee' }
+ let(:value) { 'A proper value' }
+ let!(:property) { create(:property, name: name) }
+
+ before do
+ product.properties << property
+ product.product_properties.find_by(property: property).update_column(:value, value)
+ end
+
+ context 'with_property' do
+ let(:with_property) { Spree::Product.method(:with_property) }
+
+ it "finds by a property's name" do
+ expect(with_property.call(name).count).to eq(1)
+ end
+
+ it "doesn't find any properties with an unknown name" do
+ expect(with_property.call('fake').count).to eq(0)
+ end
+
+ it 'finds by a property' do
+ expect(with_property.call(property).count).to eq(1)
+ end
+
+ it 'finds by an id' do
+ expect(with_property.call(property.id).count).to eq(1)
+ end
+
+ it 'cannot find a property with an unknown id' do
+ expect(with_property.call(0).count).to eq(0)
+ end
+ end
+
+ context 'with_property_value' do
+ let(:with_property_value) { Spree::Product.method(:with_property_value) }
+
+ it "finds by a property's name" do
+ expect(with_property_value.call(name, value).count).to eq(1)
+ end
+
+ it "cannot find by an unknown property's name" do
+ expect(with_property_value.call('fake', value).count).to eq(0)
+ end
+
+ it 'cannot find with a name by an incorrect value' do
+ expect(with_property_value.call(name, 'fake').count).to eq(0)
+ end
+
+ it 'finds by a property' do
+ expect(with_property_value.call(property, value).count).to eq(1)
+ end
+
+ it 'cannot find with a property by an incorrect value' do
+ expect(with_property_value.call(property, 'fake').count).to eq(0)
+ end
+
+ it 'finds by an id with a value' do
+ expect(with_property_value.call(property.id, value).count).to eq(1)
+ end
+
+ it 'cannot find with an invalid id' do
+ expect(with_property_value.call(0, value).count).to eq(0)
+ end
+
+ it 'cannot find with an invalid value' do
+ expect(with_property_value.call(property.id, 'fake').count).to eq(0)
+ end
+ end
+ end
+
+ context '#add_simple_scopes' do
+ let(:simple_scopes) { [:ascend_by_updated_at, :descend_by_name] }
+
+ before do
+ Spree::Product.add_simple_scopes(simple_scopes)
+ end
+
+ context 'define scope' do
+ context 'ascend_by_updated_at' do
+ context 'on class' do
+ it { expect(Spree::Product.ascend_by_updated_at.to_sql).to eq Spree::Product.order(Arel.sql("#{Spree::Product.quoted_table_name}.updated_at ASC")).to_sql }
+ end
+
+ context 'on ActiveRecord::Relation' do
+ it { expect(Spree::Product.limit(2).ascend_by_updated_at.to_sql).to eq Spree::Product.limit(2).order(Arel.sql("#{Spree::Product.quoted_table_name}.updated_at ASC")).to_sql }
+ it { expect(Spree::Product.limit(2).ascend_by_updated_at.to_sql).to eq Spree::Product.ascend_by_updated_at.limit(2).to_sql }
+ end
+ end
+
+ context 'descend_by_name' do
+ context 'on class' do
+ it { expect(Spree::Product.descend_by_name.to_sql).to eq Spree::Product.order(Arel.sql("#{Spree::Product.quoted_table_name}.name DESC")).to_sql }
+ end
+
+ context 'on ActiveRecord::Relation' do
+ it { expect(Spree::Product.limit(2).descend_by_name.to_sql).to eq Spree::Product.limit(2).order(Arel.sql("#{Spree::Product.quoted_table_name}.name DESC")).to_sql }
+ it { expect(Spree::Product.limit(2).descend_by_name.to_sql).to eq Spree::Product.descend_by_name.limit(2).to_sql }
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/product_duplicator_spec.rb b/core/spec/models/spree/product_duplicator_spec.rb
new file mode 100644
index 00000000000..7653430d465
--- /dev/null
+++ b/core/spec/models/spree/product_duplicator_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+module Spree
+ describe Spree::ProductDuplicator, type: :model do
+ let!(:product) { create(:product, properties: [create(:property, name: 'MyProperty')]) }
+ let!(:duplicator) { Spree::ProductDuplicator.new(product) }
+
+ let(:file) { File.open(File.expand_path('../../fixtures/thinking-cat.jpg', __dir__)) }
+ let(:params) do
+ {
+ viewable_id: product.master.id,
+ viewable_type: 'Spree::Variant',
+ attachment: file,
+ alt: 'position 1',
+ position: 1
+ }
+ end
+
+ before do
+ new_image = Spree::Image.new(params)
+ unless Rails.application.config.use_paperclip
+ new_image.attachment.attach(io: file, filename: File.basename(file))
+ end
+ new_image.save!
+ end
+
+ it 'will duplicate the product' do
+ expect { duplicator.duplicate }.to change { Spree::Product.count }.by(1)
+ end
+
+ it 'will duplicate already duplicated product' do
+ Timecop.scale(3600)
+ expect { 3.times { duplicator.duplicate } }.to change { Spree::Product.count }.by(3)
+ Timecop.return
+ end
+
+ context 'when image duplication enabled' do
+ it 'will duplicate the product images' do
+ expect { duplicator.duplicate }.to change { Spree::Image.count }.by(1)
+ end
+ end
+
+ context 'when image duplication disabled' do
+ let!(:duplicator) { Spree::ProductDuplicator.new(product, false) }
+
+ it 'will not duplicate the product images' do
+ expect { duplicator.duplicate }.to change { Spree::Image.count }.by(0)
+ end
+ end
+
+ context 'image duplication default' do
+ context 'when default is set to true' do
+ it 'clones images if no flag passed to initializer' do
+ expect { duplicator.duplicate }.to change { Spree::Image.count }.by(1)
+ end
+ end
+
+ context 'when default is set to false' do
+ before do
+ ProductDuplicator.clone_images_default = false
+ end
+
+ after do
+ ProductDuplicator.clone_images_default = true
+ end
+
+ it 'does not clone images if no flag passed to initializer' do
+ expect { ProductDuplicator.new(product).duplicate }.to change { Spree::Image.count }.by(0)
+ end
+ end
+ end
+
+ context 'product attributes' do
+ let!(:new_product) { duplicator.duplicate }
+
+ it 'will set an unique name' do
+ expect(new_product.name).to eql "COPY OF #{product.name}"
+ end
+
+ it 'will set an unique sku' do
+ expect(new_product.sku).to include 'COPY OF SKU'
+ end
+
+ it 'copied the properties' do
+ expect(new_product.product_properties.count).to be 1
+ expect(new_product.product_properties.first.property.name).to eql 'MyProperty'
+ end
+ end
+
+ context 'with variants' do
+ let(:option_type) { create(:option_type, name: 'MyOptionType') }
+ let(:option_value1) { create(:option_value, name: 'OptionValue1', option_type: option_type) }
+ let(:option_value2) { create(:option_value, name: 'OptionValue2', option_type: option_type) }
+
+ let!(:variant1) { create(:variant, product: product, option_values: [option_value1]) }
+ let!(:variant2) { create(:variant, product: product, option_values: [option_value2]) }
+
+ it 'will duplciate the variants' do
+ # will change the count by 3, since there will be a master variant as well
+ expect { duplicator.duplicate }.to change { Spree::Variant.count }.by(3)
+ end
+
+ it 'will not duplicate the option values' do
+ expect { duplicator.duplicate }.to change { Spree::OptionValue.count }.by(0)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/product_filter_spec.rb b/core/spec/models/spree/product_filter_spec.rb
new file mode 100644
index 00000000000..57fce5f83f0
--- /dev/null
+++ b/core/spec/models/spree/product_filter_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'spree/core/product_filters'
+
+describe 'product filters', type: :model do
+ # Regression test for #1709
+ context 'finds products filtered by brand' do
+ let(:product) { create(:product) }
+
+ before do
+ Spree::Property.create!(name: 'brand', presentation: 'brand')
+ product.set_property('brand', 'Nike')
+ end
+
+ it 'does not attempt to call value method on Arel::Table' do
+ expect { Spree::Core::ProductFilters.brand_filter }.not_to raise_error
+ end
+
+ it "can find products in the 'Nike' brand" do
+ expect(Spree::Product.brand_any('Nike')).to include(product)
+ end
+ it 'sorts products without brand specified' do
+ product.set_property('brand', 'Nike')
+ create(:product).set_property('brand', nil)
+ expect { Spree::Core::ProductFilters.brand_filter[:labels] }.not_to raise_error
+ end
+ end
+end
diff --git a/core/spec/models/spree/product_property_spec.rb b/core/spec/models/spree/product_property_spec.rb
new file mode 100644
index 00000000000..02c8b93a0b7
--- /dev/null
+++ b/core/spec/models/spree/product_property_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Spree::ProductProperty, type: :model do
+ context 'touching' do
+ it 'updates product' do
+ pp = create(:product_property)
+ expect(pp.product).to receive(:touch)
+ pp.touch
+ end
+ end
+
+ context 'property_name=' do
+ before do
+ @pp = create(:product_property)
+ end
+
+ it 'assigns property' do
+ @pp.property_name = 'Size'
+ expect(@pp.property.name).to eq('Size')
+ end
+ end
+
+ context 'ransackable_associations' do
+ it { expect(Spree::ProductProperty.whitelisted_ransackable_associations).to include('property') }
+ end
+end
diff --git a/core/spec/models/spree/product_spec.rb b/core/spec/models/spree/product_spec.rb
new file mode 100644
index 00000000000..67876190248
--- /dev/null
+++ b/core/spec/models/spree/product_spec.rb
@@ -0,0 +1,686 @@
+require 'spec_helper'
+
+module ThirdParty
+ class Extension < Spree::Base
+ # nasty hack so we don't have to create a table to back this fake model
+ self.table_name = 'spree_products'
+ end
+end
+
+describe Spree::Product, type: :model do
+ context 'product instance' do
+ let(:product) { create(:product) }
+ let(:variant) { create(:variant, product: product) }
+
+ %w[purchasable backorderable in_stock].each do |method_name|
+ describe "#{method_name}?" do
+ before { allow(product).to receive(:variants_including_master) { [product.master, variant] } }
+
+ it "returns false if no variant is #{method_name.humanize.downcase}" do
+ allow_any_instance_of(Spree::Variant).to receive("#{method_name}?").and_return(false)
+
+ expect(product.send("#{method_name}?")).to eq false
+ end
+
+ it "returns true if variant that is #{method_name.humanize.downcase} exists" do
+ allow(variant).to receive("#{method_name}?").and_return(true)
+
+ expect(product.send("#{method_name}?")).to eq true
+ end
+ end
+ end
+
+ context '#duplicate' do
+ before do
+ allow(product).to receive_messages taxons: [create(:taxon)]
+ end
+
+ it 'duplicates product' do
+ clone = product.duplicate
+ expect(clone.name).to eq('COPY OF ' + product.name)
+ expect(clone.master.sku).to eq('COPY OF ' + product.master.sku)
+ expect(clone.taxons).to eq(product.taxons)
+ expect(clone.images.size).to eq(product.images.size)
+ end
+
+ it 'calls #duplicate_extra' do
+ expect_any_instance_of(Spree::Product).to receive(:duplicate_extra).
+ with(product)
+ expect(product).not_to receive(:duplicate_extra)
+ product.duplicate
+ end
+ end
+
+ context 'master variant' do
+ context 'when master variant changed' do
+ before do
+ product.master.sku = 'Something changed'
+ end
+
+ it 'saves the master' do
+ expect(product.master).to receive(:save!)
+ product.save
+ end
+ end
+
+ context 'when master default price changed' do
+ before do
+ master = product.master
+ master.default_price.price = 11
+ master.save!
+ product.master.default_price.price = 12
+ end
+
+ it 'saves the master' do
+ expect(product.master).to receive(:save!)
+ product.save
+ end
+
+ it 'saves the default price' do
+ expect(product.master.default_price).to receive(:save)
+ product.save
+ end
+ end
+
+ context "when master variant and price haven't changed" do
+ it 'does not save the master' do
+ expect(product.master).not_to receive(:save!)
+ product.save
+ end
+ end
+ end
+
+ context 'product has no variants' do
+ context '#destroy' do
+ it 'sets deleted_at value' do
+ product.destroy
+ expect(product.deleted_at).not_to be_nil
+ expect(product.master.reload.deleted_at).not_to be_nil
+ end
+ end
+ end
+
+ context 'product has variants' do
+ before do
+ create(:variant, product: product)
+ end
+
+ context '#destroy' do
+ it 'sets deleted_at value' do
+ product.destroy
+ expect(product.deleted_at).not_to be_nil
+ expect(product.variants_including_master.all? { |v| !v.deleted_at.nil? }).to be true
+ end
+ end
+ end
+
+ context '#price' do
+ # Regression test for #1173
+ it 'strips non-price characters' do
+ product.price = '$10'
+ expect(product.price).to eq(10.0)
+ end
+ end
+
+ context '#display_price' do
+ before { product.price = 10.55 }
+
+ it 'shows the amount' do
+ expect(product.display_price.to_s).to eq('$10.55')
+ end
+
+ context 'with currency set to JPY' do
+ before do
+ product.master.default_price.currency = 'JPY'
+ product.master.default_price.save!
+ Spree::Config[:currency] = 'JPY'
+ end
+
+ it 'displays the currency in yen' do
+ expect(product.display_price.to_s).to eq('Â¥11')
+ end
+ end
+ end
+
+ context '#available?' do
+ it 'is available if date is in the past' do
+ product.available_on = 1.day.ago
+ expect(product).to be_available
+ end
+
+ it 'is not available if date is nil or in the future' do
+ product.available_on = nil
+ expect(product).not_to be_available
+
+ product.available_on = 1.day.from_now
+ expect(product).not_to be_available
+ end
+
+ it 'is not available if destroyed' do
+ product.destroy
+ expect(product).not_to be_available
+ end
+ end
+
+ context '#can_supply?' do
+ it 'is true' do
+ expect(product.can_supply?).to be(true)
+ end
+
+ it 'is false' do
+ product.variants_including_master.each { |v| v.stock_items.update_all count_on_hand: 0, backorderable: false }
+ expect(product.can_supply?).to be(false)
+ end
+ end
+
+ context 'variants_and_option_values' do
+ let!(:high) { create(:variant, product: product) }
+ let!(:low) { create(:variant, product: product) }
+
+ before { high.option_values.destroy_all }
+
+ it 'returns only variants with option values' do
+ expect(product.variants_and_option_values).to eq([low])
+ end
+ end
+
+ context 'has stock movements' do
+ let(:variant) { product.master }
+ let(:stock_item) { variant.stock_items.first }
+
+ it 'doesnt raise ReadOnlyRecord error' do
+ Spree::StockMovement.create!(stock_item: stock_item, quantity: 1)
+ expect { product.destroy }.not_to raise_error
+ end
+ end
+
+ # Regression test for #8906
+ context 'tags' do
+ let(:tag_list) { %w[tag1 tag2] }
+
+ it "doesn't raise an error when adding tags to a product" do
+ expect { product.update(tag_list: tag_list) }.not_to raise_error
+ end
+ end
+
+ # Regression test for #3737
+ context 'has stock items' do
+ it 'can retrieve stock items' do
+ expect(product.master.stock_items.first).not_to be_nil
+ expect(product.stock_items.first).not_to be_nil
+ end
+ end
+
+ context 'slugs' do
+ it 'normalizes slug on update validation' do
+ product.slug = 'hey//joe'
+ product.valid?
+ expect(product.slug).not_to match '/'
+ end
+
+ it 'stores old slugs in FriendlyIds history' do
+ expect(product).to receive(:create_slug)
+ # Set it, otherwise the create_slug method avoids writing a new one
+ product.slug = 'custom-slug'
+ product.run_callbacks :save
+ end
+
+ context 'when product destroyed' do
+ it 'renames slug' do
+ expect { product.destroy }.to change(product, :slug)
+ end
+
+ context 'when slug is already at or near max length' do
+ before do
+ product.slug = 'x' * 255
+ product.save!
+ end
+
+ it 'truncates renamed slug to ensure it remains within length limit' do
+ product.destroy
+ expect(product.slug.length).to eq 255
+ end
+ end
+ end
+
+ it 'validates slug uniqueness' do
+ existing_product = product
+ new_product = create(:product)
+ new_product.slug = existing_product.slug
+
+ expect(new_product.valid?).to eq false
+ end
+
+ it "falls back to 'name-sku' for slug if regular name-based slug already in use" do
+ product1 = build(:product)
+ product1.name = 'test'
+ product1.sku = '123'
+ product1.save!
+
+ product2 = build(:product)
+ product2.name = 'test'
+ product2.sku = '456'
+ product2.save!
+
+ expect(product2.slug).to eq 'test-456'
+ end
+ end
+
+ describe '#discontinue_on_must_be_later_than_available_on' do
+ before { product.available_on = Date.today }
+
+ context 'available_on is a date earlier than discontinue_on' do
+ before { product.discontinue_on = 5.days.from_now }
+
+ it 'is valid' do
+ expect(product).to be_valid
+ end
+ end
+
+ context 'available_on is a date earlier than discontinue_on' do
+ before { product.discontinue_on = 5.days.ago }
+
+ context 'is not valid' do
+ before { product.valid? }
+
+ it { expect(product).not_to be_valid }
+ it { expect(product.errors[:discontinue_on]).to include(I18n.t(:invalid_date_range, scope: 'activerecord.errors.models.spree/product.attributes.discontinue_on')) }
+ end
+ end
+
+ context 'available_on and discontinue_on are nil' do
+ before do
+ product.discontinue_on = nil
+ product.available_on = nil
+ end
+
+ it 'is valid' do
+ expect(product).to be_valid
+ end
+ end
+ end
+
+ context 'hard deletion' do
+ it 'doesnt raise ActiveRecordError error' do
+ expect { product.really_destroy! }.not_to raise_error
+ end
+ end
+
+ context 'history' do
+ before do
+ @product = create(:product)
+ end
+
+ it 'keeps the history when the product is destroyed' do
+ @product.destroy
+
+ expect(@product.slugs.with_deleted).not_to be_empty
+ end
+
+ it 'updates the history when the product is restored' do
+ @product.destroy
+
+ @product.restore(recursive: true)
+
+ latest_slug = @product.slugs.find_by slug: @product.slug
+ expect(latest_slug).not_to be_nil
+ end
+ end
+ end
+
+ context 'properties' do
+ let(:product) { create(:product) }
+
+ it 'properly assigns properties' do
+ product.set_property('the_prop', 'value1')
+ expect(product.property('the_prop')).to eq('value1')
+
+ product.set_property('the_prop', 'value2')
+ expect(product.property('the_prop')).to eq('value2')
+ end
+
+ it 'does not create duplicate properties when set_property is called' do
+ expect do
+ product.set_property('the_prop', 'value2')
+ product.save
+ product.reload
+ end.not_to change(product.properties, :length)
+
+ expect do
+ product.set_property('the_prop_new', 'value')
+ product.save
+ product.reload
+ expect(product.property('the_prop_new')).to eq('value')
+ end.to change { product.properties.length }.by(1)
+ end
+
+ context 'optional property_presentation' do
+ subject { Spree::Property.where(name: 'foo').first.presentation }
+
+ let(:name) { 'foo' }
+ let(:presentation) { 'baz' }
+
+ describe 'is not used' do
+ before { product.set_property(name, 'bar') }
+
+ it { is_expected.to eq name }
+ end
+
+ describe 'is used' do
+ before { product.set_property(name, 'bar', presentation) }
+
+ it { is_expected.to eq presentation }
+ end
+ end
+
+ # Regression test for #2455
+ it "does not overwrite properties' presentation names" do
+ Spree::Property.where(name: 'foo').first_or_create!(presentation: "Foo's Presentation Name")
+ product.set_property('foo', 'value1')
+ product.set_property('bar', 'value2')
+ expect(Spree::Property.where(name: 'foo').first.presentation).to eq("Foo's Presentation Name")
+ expect(Spree::Property.where(name: 'bar').first.presentation).to eq('bar')
+ end
+
+ # Regression test for #4416
+ context '#possible_promotions' do
+ let!(:possible_promotion) { create(:promotion, advertise: true, starts_at: 1.day.ago) }
+ let!(:unadvertised_promotion) { create(:promotion, advertise: false, starts_at: 1.day.ago) }
+ let!(:inactive_promotion) { create(:promotion, advertise: true, starts_at: 1.day.since) }
+
+ before do
+ product.promotion_rules.create!(promotion: possible_promotion)
+ product.promotion_rules.create!(promotion: unadvertised_promotion)
+ product.promotion_rules.create!(promotion: inactive_promotion)
+ end
+
+ it 'lists the promotion as a possible promotion' do
+ expect(product.possible_promotions).to include(possible_promotion)
+ expect(product.possible_promotions).not_to include(unadvertised_promotion)
+ expect(product.possible_promotions).not_to include(inactive_promotion)
+ end
+ end
+ end
+
+ context '#create' do
+ let!(:prototype) { create(:prototype) }
+ let!(:product) { Spree::Product.new(name: 'Foo', price: 1.99, shipping_category_id: create(:shipping_category).id) }
+
+ before { product.prototype_id = prototype.id }
+
+ context 'when prototype is supplied' do
+ it 'creates properties based on the prototype' do
+ product.save
+ expect(product.properties.count).to eq(1)
+ end
+ end
+
+ context 'when prototype with option types is supplied' do
+ def build_option_type_with_values(name, values)
+ values.each_with_object(create(:option_type, name: name)) do |val, ot|
+ ot.option_values.create(name: val.downcase, presentation: val)
+ end
+ end
+
+ let(:prototype) do
+ size = build_option_type_with_values('size', %w(Small Medium Large))
+ create(:prototype, name: 'Size', option_types: [size])
+ end
+
+ let(:option_values_hash) do
+ hash = {}
+ prototype.option_types.each do |i|
+ hash[i.id.to_s] = i.option_value_ids
+ end
+ hash
+ end
+
+ it 'creates option types based on the prototype' do
+ product.save
+ expect(product.option_type_ids.length).to eq(1)
+ expect(product.option_type_ids).to eq(prototype.option_type_ids)
+ end
+
+ it 'creates product option types based on the prototype' do
+ product.save
+ expect(product.product_option_types.pluck(:option_type_id)).to eq(prototype.option_type_ids)
+ end
+
+ it 'creates variants from an option values hash with one option type' do
+ product.option_values_hash = option_values_hash
+ product.save
+ expect(product.variants.length).to eq(3)
+ end
+
+ it 'stills create variants when option_values_hash is given but prototype id is nil' do
+ product.option_values_hash = option_values_hash
+ product.prototype_id = nil
+ product.save
+ product.reload
+ expect(product.option_type_ids.length).to eq(1)
+ expect(product.option_type_ids).to eq(prototype.option_type_ids)
+ expect(product.variants.length).to eq(3)
+ end
+
+ it 'creates variants from an option values hash with multiple option types' do
+ color = build_option_type_with_values('color', %w(Red Green Blue))
+ logo = build_option_type_with_values('logo', %w(Ruby Rails Nginx))
+ option_values_hash[color.id.to_s] = color.option_value_ids
+ option_values_hash[logo.id.to_s] = logo.option_value_ids
+ product.option_values_hash = option_values_hash
+ product.save
+ product.reload
+ expect(product.option_type_ids.length).to eq(3)
+ expect(product.variants.length).to eq(27)
+ end
+ end
+ end
+
+ context '#images' do
+ let(:product) { create(:product) }
+ let(:file) { File.open(File.expand_path('../../fixtures/thinking-cat.jpg', __dir__)) }
+ let(:params) { { viewable_id: product.master.id, viewable_type: 'Spree::Variant', attachment: file, alt: 'position 2', position: 2 } }
+
+ before do
+ images = [
+ Spree::Image.create(params),
+ Spree::Image.create(params.merge(alt: 'position 1', position: 1)),
+ Spree::Image.create(params.merge(viewable_type: 'ThirdParty::Extension', alt: 'position 1', position: 2))
+ ]
+ # fix for ActiveStorage
+ unless Rails.application.config.use_paperclip
+ images.each_with_index do |image, index|
+ image.attachment.attach(io: file, filename: "thinking-cat-#{index + 1}.jpg", content_type: 'image/jpeg')
+ image.save!
+ file.rewind # we need to do this to avoid `ActiveStorage::IntegrityError`
+ end
+ end
+ end
+
+ it 'only looks for variant images' do
+ expect(product.images.size).to eq(2)
+ end
+
+ it 'is sorted by position' do
+ expect(product.images.pluck(:alt)).to eq(['position 1', 'position 2'])
+ end
+ end
+
+ # Regression tests for #2352
+ context 'classifications and taxons' do
+ it 'is joined through classifications' do
+ reflection = Spree::Product.reflect_on_association(:taxons)
+ expect(reflection.options[:through]).to eq(:classifications)
+ end
+
+ it 'will delete all classifications' do
+ reflection = Spree::Product.reflect_on_association(:classifications)
+ expect(reflection.options[:dependent]).to eq(:delete_all)
+ end
+ end
+
+ context '#total_on_hand' do
+ let(:product) { create(:product) }
+
+ it 'is infinite if track_inventory_levels is false' do
+ Spree::Config[:track_inventory_levels] = false
+ expect(build(:product, variants_including_master: [build(:master_variant)]).total_on_hand).to eql(Float::INFINITY)
+ end
+
+ it 'is infinite if variant is on demand' do
+ Spree::Config[:track_inventory_levels] = true
+ expect(build(:product, variants_including_master: [build(:on_demand_master_variant)]).total_on_hand).to eql(Float::INFINITY)
+ end
+
+ it 'returns sum of stock items count_on_hand' do
+ product.stock_items.first.set_count_on_hand 5
+ product.variants_including_master.reload # force load association
+ expect(product.total_on_hand).to be(5)
+ end
+
+ it 'returns sum of stock items count_on_hand when variants_including_master is not loaded' do
+ product.stock_items.first.set_count_on_hand 5
+ expect(product.reload.total_on_hand).to be(5)
+ end
+ end
+
+ # Regression spec for https://github.com/spree/spree/issues/5588
+ context '#validate_master when duplicate SKUs entered' do
+ subject { second_product }
+
+ let!(:first_product) { create(:product, sku: 'a-sku') }
+ let(:second_product) { build(:product, sku: 'a-sku') }
+
+ it { is_expected.to be_invalid }
+ end
+
+ it 'initializes a master variant when building a product' do
+ product = Spree::Product.new
+ expect(product.master.is_master).to be true
+ end
+
+ context '#discontinue!' do
+ let(:product) { create(:product, sku: 'a-sku') }
+
+ it 'sets the discontinued' do
+ product.discontinue!
+ product.reload
+ expect(product.discontinued?).to be(true)
+ end
+
+ it 'changes updated_at' do
+ Timecop.scale(1000) do
+ expect { product.discontinue! }.to change(product, :updated_at)
+ end
+ end
+ end
+
+ context '#discontinued?' do
+ let(:product_live) { build(:product, sku: 'a-sku') }
+ let(:product_discontinued) { build(:product, sku: 'a-sku', discontinue_on: Time.now - 1.day) }
+
+ it 'is false' do
+ expect(product_live.discontinued?).to be(false)
+ end
+
+ it 'is true' do
+ expect(product_discontinued.discontinued?).to be(true)
+ end
+ end
+
+ context 'acts_as_taggable' do
+ let(:product) { create(:product) }
+
+ it 'adds tags' do
+ product.tag_list.add('awesome')
+ expect(product.tag_list).to include('awesome')
+ end
+
+ it 'removes tags' do
+ product.tag_list.remove('awesome')
+ expect(product.tag_list).not_to include('awesome')
+ end
+ end
+
+ context '#brand' do
+ let(:taxonomy) { create(:taxonomy, name: I18n.t('spree.taxonomy_brands_name')) }
+ let(:product) { create(:product, taxons: [taxonomy.taxons.first]) }
+
+ it 'fetches Brand Taxon' do
+ expect(product.brand).to eql(taxonomy.taxons.first)
+ end
+ end
+
+ context '#category' do
+ let(:taxonomy) { create(:taxonomy, name: I18n.t('spree.taxonomy_categories_name')) }
+ let(:product) { create(:product, taxons: [taxonomy.taxons.first]) }
+
+ it 'fetches Category Taxon' do
+ expect(product.category).to eql(taxonomy.taxons.first)
+ end
+ end
+
+ context '#backordered?' do
+ let!(:product) { create(:product) }
+
+ it 'returns true when out of stock and backorderable' do
+ expect(product.backordered?).to eq(true)
+ end
+
+ it 'returns false when out of stock and not backorderable' do
+ product.stock_items.first.update(backorderable: false)
+ expect(product.backordered?).to eq(false)
+ end
+
+ it 'returns false when there is available item in stock' do
+ product.stock_items.first.update(count_on_hand: 10)
+ expect(product.backordered?).to eq(false)
+ end
+ end
+
+ describe '#ensure_no_line_items' do
+ let(:product) { create(:product) }
+ let!(:line_item) { create(:line_item, variant: product.master) }
+
+ it 'adds error on product destroy' do
+ expect(product.destroy).to eq false
+ expect(product.errors[:base]).to include I18n.t('activerecord.errors.models.spree/product.attributes.base.cannot_destroy_if_attached_to_line_items')
+ end
+ end
+
+ context '#default_variant' do
+ let(:product) { create(:product) }
+
+ context 'product has variants' do
+ let!(:variant) { create(:variant, product: product) }
+
+ it 'returns first non-master variant' do
+ expect(product.default_variant).to eq(variant)
+ end
+ end
+
+ context 'product without variants' do
+ it 'returns master variant' do
+ expect(product.default_variant).to eq(product.master)
+ end
+ end
+ end
+
+ context '#default_variant_id' do
+ let(:product) { create(:product) }
+
+ context 'product has variants' do
+ let!(:variant) { create(:variant, product: product) }
+
+ it 'returns first non-master variant ID' do
+ expect(product.default_variant_id).to eq(variant.id)
+ end
+ end
+
+ context 'product without variants' do
+ it 'returns master variant ID' do
+ expect(product.default_variant_id).to eq(product.master.id)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/actions/create_adjustment_spec.rb b/core/spec/models/spree/promotion/actions/create_adjustment_spec.rb
new file mode 100644
index 00000000000..1784f757fa0
--- /dev/null
+++ b/core/spec/models/spree/promotion/actions/create_adjustment_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Actions::CreateAdjustment, type: :model do
+ let(:order) { create(:order_with_line_items, line_items_count: 1) }
+ let(:promotion) { create(:promotion) }
+ let(:action) { Spree::Promotion::Actions::CreateAdjustment.new }
+ let(:payload) { { order: order } }
+
+ it_behaves_like 'an adjustment source'
+
+ describe '#compute_amount' do
+ subject { described_class.new }
+
+ let(:shipping_discount) { 10 }
+ let(:order) do
+ double(:order, item_total: 30, ship_total: 10, shipping_discount: shipping_discount)
+ end
+
+ context 'when shipping_discount is applied' do
+ context 'and total is less than discount' do
+ it 'returns discount amount eq to total' do
+ allow(subject).to receive(:compute).with(order).and_return(100)
+
+ expect(subject.compute_amount(order)).to eq(-30)
+ end
+ end
+
+ context 'and total is equal to discount' do
+ it 'returns discount amount' do
+ allow(subject).to receive(:compute).with(order).and_return(30)
+
+ expect(subject.compute_amount(order)).to eq(-30)
+ end
+ end
+
+ context 'and total is greater than discount' do
+ it 'returns discount amount' do
+ allow(subject).to receive(:compute).with(order).and_return(10)
+
+ expect(subject.compute_amount(order)).to eq(-10)
+ end
+ end
+ end
+
+ context 'when shipping_discount is not applied' do
+ let(:shipping_discount) { 0 }
+
+ context 'and total is less than discount' do
+ it 'returns discount amount eq to total' do
+ allow(subject).to receive(:compute).with(order).and_return(100)
+
+ expect(subject.compute_amount(order)).to eq(-40)
+ end
+ end
+
+ context 'and total is equal to discount' do
+ it 'returns discount amount' do
+ allow(subject).to receive(:compute).with(order).and_return(40)
+
+ expect(subject.compute_amount(order)).to eq(-40)
+ end
+ end
+
+ context 'and total is greater than discount' do
+ it 'returns discount amount' do
+ allow(subject).to receive(:compute).with(order).and_return(10)
+
+ expect(subject.compute_amount(order)).to eq(-10)
+ end
+ end
+ end
+ end
+
+ # From promotion spec:
+ context '#perform' do
+ before do
+ action.calculator = Spree::Calculator::FlatRate.new(preferred_amount: 10)
+ promotion.promotion_actions = [action]
+ allow(action).to receive_messages(promotion: promotion)
+ end
+
+ # Regression test for #3966
+ it 'does not apply an adjustment if the amount is 0' do
+ action.calculator.preferred_amount = 0
+ action.perform(payload)
+ expect(promotion.credits_count).to eq(0)
+ expect(order.adjustments.count).to eq(0)
+ end
+
+ it 'creates a discount with correct negative amount' do
+ order.shipments.create!(cost: 10, stock_location: create(:stock_location))
+
+ action.perform(payload)
+ expect(promotion.credits_count).to eq(1)
+ expect(order.adjustments.count).to eq(1)
+ expect(order.adjustments.first.amount.to_i).to eq(-10)
+ end
+
+ it 'creates a discount accessible through both order_id and adjustable_id' do
+ action.perform(payload)
+ expect(order.adjustments.count).to eq(1)
+ expect(order.all_adjustments.count).to eq(1)
+ end
+
+ it 'does not create a discount when order already has one from this promotion' do
+ order.shipments.create!(cost: 10, stock_location: create(:stock_location))
+
+ action.perform(payload)
+ action.perform(payload)
+ expect(promotion.credits_count).to eq(1)
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/actions/create_item_adjustments_spec.rb b/core/spec/models/spree/promotion/actions/create_item_adjustments_spec.rb
new file mode 100644
index 00000000000..58aca2bd51a
--- /dev/null
+++ b/core/spec/models/spree/promotion/actions/create_item_adjustments_spec.rb
@@ -0,0 +1,149 @@
+require 'spec_helper'
+
+module Spree
+ class Promotion
+ module Actions
+ describe CreateItemAdjustments, type: :model do
+ let(:order) { create(:order) }
+ let(:promotion) { create(:promotion) }
+ let(:action) { CreateItemAdjustments.new }
+ let!(:line_item) { create(:line_item, order: order) }
+ let(:payload) { { order: order, promotion: promotion } }
+
+ before do
+ allow(action).to receive(:promotion).and_return(promotion)
+ promotion.promotion_actions = [action]
+ end
+
+ it_behaves_like 'an adjustment source'
+
+ context '#perform' do
+ # Regression test for #3966
+ context 'when calculator computes 0' do
+ before do
+ allow(action).to receive_messages compute_amount: 0
+ end
+
+ it 'does not create an adjustment when calculator returns 0' do
+ action.perform(payload)
+ expect(action.adjustments).to be_empty
+ end
+ end
+
+ context 'when calculator returns a non-zero value' do
+ before do
+ promotion.promotion_actions = [action]
+ allow(action).to receive_messages compute_amount: 10
+ end
+
+ it 'creates adjustment with item as adjustable' do
+ action.perform(payload)
+ expect(action.adjustments.count).to eq(1)
+ expect(line_item.reload.adjustments).to eq(action.adjustments)
+ end
+
+ it 'creates adjustment with self as source' do
+ action.perform(payload)
+ expect(line_item.reload.adjustments.first.source).to eq action
+ end
+
+ it 'does not perform twice on the same item' do
+ 2.times { action.perform(payload) }
+ expect(action.adjustments.count).to eq(1)
+ end
+
+ context 'with products rules' do
+ let!(:second_line_item) { create(:line_item, order: order) }
+ let(:rule) { double Spree::Promotion::Rules::Product }
+
+ before do
+ allow(promotion).to receive(:eligible_rules) { [rule] }
+ allow(rule).to receive(:actionable?).and_return(true, false)
+ end
+
+ it 'does not create adjustments for line_items not in product rule' do
+ action.perform(payload)
+ expect(action.adjustments.count).to be 1
+ expect(line_item.reload.adjustments).to match_array action.adjustments
+ expect(second_line_item.reload.adjustments).to be_empty
+ end
+ end
+ end
+ end
+
+ context '#compute_amount' do
+ before { promotion.promotion_actions = [action] }
+
+ context 'when the adjustable is actionable' do
+ it 'calls compute on the calculator' do
+ allow(action.calculator).to receive(:compute).and_return(10)
+ expect(action.calculator).to receive(:compute).with(line_item)
+ action.compute_amount(line_item)
+ end
+
+ context 'calculator returns amount greater than item total' do
+ before do
+ expect(action.calculator).to receive(:compute).with(line_item).and_return(300)
+ allow(line_item).to receive_messages(amount: 100)
+ end
+
+ it 'does not exceed it' do
+ expect(action.compute_amount(line_item)).to be(-100)
+ end
+ end
+ end
+
+ context 'when the adjustable is not actionable' do
+ before { allow(promotion).to receive(:line_item_actionable?).and_return(false) }
+
+ it 'returns 0' do
+ expect(action.compute_amount(line_item)).to be(0)
+ end
+ end
+ end
+
+ context '#destroy' do
+ let!(:action) { CreateItemAdjustments.create! }
+ let(:other_action) { CreateItemAdjustments.create! }
+
+ before { promotion.promotion_actions = [other_action] }
+
+ it 'destroys adjustments for incompleted orders' do
+ order = Order.create
+ action.adjustments.create!(label: 'Check',
+ amount: 0,
+ order: order,
+ adjustable: line_item)
+
+ expect do
+ action.destroy
+ end.to change(Adjustment, :count).by(-1)
+ end
+
+ it 'nullifies adjustments for completed orders' do
+ order = Order.create(completed_at: Time.current)
+ adjustment = action.adjustments.create!(label: 'Check',
+ amount: 0,
+ order: order,
+ adjustable: line_item)
+
+ expect do
+ action.destroy
+ end.to change { adjustment.reload.source_id }.from(action.id).to nil
+ end
+
+ it 'doesnt mess with unrelated adjustments' do
+ other_action.adjustments.create!(label: 'Check',
+ amount: 0,
+ order: order,
+ adjustable: line_item)
+
+ expect do
+ action.destroy
+ end.not_to change { other_action.adjustments.count }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/actions/create_line_items_spec.rb b/core/spec/models/spree/promotion/actions/create_line_items_spec.rb
new file mode 100644
index 00000000000..1a8b98e7588
--- /dev/null
+++ b/core/spec/models/spree/promotion/actions/create_line_items_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Actions::CreateLineItems, type: :model do
+ let(:order) { create(:order) }
+ let(:action) { Spree::Promotion::Actions::CreateLineItems.create }
+ let(:promotion) { stub_model(Spree::Promotion) }
+ let(:shirt) { create(:variant) }
+ let(:mug) { create(:variant) }
+ let(:payload) { { order: order } }
+
+ def empty_stock(variant)
+ variant.stock_items.update_all(backorderable: false)
+ variant.stock_items.each(&:reduce_count_on_hand_to_zero)
+ end
+
+ context '#perform' do
+ before do
+ allow(action).to receive_messages promotion: promotion
+ action.promotion_action_line_items.create!(
+ variant: mug,
+ quantity: 1
+ )
+ action.promotion_action_line_items.create!(
+ variant: shirt,
+ quantity: 2
+ )
+ end
+
+ context 'order is eligible' do
+ before do
+ allow(promotion).to receive_messages eligible: true
+ end
+
+ it 'adds line items to order with correct variant and quantity' do
+ action.perform(payload)
+ expect(order.line_items.count).to eq(2)
+ line_item = order.line_items.find_by(variant_id: mug.id)
+ expect(line_item).not_to be_nil
+ expect(line_item.quantity).to eq(1)
+ end
+
+ it 'only adds the delta of quantity to an order' do
+ Spree::Cart::AddItem.call(order: order, variant: shirt)
+ action.perform(payload)
+ line_item = order.line_items.find_by(variant_id: shirt.id)
+ expect(line_item).not_to be_nil
+ expect(line_item.quantity).to eq(2)
+ end
+
+ it "doesn't add if the quantity is greater" do
+ Spree::Cart::AddItem.call(order: order, variant: shirt, quantity: 3)
+ action.perform(payload)
+ line_item = order.line_items.find_by(variant_id: shirt.id)
+ expect(line_item).not_to be_nil
+ expect(line_item.quantity).to eq(3)
+ end
+
+ it "doesn't try to add an item if it's out of stock" do
+ empty_stock(mug)
+ empty_stock(shirt)
+
+ expect(order.contents).not_to receive(:add)
+ action.perform(order: order)
+ end
+ end
+ end
+
+ describe '#item_available?' do
+ let(:item_out_of_stock) do
+ action.promotion_action_line_items.create!(variant: mug, quantity: 1)
+ end
+
+ let(:item_in_stock) do
+ action.promotion_action_line_items.create!(variant: shirt, quantity: 1)
+ end
+
+ it 'returns false if the item is out of stock' do
+ empty_stock(mug)
+ expect(action.item_available?(item_out_of_stock)).to be false
+ end
+
+ it 'returns true if the item is in stock' do
+ expect(action.item_available?(item_in_stock)).to be true
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/actions/free_shipping_spec.rb b/core/spec/models/spree/promotion/actions/free_shipping_spec.rb
new file mode 100644
index 00000000000..f86c9ece3ee
--- /dev/null
+++ b/core/spec/models/spree/promotion/actions/free_shipping_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Actions::FreeShipping, type: :model do
+ let(:order) { create(:completed_order_with_totals) }
+ let(:promotion) { create(:promotion) }
+ let(:action) { Spree::Promotion::Actions::FreeShipping.create }
+ let(:payload) { { order: order } }
+
+ it_behaves_like 'an adjustment source'
+
+ # From promotion spec:
+ context '#perform' do
+ before do
+ order.shipments << create(:shipment)
+ promotion.promotion_actions << action
+ end
+
+ it 'creates a discount with correct negative amount' do
+ expect(order.shipments.count).to eq(2)
+ expect(order.shipments.first.cost).to eq(100)
+ expect(order.shipments.last.cost).to eq(100)
+ expect(action.perform(payload)).to be true
+ expect(promotion.credits_count).to eq(2)
+ expect(order.shipment_adjustments.count).to eq(2)
+ expect(order.shipment_adjustments.first.amount.to_i).to eq(-100)
+ expect(order.shipment_adjustments.last.amount.to_i).to eq(-100)
+ end
+
+ it 'does not create a discount when order already has one from this promotion' do
+ expect(action.perform(payload)).to be true
+ expect(action.perform(payload)).to be false
+ expect(promotion.credits_count).to eq(2)
+ expect(order.shipment_adjustments.count).to eq(2)
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/rules/country_spec.rb b/core/spec/models/spree/promotion/rules/country_spec.rb
new file mode 100644
index 00000000000..0996976f98e
--- /dev/null
+++ b/core/spec/models/spree/promotion/rules/country_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Rules::Country, type: :model do
+ let(:rule) { Spree::Promotion::Rules::Country.new }
+ let(:order) { create(:order) }
+ let(:country) { create(:country) }
+ let(:other_country) { create(:country) }
+
+ before { allow(Spree::Country).to receive(:default) { other_country.id } }
+
+ context 'preferred country is set' do
+ before { rule.preferred_country_id = country.id }
+
+ it 'is eligible for correct country' do
+ allow(order).to receive_message_chain(:ship_address, :country_id) { country.id }
+ expect(rule).to be_eligible(order)
+ end
+
+ it 'is not eligible for incorrect country' do
+ allow(order).to receive_message_chain(:ship_address, :country_id) { other_country.id }
+ expect(rule).not_to be_eligible(order)
+ end
+ end
+
+ context 'preferred country is not set' do
+ it 'is eligible for default country' do
+ allow(order).to receive_message_chain(:ship_address, :country_id) { other_country.id }
+ expect(rule).to be_eligible(order)
+ end
+
+ it 'is not eligible for incorrect country' do
+ allow(order).to receive_message_chain(:ship_address, :country_id) { country.id }
+ expect(rule).not_to be_eligible(order)
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/rules/first_order_spec.rb b/core/spec/models/spree/promotion/rules/first_order_spec.rb
new file mode 100644
index 00000000000..bd17b72b3a5
--- /dev/null
+++ b/core/spec/models/spree/promotion/rules/first_order_spec.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Rules::FirstOrder, type: :model do
+ let(:rule) { Spree::Promotion::Rules::FirstOrder.new }
+ let(:order) { mock_model(Spree::Order, user: nil, email: nil) }
+ let(:user) { mock_model(Spree::LegacyUser) }
+
+ context 'without a user or email' do
+ it { expect(rule).not_to be_eligible(order) }
+ it 'sets an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'You need to login or provide your email before applying this coupon code.'
+ end
+ end
+
+ context 'first order' do
+ context 'for a signed user' do
+ context 'with no completed orders' do
+ before do
+ allow(user).to receive_message_chain(:orders, complete: [])
+ end
+
+ specify do
+ allow(order).to receive_messages(user: user)
+ expect(rule).to be_eligible(order)
+ end
+
+ it 'is eligible when user passed in payload data' do
+ expect(rule).to be_eligible(order, user: user)
+ end
+ end
+
+ context 'with completed orders' do
+ before do
+ allow(order).to receive_messages(user: user)
+ end
+
+ it 'is eligible when checked against first completed order' do
+ allow(user).to receive_message_chain(:orders, complete: [order])
+ expect(rule).to be_eligible(order)
+ end
+
+ context 'with another order' do
+ before { allow(user).to receive_message_chain(:orders, complete: [mock_model(Spree::Order)]) }
+
+ it { expect(rule).not_to be_eligible(order) }
+ it 'sets an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'This coupon code can only be applied to your first order.'
+ end
+ end
+ end
+ end
+
+ context 'for a guest user' do
+ let(:email) { 'user@spreecommerce.org' }
+
+ before { allow(order).to receive_messages email: 'user@spreecommerce.org' }
+
+ context 'with no other orders' do
+ it { expect(rule).to be_eligible(order) }
+ end
+
+ context 'with another order' do
+ before { allow(rule).to receive_messages(orders_by_email: [mock_model(Spree::Order)]) }
+
+ it { expect(rule).not_to be_eligible(order) }
+ it 'sets an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'This coupon code can only be applied to your first order.'
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/rules/item_total_spec.rb b/core/spec/models/spree/promotion/rules/item_total_spec.rb
new file mode 100644
index 00000000000..4e8e2ed2c2f
--- /dev/null
+++ b/core/spec/models/spree/promotion/rules/item_total_spec.rb
@@ -0,0 +1,275 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Rules::ItemTotal, type: :model do
+ let(:rule) { Spree::Promotion::Rules::ItemTotal.new }
+ let(:order) { double(:order) }
+
+ before do
+ rule.preferred_amount_min = 50
+ rule.preferred_amount_max = 60
+ end
+
+ context 'preferred operator_min set to gt and preferred operator_max set to lt' do
+ before do
+ rule.preferred_operator_min = 'gt'
+ rule.preferred_operator_max = 'lt'
+ end
+
+ context 'and item total is lower than prefered maximum amount' do
+ context 'and item total is higher than prefered minimum amount' do
+ it 'is eligible' do
+ allow(order).to receive_messages item_total: 51
+ expect(rule).to be_eligible(order)
+ end
+ end
+
+ context 'and item total is equal to the prefered minimum amount' do
+ before { allow(order).to receive_messages item_total: 50 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders less than or equal to $50.00."
+ end
+ end
+
+ context 'and item total is lower to the prefered minimum amount' do
+ before { allow(order).to receive_messages item_total: 49 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders less than or equal to $50.00."
+ end
+ end
+ end
+
+ context 'and item total is equal to the prefered maximum amount' do
+ before { allow(order).to receive_messages item_total: 60 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders higher than $60.00."
+ end
+ end
+
+ context 'and item total is higher than the prefered maximum amount' do
+ before { allow(order).to receive_messages item_total: 61 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders higher than $60.00."
+ end
+ end
+ end
+
+ context 'preferred operator set to gt and preferred operator_max set to lte' do
+ before do
+ rule.preferred_operator_min = 'gt'
+ rule.preferred_operator_max = 'lte'
+ end
+
+ context 'and item total is lower than prefered maximum amount' do
+ context 'and item total is higher than prefered minimum amount' do
+ it 'is eligible' do
+ allow(order).to receive_messages item_total: 51
+ expect(rule).to be_eligible(order)
+ end
+ end
+
+ context 'and item total is equal to the prefered minimum amount' do
+ before { allow(order).to receive_messages item_total: 50 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders less than or equal to $50.00."
+ end
+ end
+
+ context 'and item total is lower to the prefered minimum amount' do
+ before { allow(order).to receive_messages item_total: 49 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders less than or equal to $50.00."
+ end
+ end
+ end
+
+ context 'and item total is equal to the prefered maximum amount' do
+ before { allow(order).to receive_messages item_total: 60 }
+
+ it 'is not eligible' do
+ expect(rule).to be_eligible(order)
+ end
+ end
+
+ context 'and item total is higher than the prefered maximum amount' do
+ before { allow(order).to receive_messages item_total: 61 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders higher than $60.00."
+ end
+ end
+ end
+
+ context 'preferred operator set to gte and preferred operator_max set to lt' do
+ before do
+ rule.preferred_operator_min = 'gte'
+ rule.preferred_operator_max = 'lt'
+ end
+
+ context 'and item total is lower than prefered maximum amount' do
+ context 'and item total is higher than prefered minimum amount' do
+ it 'is eligible' do
+ allow(order).to receive_messages item_total: 51
+ expect(rule).to be_eligible(order)
+ end
+ end
+
+ context 'and item total is equal to the prefered minimum amount' do
+ before { allow(order).to receive_messages item_total: 50 }
+
+ it 'is not eligible' do
+ expect(rule).to be_eligible(order)
+ end
+ end
+
+ context 'and item total is lower to the prefered minimum amount' do
+ before { allow(order).to receive_messages item_total: 49 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders less than $50.00."
+ end
+ end
+ end
+
+ context 'and item total is equal to the prefered maximum amount' do
+ before { allow(order).to receive_messages item_total: 60 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders higher than $60.00."
+ end
+ end
+
+ context 'and item total is higher than the prefered maximum amount' do
+ before { allow(order).to receive_messages item_total: 61 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders higher than $60.00."
+ end
+ end
+ end
+
+ context 'preferred operator set to gte and preferred operator_max set to lte' do
+ before do
+ rule.preferred_operator_min = 'gte'
+ rule.preferred_operator_max = 'lte'
+ end
+
+ context 'and item total is lower than prefered maximum amount' do
+ context 'and item total is higher than prefered minimum amount' do
+ it 'is eligible' do
+ allow(order).to receive_messages item_total: 51
+ expect(rule).to be_eligible(order)
+ end
+ end
+
+ context 'and item total is equal to the prefered minimum amount' do
+ before { allow(order).to receive_messages item_total: 50 }
+
+ it 'is not eligible' do
+ expect(rule).to be_eligible(order)
+ end
+ end
+
+ context 'and item total is lower to the prefered minimum amount' do
+ before { allow(order).to receive_messages item_total: 49 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders less than $50.00."
+ end
+ end
+ end
+
+ context 'and item total is equal to the prefered maximum amount' do
+ before { allow(order).to receive_messages item_total: 60 }
+
+ it 'is not eligible' do
+ expect(rule).to be_eligible(order)
+ end
+ end
+
+ context 'and item total is higher than the prefered maximum amount' do
+ before { allow(order).to receive_messages item_total: 61 }
+
+ it 'is not eligible' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'set an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied to orders higher than $60.00."
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/rules/one_use_per_user_spec.rb b/core/spec/models/spree/promotion/rules/one_use_per_user_spec.rb
new file mode 100644
index 00000000000..97e7259bf04
--- /dev/null
+++ b/core/spec/models/spree/promotion/rules/one_use_per_user_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Rules::OneUsePerUser, type: :model do
+ let(:rule) { described_class.new }
+
+ describe '#eligible?(order)' do
+ subject { rule.eligible?(order) }
+
+ let(:order) { double Spree::Order, user: user }
+ let(:user) { double Spree::LegacyUser }
+ let(:promotion) { stub_model Spree::Promotion, used_by?: used_by }
+ let(:used_by) { false }
+
+ before { rule.promotion = promotion }
+
+ context 'when the order is assigned to a user' do
+ context 'when the user has used this promotion before' do
+ let(:used_by) { true }
+
+ it { is_expected.to be false }
+ it 'sets an error message' do
+ subject
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'This coupon code can only be used once per user.'
+ end
+ end
+
+ context 'when the user has not used this promotion before' do
+ it { is_expected.to be true }
+ end
+ end
+
+ context 'when the order is not assigned to a user' do
+ let(:user) { nil }
+
+ it { is_expected.to be false }
+ it 'sets an error message' do
+ subject
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'You need to login before applying this coupon code.'
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/rules/option_value_spec.rb b/core/spec/models/spree/promotion/rules/option_value_spec.rb
new file mode 100644
index 00000000000..c5480b5e4dd
--- /dev/null
+++ b/core/spec/models/spree/promotion/rules/option_value_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Rules::OptionValue do
+ let(:rule) { described_class.new }
+
+ describe '#preferred_eligible_values' do
+ subject { rule.preferred_eligible_values }
+
+ it 'assigns a nicely formatted hash' do
+ rule.preferred_eligible_values = Hash['5' => '1,2', '6' => '1']
+ expect(subject).to eq Hash[5 => [1, 2], 6 => [1]]
+ end
+ end
+
+ describe '#applicable?' do
+ subject { rule.applicable?(promotable) }
+
+ context 'when promotable is an order' do
+ let(:promotable) { Spree::Order.new }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when promotable is not an order' do
+ let(:promotable) { Spree::LineItem.new }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '#eligible?' do
+ subject { rule.eligible?(promotable) }
+
+ let(:variant) { create :variant }
+ let(:line_item) { create :line_item, variant: variant }
+ let(:promotable) { line_item.order }
+
+ context 'when there are any applicable line items' do
+ before do
+ rule.preferred_eligible_values = Hash[variant.product.id => [
+ variant.option_values.pluck(:id).first
+ ]]
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when there are no applicable line items' do
+ before do
+ rule.preferred_eligible_values = Hash[99 => [99]]
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '#actionable?' do
+ subject { rule.actionable?(line_item) }
+
+ let(:line_item) { create :line_item }
+ let(:option_value_blue) do
+ create(
+ :option_value,
+ name: 'Blue',
+ presentation: 'Blue',
+ option_type: create(
+ :option_type,
+ name: 'foo-colour',
+ presentation: 'Colour'
+ )
+ )
+ end
+ let(:option_value_medium) do
+ create(
+ :option_value,
+ name: 'Medium',
+ presentation: 'M'
+ )
+ end
+
+ before do
+ line_item.variant.option_values << option_value_blue
+ rule.preferred_eligible_values = Hash[product_id => option_value_ids]
+ end
+
+ context 'when the line item has the correct product' do
+ let(:product_id) { line_item.product.id }
+
+ context 'when all of the option values match' do
+ let(:option_value_ids) { [option_value_blue.id] }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when not all of the option values match' do
+ let(:option_value_ids) { [option_value_blue.id, option_value_medium.id] }
+
+ it { is_expected.to be true }
+ end
+ end
+
+ context "when the line item's product doesn't match" do
+ let(:product_id) { 99 }
+ let(:option_value_ids) { [99] }
+
+ it { is_expected.to be false }
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/rules/product_spec.rb b/core/spec/models/spree/promotion/rules/product_spec.rb
new file mode 100644
index 00000000000..0dbf6651eea
--- /dev/null
+++ b/core/spec/models/spree/promotion/rules/product_spec.rb
@@ -0,0 +1,152 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Rules::Product, type: :model do
+ let(:rule) { Spree::Promotion::Rules::Product.new(rule_options) }
+ let(:rule_options) { {} }
+
+ context '#eligible?(order)' do
+ let(:order) { Spree::Order.new }
+
+ before do
+ 3.times { |i| instance_variable_set("@product#{i}", mock_model(Spree::Product)) }
+ end
+
+ it 'is eligible if there are no products' do
+ allow(rule).to receive_messages(eligible_products: [])
+ expect(rule).to be_eligible(order)
+ end
+
+ context "with 'any' match policy" do
+ let(:rule_options) { super().merge(preferred_match_policy: 'any') }
+
+ it 'is eligible if any of the products is in eligible products' do
+ allow(order).to receive_messages(products: [@product1, @product2])
+ allow(rule).to receive_messages(eligible_products: [@product2, @product3])
+ expect(rule).to be_eligible(order)
+ end
+
+ context 'when none of the products are eligible products' do
+ before do
+ allow(order).to receive_messages(products: [@product1])
+ allow(rule).to receive_messages(eligible_products: [@product2, @product3])
+ end
+
+ it { expect(rule).not_to be_eligible(order) }
+ it 'sets an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'You need to add an applicable product before applying this coupon code.'
+ end
+ end
+ end
+
+ context "with 'all' match policy" do
+ let(:rule_options) { super().merge(preferred_match_policy: 'all') }
+
+ it 'is eligible if all of the eligible products are ordered' do
+ allow(order).to receive_messages(products: [@product3, @product2, @product1])
+ allow(rule).to receive_messages(eligible_products: [@product2, @product3])
+ expect(rule).to be_eligible(order)
+ end
+
+ context 'when any of the eligible products is not ordered' do
+ before do
+ allow(order).to receive_messages(products: [@product1, @product2])
+ allow(rule).to receive_messages(eligible_products: [@product1, @product2, @product3])
+ end
+
+ it { expect(rule).not_to be_eligible(order) }
+ it 'sets an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq "This coupon code can't be applied because you don't have all of the necessary products in your cart."
+ end
+ end
+ end
+
+ context "with 'none' match policy" do
+ let(:rule_options) { super().merge(preferred_match_policy: 'none') }
+
+ it "is eligible if none of the order's products are in eligible products" do
+ allow(order).to receive_messages(products: [@product1])
+ allow(rule).to receive_messages(eligible_products: [@product2, @product3])
+ expect(rule).to be_eligible(order)
+ end
+
+ context "when any of the order's products are in eligible products" do
+ before do
+ allow(order).to receive_messages(products: [@product1, @product2])
+ allow(rule).to receive_messages(eligible_products: [@product2, @product3])
+ end
+
+ it { expect(rule).not_to be_eligible(order) }
+ it 'sets an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'Your cart contains a product that prevents this coupon code from being applied.'
+ end
+ end
+ end
+ end
+
+ describe '#actionable?' do
+ subject do
+ rule.actionable?(line_item)
+ end
+
+ let(:rule_line_item) { Spree::LineItem.new(product: rule_product) }
+ let(:other_line_item) { Spree::LineItem.new(product: other_product) }
+
+ let(:rule_options) { super().merge(products: [rule_product]) }
+ let(:rule_product) { mock_model(Spree::Product) }
+ let(:other_product) { mock_model(Spree::Product) }
+
+ context "with 'any' match policy" do
+ let(:rule_options) { super().merge(preferred_match_policy: 'any') }
+
+ context 'for product in rule' do
+ let(:line_item) { rule_line_item }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for product not in rule' do
+ let(:line_item) { other_line_item }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context "with 'all' match policy" do
+ let(:rule_options) { super().merge(preferred_match_policy: 'all') }
+
+ context 'for product in rule' do
+ let(:line_item) { rule_line_item }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for product not in rule' do
+ let(:line_item) { other_line_item }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context "with 'none' match policy" do
+ let(:rule_options) { super().merge(preferred_match_policy: 'none') }
+
+ context 'for product in rule' do
+ let(:line_item) { rule_line_item }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for product not in rule' do
+ let(:line_item) { other_line_item }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/rules/taxon_spec.rb b/core/spec/models/spree/promotion/rules/taxon_spec.rb
new file mode 100644
index 00000000000..e8a0b178713
--- /dev/null
+++ b/core/spec/models/spree/promotion/rules/taxon_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Rules::Taxon, type: :model do
+ let(:rule) { subject }
+
+ context '#elegible?(order)' do
+ let(:taxon) { create :taxon, name: 'first' }
+ let(:taxon2) { create :taxon, name: 'second' }
+ let(:order) { create :order_with_line_items }
+
+ before do
+ rule.save
+ end
+
+ context 'with any match policy' do
+ before do
+ rule.preferred_match_policy = 'any'
+ end
+
+ it 'is eligible if order does has any prefered taxon' do
+ order.products.first.taxons << taxon
+ rule.taxons << taxon
+ expect(rule).to be_eligible(order)
+ end
+
+ context 'when order contains items from different taxons' do
+ before do
+ order.products.first.taxons << taxon
+ rule.taxons << taxon
+ end
+
+ it 'acts on a product within the eligible taxon' do
+ expect(rule).to be_actionable(order.line_items.last)
+ end
+
+ it 'does not act on a product in another taxon' do
+ order.line_items << create(:line_item, product: create(:product, taxons: [taxon2]))
+ expect(rule).not_to be_actionable(order.line_items.last)
+ end
+ end
+
+ context 'when order does not have any prefered taxon' do
+ before { rule.taxons << taxon2 }
+
+ it { expect(rule).not_to be_eligible(order) }
+ it 'sets an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'You need to add a product from an applicable category before applying this coupon code.'
+ end
+ end
+
+ context 'when a product has a taxon child of a taxon rule' do
+ before do
+ taxon.children << taxon2
+ order.products.first.taxons << taxon2
+ rule.taxons << taxon2
+ end
+
+ it { expect(rule).to be_eligible(order) }
+ end
+ end
+
+ context 'with all match policy' do
+ before do
+ rule.preferred_match_policy = 'all'
+ end
+
+ it 'is eligible order has all prefered taxons' do
+ order.products.first.taxons << taxon2
+ order.products.last.taxons << taxon
+
+ rule.taxons = [taxon, taxon2]
+
+ expect(rule).to be_eligible(order)
+ end
+
+ context 'when order does not have all prefered taxons' do
+ before { rule.taxons << taxon }
+
+ it { expect(rule).not_to be_eligible(order) }
+ it 'sets an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'You need to add a product from all applicable categories before applying this coupon code.'
+ end
+ end
+
+ context 'when a product has a taxon child of a taxon rule' do
+ let(:taxon3) { create :taxon }
+
+ before do
+ taxon.children << taxon2
+ order.products.first.taxons << taxon2
+ order.products.last.taxons << taxon3
+ rule.taxons << taxon2
+ rule.taxons << taxon3
+ end
+
+ it { expect(rule).to be_eligible(order) }
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/rules/user_logged_in_spec.rb b/core/spec/models/spree/promotion/rules/user_logged_in_spec.rb
new file mode 100644
index 00000000000..4985ec3cd2b
--- /dev/null
+++ b/core/spec/models/spree/promotion/rules/user_logged_in_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Rules::UserLoggedIn, type: :model do
+ let(:rule) { Spree::Promotion::Rules::UserLoggedIn.new }
+
+ context '#eligible?(order)' do
+ let(:order) { Spree::Order.new }
+
+ it 'is eligible if order has an associated user' do
+ user = double('User')
+ allow(order).to receive_messages(user: user)
+
+ expect(rule).to be_eligible(order)
+ end
+
+ context 'when user is not logged in' do
+ before { allow(order).to receive_messages(user: nil) } # better to be explicit here
+
+ it { expect(rule).not_to be_eligible(order) }
+ it 'sets an error message' do
+ rule.eligible?(order)
+ expect(rule.eligibility_errors.full_messages.first).
+ to eq 'You need to login before applying this coupon code.'
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion/rules/user_spec.rb b/core/spec/models/spree/promotion/rules/user_spec.rb
new file mode 100644
index 00000000000..bf78c27e1eb
--- /dev/null
+++ b/core/spec/models/spree/promotion/rules/user_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Spree::Promotion::Rules::User, type: :model do
+ let(:rule) { Spree::Promotion::Rules::User.new }
+ let(:test_user) { create :user }
+
+ context '#eligible?(order)' do
+ let(:order) { Spree::Order.new }
+
+ it 'is not eligible if users are not provided' do
+ expect(rule).not_to be_eligible(order)
+ end
+
+ it 'is eligible if users include user placing the order' do
+ user = mock_model(Spree::LegacyUser)
+ users = [user, mock_model(Spree::LegacyUser)]
+ allow(rule).to receive_messages(users: users)
+ allow(order).to receive_messages(user: user)
+
+ expect(rule).to be_eligible(order)
+ end
+
+ it 'is not eligible if user placing the order is not listed' do
+ allow(order).to receive_messages(user: mock_model(Spree::LegacyUser))
+ users = [mock_model(Spree::LegacyUser), mock_model(Spree::LegacyUser)]
+ allow(rule).to receive_messages(users: users)
+
+ expect(rule).not_to be_eligible(order)
+ end
+
+ # Regression test for #3885
+ it 'can assign to user_ids' do
+ user1 = Spree::LegacyUser.create!(email: 'test1@example.com')
+ user2 = Spree::LegacyUser.create!(email: 'test2@example.com')
+ expect { rule.user_ids = "#{user1.id}, #{user2.id}" }.not_to raise_error
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion_action_spec.rb b/core/spec/models/spree/promotion_action_spec.rb
new file mode 100644
index 00000000000..32a89ff3e46
--- /dev/null
+++ b/core/spec/models/spree/promotion_action_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+
+describe Spree::PromotionAction, type: :model do
+ it "forces developer to implement 'perform' method" do
+ expect { MyAction.new.perform }.to raise_error(NameError)
+ end
+end
diff --git a/core/spec/models/spree/promotion_category_spec.rb b/core/spec/models/spree/promotion_category_spec.rb
new file mode 100644
index 00000000000..7796d5eda37
--- /dev/null
+++ b/core/spec/models/spree/promotion_category_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Spree::PromotionCategory, type: :model do
+ describe 'validation' do
+ subject { Spree::PromotionCategory.new name: name }
+
+ let(:name) { 'Nom' }
+
+ context 'when all required attributes are specified' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'when name is missing' do
+ let(:name) { nil }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion_handler/cart_spec.rb b/core/spec/models/spree/promotion_handler/cart_spec.rb
new file mode 100644
index 00000000000..60c254b26d2
--- /dev/null
+++ b/core/spec/models/spree/promotion_handler/cart_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+module Spree
+ module PromotionHandler
+ describe Cart, type: :model do
+ subject { Cart.new(order, line_item) }
+
+ let(:line_item) { create(:line_item) }
+ let(:order) { line_item.order }
+
+ let(:promotion) { Promotion.create(name: 'At line items') }
+ let(:calculator) { Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) }
+
+ context 'activates in LineItem level' do
+ let!(:action) { Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) }
+ let(:adjustable) { line_item }
+
+ shared_context 'creates the adjustment' do
+ it 'creates the adjustment' do
+ expect do
+ subject.activate
+ end.to change { adjustable.adjustments.count }.by(1)
+ end
+ end
+
+ context 'promotion with no rules' do
+ include_context 'creates the adjustment'
+ end
+
+ context 'promotion includes item involved' do
+ let!(:rule) { Promotion::Rules::Product.create(products: [line_item.product], promotion: promotion) }
+
+ include_context 'creates the adjustment'
+ end
+
+ context 'promotion has item total rule' do
+ let(:shirt) { create(:product) }
+ let!(:rule) { Promotion::Rules::ItemTotal.create(preferred_operator_min: 'gt', preferred_amount_min: 50, preferred_operator_max: 'lt', preferred_amount_max: 150, promotion: promotion) }
+
+ before do
+ # Makes the order eligible for this promotion
+ order.item_total = 100
+ order.save
+ end
+
+ include_context 'creates the adjustment'
+ end
+ end
+
+ context 'activates in Order level' do
+ let!(:action) { Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) }
+ let(:adjustable) { order }
+
+ shared_context 'creates the adjustment' do
+ it 'creates the adjustment' do
+ expect do
+ subject.activate
+ end.to change { adjustable.adjustments.count }.by(1)
+ end
+ end
+
+ context 'promotion with no rules' do
+ before do
+ # Gives the calculator something to discount
+ order.item_total = 10
+ order.save
+ end
+
+ include_context 'creates the adjustment'
+ end
+
+ context 'promotion has item total rule' do
+ let(:shirt) { create(:product) }
+ let!(:rule) { Promotion::Rules::ItemTotal.create(preferred_operator_min: 'gt', preferred_amount_min: 50, preferred_operator_max: 'lt', preferred_amount_max: 150, promotion: promotion) }
+
+ before do
+ # Makes the order eligible for this promotion
+ order.item_total = 100
+ order.save
+ end
+
+ include_context 'creates the adjustment'
+ end
+ end
+
+ context 'activates promotions associated with the order' do
+ let(:promo) { create :promotion_with_item_adjustment, adjustment_rate: 5, code: 'promo' }
+ let(:adjustable) { line_item }
+
+ before do
+ order.promotions << promo
+ end
+
+ it 'creates the adjustment' do
+ expect do
+ subject.activate
+ end.to change { adjustable.adjustments.count }.by(1)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion_handler/coupon_spec.rb b/core/spec/models/spree/promotion_handler/coupon_spec.rb
new file mode 100644
index 00000000000..c77a9dd9af9
--- /dev/null
+++ b/core/spec/models/spree/promotion_handler/coupon_spec.rb
@@ -0,0 +1,333 @@
+require 'spec_helper'
+
+module Spree
+ module PromotionHandler
+ describe Coupon, type: :model do
+ subject { Coupon.new(order) }
+
+ let(:order) { double('Order', coupon_code: '10off').as_null_object }
+
+ it 'returns self in apply' do
+ expect(subject.apply).to be_a Coupon
+ end
+
+ context 'status messages' do
+ let(:coupon) { Coupon.new(order) }
+
+ describe '#set_success_code' do
+ subject { coupon.set_success_code status }
+
+ let(:status) { :coupon_code_applied }
+
+ it 'has status_code' do
+ subject
+ expect(coupon.status_code).to eq(status)
+ end
+
+ it 'has success message' do
+ subject
+ expect(coupon.success).to eq(Spree.t(status))
+ end
+ end
+
+ describe '#set_error_code' do
+ subject { coupon.set_error_code status }
+
+ let(:status) { :coupon_code_not_found }
+
+ it 'has status_code' do
+ subject
+ expect(coupon.status_code).to eq(status)
+ end
+
+ it 'has error message' do
+ subject
+ expect(coupon.error).to eq(Spree.t(status))
+ end
+ end
+ end
+
+ context 'coupon code promotion doesnt exist' do
+ before { Promotion.create name: 'promo', code: nil }
+
+ it 'doesnt fetch any promotion' do
+ expect(subject.promotion).to be_blank
+ end
+
+ context 'with no actions defined' do
+ before { Promotion.create name: 'promo', code: '10off' }
+
+ it 'populates error message' do
+ subject.apply
+ expect(subject.error).to eq Spree.t(:coupon_code_not_found)
+ end
+ end
+ end
+
+ context 'existing coupon code promotion' do
+ let!(:promotion) { Promotion.create name: 'promo', code: '10off' }
+ let!(:action) { Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) }
+ let(:calculator) { Calculator::FlatRate.new(preferred_amount: 10) }
+
+ it 'fetches with given code' do
+ expect(subject.promotion).to eq promotion
+ end
+
+ context 'with a per-item adjustment action' do
+ let(:order) { create(:order_with_line_items, line_items_count: 3) }
+
+ context 'right coupon given' do
+ context 'with correct coupon code casing' do
+ before { allow(order).to receive_messages coupon_code: '10off' }
+
+ it 'successfully activates promo' do
+ expect(order.total).to eq(130)
+ subject.apply
+ expect(subject.success).to be_present
+ order.line_items.each do |line_item|
+ expect(line_item.adjustments.count).to eq(1)
+ end
+ # Ensure that applying the adjustment actually affects the order's total!
+ expect(order.reload.total).to eq(100)
+ end
+
+ it 'coupon already applied to the order' do
+ subject.apply
+ expect(subject.success).to be_present
+ subject.apply
+ expect(subject.error).to eq Spree.t(:coupon_code_already_applied)
+ end
+ end
+
+ # Regression test for #4211
+ context 'with incorrect coupon code casing' do
+ before { allow(order).to receive_messages coupon_code: '10OFF' }
+
+ it 'successfully activates promo' do
+ expect(order.total).to eq(130)
+ subject.apply
+ expect(subject.success).to be_present
+ order.line_items.each do |line_item|
+ expect(line_item.adjustments.count).to eq(1)
+ end
+ # Ensure that applying the adjustment actually affects the order's total!
+ expect(order.reload.total).to eq(100)
+ end
+ end
+ end
+
+ context 'coexists with a non coupon code promo' do
+ let!(:order) { Order.create }
+
+ before do
+ allow(order).to receive_messages coupon_code: '10off'
+ calculator = Calculator::FlatRate.new(preferred_amount: 10)
+ general_promo = Promotion.create name: 'General Promo'
+ Promotion::Actions::CreateItemAdjustments.create(promotion: general_promo, calculator: calculator) # general_action
+
+ Spree::Cart::AddItem.call(order: order, variant: create(:variant))
+ end
+
+ # regression spec for #4515
+ it 'successfully activates promo' do
+ subject.apply
+ expect(subject).to be_successful
+ end
+ end
+ end
+
+ context 'with a free-shipping adjustment action' do
+ let!(:action) { Promotion::Actions::FreeShipping.create(promotion: promotion) }
+
+ context 'right coupon code given' do
+ let(:order) { create(:order_with_line_items, line_items_count: 3) }
+
+ before { allow(order).to receive_messages coupon_code: '10off' }
+
+ it 'successfully activates promo' do
+ expect(order.total).to eq(130)
+ subject.apply
+ expect(subject.success).to be_present
+
+ expect(order.shipment_adjustments.count).to eq(1)
+ end
+
+ it 'coupon already applied to the order' do
+ subject.apply
+ expect(subject.success).to be_present
+ subject.apply
+ expect(subject.error).to eq Spree.t(:coupon_code_already_applied)
+ end
+ end
+ end
+
+ context 'with a whole-order adjustment action' do
+ let!(:action) { Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) }
+
+ context 'right coupon given' do
+ let(:order) { create(:order) }
+ let(:calculator) { Calculator::FlatRate.new(preferred_amount: 10) }
+
+ before do
+ allow(order).to receive_messages(coupon_code: '10off',
+ # These need to be here so that promotion adjustment "wins"
+ item_total: 50,
+ ship_total: 10)
+ end
+
+ it 'successfully activates promo' do
+ subject.apply
+ expect(subject.success).to be_present
+ expect(order.adjustments.count).to eq(1)
+ end
+
+ it 'coupon already applied to the order' do
+ subject.apply
+ expect(subject.success).to be_present
+ subject.apply
+ expect(subject.error).to eq Spree.t(:coupon_code_already_applied)
+ end
+
+ it 'coupon fails to activate' do
+ allow_any_instance_of(Spree::Promotion).to receive(:activate).and_return false
+ subject.apply
+ expect(subject.error).to eq Spree.t(:coupon_code_unknown_error)
+ end
+
+ it 'coupon code hit max usage' do
+ promotion.update_column(:usage_limit, 1)
+ coupon = Coupon.new(order)
+ coupon.apply
+ expect(coupon.successful?).to be true
+
+ order_2 = create(:order)
+ allow(order_2).to receive_messages coupon_code: '10off'
+ coupon = Coupon.new(order_2)
+ coupon.apply
+ expect(coupon.successful?).to be false
+ expect(coupon.error).to eq Spree.t(:coupon_code_max_usage)
+ end
+
+ context 'when the a new coupon is less good' do
+ let!(:action_5) { Promotion::Actions::CreateAdjustment.create(promotion: promotion_5, calculator: calculator_5) }
+ let(:calculator_5) { Calculator::FlatRate.new(preferred_amount: 5) }
+ let!(:promotion_5) { Promotion.create name: 'promo', code: '5off' }
+
+ it 'notifies of better deal' do
+ subject.apply
+ allow(order).to receive_messages(coupon_code: '5off')
+ coupon = Coupon.new(order).apply
+ expect(coupon.error).to eq Spree.t(:coupon_code_better_exists)
+ end
+ end
+ end
+ end
+
+ context 'for an order with taxable line items' do
+ before do
+ @country = create(:country)
+ @zone = create(:zone, name: 'Country Zone', default_tax: true, zone_members: [])
+ @zone.zone_members.create(zoneable: @country)
+ @category = Spree::TaxCategory.create name: 'Taxable Foo'
+ @rate1 = Spree::TaxRate.create(
+ amount: 0.10,
+ calculator: Spree::Calculator::DefaultTax.create,
+ tax_category: @category,
+ zone: @zone
+ )
+
+ @order = Spree::Order.create!
+ allow(@order).to receive_messages coupon_code: '10off'
+ end
+
+ context 'and the product price is less than promo discount' do
+ before do
+ 3.times do |_i|
+ taxable = create(:product, tax_category: @category, price: 9.0)
+ Spree::Cart::AddItem.call(order: @order, variant: taxable.master)
+ end
+ end
+
+ it 'successfully applies the promo' do
+ # 3 * (9 + 0.9)
+ expect(@order.total).to eq(29.7)
+ coupon = Coupon.new(@order)
+ coupon.apply
+ expect(coupon.success).to be_present
+ # 3 * ((9 - [9,10].min) + 0)
+ expect(@order.reload.total).to eq(0)
+ expect(@order.additional_tax_total).to eq(0)
+ end
+ end
+
+ context 'and the product price is greater than promo discount' do
+ before do
+ 3.times do |_i|
+ taxable = create(:product, tax_category: @category, price: 11.0)
+ Spree::Cart::AddItem.call(order: @order, variant: taxable.master, quantity: 2)
+ end
+ end
+
+ it 'successfully applies the promo' do
+ # 3 * (22 + 2.2)
+ expect(@order.total.to_f).to eq(72.6)
+ coupon = Coupon.new(@order)
+ coupon.apply
+ expect(coupon.success).to be_present
+ # 3 * ( (22 - 10) + 1.2)
+ expect(@order.reload.total).to eq(39.6)
+ expect(@order.additional_tax_total).to eq(3.6)
+ end
+ end
+
+ context 'and multiple quantity per line item' do
+ before do
+ twnty_off = Promotion.create name: 'promo', code: '20off'
+ twnty_off_calc = Calculator::FlatRate.new(preferred_amount: 20)
+ Promotion::Actions::CreateItemAdjustments.create(promotion: twnty_off,
+ calculator: twnty_off_calc)
+
+ allow(@order).to receive(:coupon_code).and_call_original
+ allow(@order).to receive_messages coupon_code: '20off'
+ 3.times do |_i|
+ taxable = create(:product, tax_category: @category, price: 10.0)
+ Spree::Cart::AddItem.call(order: @order, variant: taxable.master, quantity: 2)
+ end
+ end
+
+ it 'successfully applies the promo' do
+ # 3 * ((2 * 10) + 2.0)
+ expect(@order.total.to_f).to eq(66)
+ coupon = Coupon.new(@order)
+ coupon.apply
+ expect(coupon.success).to be_present
+ # 0
+ expect(@order.reload.total).to eq(0)
+ expect(@order.additional_tax_total).to eq(0)
+ end
+ end
+ end
+
+ context 'with a CreateLineItems action' do
+ let!(:variant) { create(:variant) }
+ let!(:action) { Promotion::Actions::CreateLineItems.create(promotion: promotion) }
+ let(:order) { create(:order) }
+
+ before do
+ action.promotion_action_line_items.create(
+ variant: variant,
+ quantity: 1
+ )
+ allow(order).to receive_messages(coupon_code: '10off')
+ end
+
+ it 'successfully activates promo' do
+ subject.apply
+ expect(subject.success).to be_present
+ expect(order.line_items.pluck(:variant_id)).to include(variant.id)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion_handler/free_shipping_spec.rb b/core/spec/models/spree/promotion_handler/free_shipping_spec.rb
new file mode 100644
index 00000000000..acd5c83bdf7
--- /dev/null
+++ b/core/spec/models/spree/promotion_handler/free_shipping_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+module Spree
+ module PromotionHandler
+ describe FreeShipping, type: :model do
+ subject { Spree::PromotionHandler::FreeShipping.new(order) }
+
+ let(:order) { create(:order) }
+ let(:shipment) { create(:shipment, order: order) }
+
+ let(:promotion) { Promotion.create(name: 'Free Shipping') }
+ let(:calculator) { Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10) }
+ let!(:action) { Promotion::Actions::FreeShipping.create(promotion: promotion) }
+
+ context 'activates in Shipment level' do
+ it 'creates the adjustment' do
+ expect { subject.activate }.to change { shipment.adjustments.count }.by(1)
+ end
+ end
+
+ context 'if promo has a code' do
+ before do
+ promotion.update_column(:code, 'code')
+ end
+
+ it 'does adjust the shipment when applied to order' do
+ order.promotions << promotion
+
+ expect { subject.activate }.to change { shipment.adjustments.count }
+ end
+
+ it 'does not adjust the shipment when not applied to order' do
+ expect { subject.activate }.not_to change { shipment.adjustments.count }
+ end
+ end
+
+ context 'if promo has a path' do
+ before do
+ promotion.update_column(:path, 'path')
+ end
+
+ it 'does not adjust the shipment' do
+ expect { subject.activate }.not_to change { shipment.adjustments.count }
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion_handler/page_spec.rb b/core/spec/models/spree/promotion_handler/page_spec.rb
new file mode 100644
index 00000000000..ccd08ab32c7
--- /dev/null
+++ b/core/spec/models/spree/promotion_handler/page_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+module Spree
+ module PromotionHandler
+ describe Page, type: :model do
+ let(:order) { create(:order_with_line_items, line_items_count: 1) }
+
+ let(:promotion) { Promotion.create(name: '10% off', path: '10off') }
+
+ before do
+ calculator = Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)
+ action = Promotion::Actions::CreateItemAdjustments.create(calculator: calculator)
+ promotion.actions << action
+ end
+
+ it 'activates at the right path' do
+ expect(order.line_item_adjustments.count).to eq(0)
+ Spree::PromotionHandler::Page.new(order, '10off').activate
+ expect(order.line_item_adjustments.count).to eq(1)
+ end
+
+ context 'when promotion is expired' do
+ before do
+ promotion.update_columns(
+ starts_at: 1.week.ago,
+ expires_at: 1.day.ago
+ )
+ end
+
+ it 'is not activated' do
+ expect(order.line_item_adjustments.count).to eq(0)
+ Spree::PromotionHandler::Page.new(order, '10off').activate
+ expect(order.line_item_adjustments.count).to eq(0)
+ end
+ end
+
+ it 'does not activate at the wrong path' do
+ expect(order.line_item_adjustments.count).to eq(0)
+ Spree::PromotionHandler::Page.new(order, 'wrongpath').activate
+ expect(order.line_item_adjustments.count).to eq(0)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion_handler/promotion_duplicator_spec.rb b/core/spec/models/spree/promotion_handler/promotion_duplicator_spec.rb
new file mode 100644
index 00000000000..57908e0fc5b
--- /dev/null
+++ b/core/spec/models/spree/promotion_handler/promotion_duplicator_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe Spree::PromotionHandler::PromotionDuplicator do
+ subject { described_class.new(promotion) }
+
+ let!(:promo_category) { create(:promotion_category) }
+ let!(:calculator) { create(:calculator) }
+
+ let!(:promotion) do
+ create(:promotion_with_item_total_rule,
+ description: 'Test description',
+ expires_at: Date.current + 30,
+ starts_at: Date.current,
+ usage_limit: 100,
+ match_policy: 'all',
+ code: 'test1',
+ advertise: true,
+ path: 'test1',
+ promotion_category: promo_category)
+ end
+
+ before do
+ Spree::Promotion::Actions::CreateItemAdjustments.create!(calculator: calculator, promotion: promotion)
+ end
+
+ describe '#duplicate' do
+ let(:new_promotion) { subject.duplicate }
+
+ context 'model fields' do
+ let(:excluded_fields) { ['code', 'name', 'path', 'id', 'created_at', 'updated_at', 'deleted_at'] }
+
+ it 'returns a duplicate of a promotion with the path, name and code fields changed' do
+ expect("#{promotion.path}_new").to eq new_promotion.path
+ expect("New #{promotion.name}").to eq new_promotion.name
+ expect("#{promotion.code}_new").to eq new_promotion.code
+ end
+
+ it 'returns a duplicate of a promotion with all the fields (except the path, name and code fields) the same' do
+ promotion.attributes.each_key do |key|
+ expect(promotion.send(key)).to eq new_promotion.send(key) unless excluded_fields.include?(key)
+ end
+ end
+ end
+
+ context 'model associations - rules' do
+ let(:excluded_fields) { ['promotion_id', 'id', 'created_at', 'updated_at', 'deleted_at'] }
+
+ it 'copies all promotion rules' do
+ expect(promotion.promotion_rules.size).to eq new_promotion.promotion_rules.size
+ end
+
+ it "promotion rule's fields (except promotion_id) are the same" do
+ old_rule = promotion.promotion_rules.first
+ new_rule = new_promotion.promotion_rules.first
+
+ old_rule.attributes.each_key do |key|
+ expect(old_rule.send(key)).to eq new_rule.send(key) unless excluded_fields.include?(key)
+ end
+ end
+
+ it 'assigns a new promotion rule to new promotion' do
+ expect(promotion.promotion_rules.first.promotion_id).not_to eq new_promotion.promotion_rules.first.promotion_id
+ end
+ end
+
+ context 'model associations - actions' do
+ let(:excluded_fields) { ['promotion_id', 'id', 'created_at', 'updated_at', 'deleted_at'] }
+
+ it 'copies all promotion actions' do
+ expect(promotion.promotion_actions.size).to eq new_promotion.promotion_actions.size
+ end
+
+ it "promotion action's fields (except promotion_id) are the same" do
+ old_action = promotion.promotion_actions.first
+ new_action = new_promotion.promotion_actions.first
+
+ old_action.attributes.each_key do |key|
+ expect(old_action.send(key)).to eq new_action.send(key) unless excluded_fields.include?(key)
+ end
+ end
+
+ it 'assigns a new promotion action to new promotion' do
+ expect(promotion.promotion_actions.first.promotion_id).not_to eq new_promotion.promotion_actions.first.promotion_id
+ end
+ end
+
+ context "model associations - action's calculator" do
+ let(:excluded_fields) { ['calculable_id', 'id', 'created_at', 'updated_at', 'deleted_at'] }
+
+ it "copies promotion action's calculator" do
+ new_calc = new_promotion.promotion_actions.first.calculator
+ old_calc = promotion.promotion_actions.first.calculator
+
+ new_calc.attributes.each_key do |key|
+ expect(old_calc.send(key)).to eq new_calc.send(key) unless excluded_fields.include?(key)
+ end
+ end
+
+ it 'assigns a new calculator to promotion action' do
+ new_calc = new_promotion.promotion_actions.first.calculator
+ old_calc = promotion.promotion_actions.first.calculator
+
+ expect(old_calc.calculable_id).not_to eq new_calc.calculable_id
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion_rule_spec.rb b/core/spec/models/spree/promotion_rule_spec.rb
new file mode 100644
index 00000000000..7c880f8e8f0
--- /dev/null
+++ b/core/spec/models/spree/promotion_rule_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+module Spree
+ describe Spree::PromotionRule, type: :model do
+ class BadTestRule < Spree::PromotionRule; end
+
+ class TestRule < Spree::PromotionRule
+ def eligible?
+ true
+ end
+ end
+
+ it 'forces developer to implement eligible? method' do
+ expect { BadTestRule.new.eligible? }.to raise_error(ArgumentError)
+ end
+
+ it 'validates unique rules for a promotion' do
+ p1 = TestRule.new
+ p1.promotion_id = 1
+ p1.save
+
+ p2 = TestRule.new
+ p2.promotion_id = 1
+ expect(p2).not_to be_valid
+ end
+ end
+end
diff --git a/core/spec/models/spree/promotion_spec.rb b/core/spec/models/spree/promotion_spec.rb
new file mode 100644
index 00000000000..2a4d9877112
--- /dev/null
+++ b/core/spec/models/spree/promotion_spec.rb
@@ -0,0 +1,713 @@
+require 'spec_helper'
+
+describe Spree::Promotion, type: :model do
+ let(:promotion) { Spree::Promotion.new }
+
+ describe 'validations' do
+ before do
+ @valid_promotion = Spree::Promotion.new name: 'A promotion'
+ end
+
+ it 'valid_promotion is valid' do
+ expect(@valid_promotion).to be_valid
+ end
+
+ it 'validates usage limit' do
+ @valid_promotion.usage_limit = -1
+ expect(@valid_promotion).not_to be_valid
+
+ @valid_promotion.usage_limit = 100
+ expect(@valid_promotion).to be_valid
+ end
+
+ it 'validates name' do
+ @valid_promotion.name = nil
+ expect(@valid_promotion).not_to be_valid
+ end
+
+ describe 'expires_at_must_be_later_than_starts_at' do
+ before do
+ @valid_promotion.starts_at = Date.today
+ end
+
+ context 'starts_at is a date earlier than expires_at' do
+ before { @valid_promotion.expires_at = 5.days.from_now }
+
+ it 'is valid' do
+ expect(@valid_promotion).to be_valid
+ end
+ end
+
+ context 'starts_at is a date earlier than expires_at' do
+ before { @valid_promotion.expires_at = 5.days.ago }
+
+ context 'is not valid' do
+ before { @valid_promotion.valid? }
+
+ it { expect(@valid_promotion).not_to be_valid }
+ it { expect(@valid_promotion.errors[:expires_at]).to include(I18n.t(:invalid_date_range, scope: 'activerecord.errors.models.spree/promotion.attributes.expires_at')) }
+ end
+ end
+
+ context 'starts_at and expires_at are nil' do
+ before do
+ @valid_promotion.expires_at = nil
+ @valid_promotion.starts_at = nil
+ end
+
+ it 'is valid' do
+ expect(@valid_promotion).to be_valid
+ end
+ end
+ end
+ end
+
+ describe 'scopes' do
+ describe '.coupons' do
+ subject { Spree::Promotion.coupons }
+
+ let!(:promotion_without_code) { Spree::Promotion.create! name: 'test', code: '' }
+ let!(:promotion_with_code) { Spree::Promotion.create! name: 'test1', code: 'code' }
+
+ it 'is expected to not include promotion without code' do
+ expect(subject).not_to include(promotion_without_code)
+ end
+
+ it 'is expected to include promotion with code' do
+ expect(subject).to include(promotion_with_code)
+ end
+ end
+
+ describe '.applied' do
+ subject { Spree::Promotion.applied }
+
+ let!(:promotion_not_applied) { Spree::Promotion.create! name: 'test', code: '' }
+ let(:order) { create(:order) }
+ let!(:promotion_applied) do
+ promotion = Spree::Promotion.create!(name: 'test1', code: '')
+ promotion.orders << order
+ promotion
+ end
+
+ it 'is expected to not include promotion not applied' do
+ expect(subject).not_to include(promotion_not_applied)
+ end
+
+ it 'is expected to include promotion applied' do
+ expect(subject).to include(promotion_applied)
+ end
+ end
+
+ describe '.advertised' do
+ subject { Spree::Promotion.advertised }
+
+ let!(:promotion_not_advertised) { Spree::Promotion.create! name: 'test', advertise: false }
+ let!(:promotion_advertised) { Spree::Promotion.create! name: 'test1', advertise: true }
+
+ it 'is expected to not include promotion not advertised' do
+ expect(subject).not_to include(promotion_not_advertised)
+ end
+
+ it 'is expected to include promotion advertised' do
+ expect(subject).to include(promotion_advertised)
+ end
+ end
+ end
+
+ describe '#destroy' do
+ let(:promotion) { Spree::Promotion.create(name: 'delete me') }
+
+ before do
+ promotion.actions << Spree::Promotion::Actions::CreateAdjustment.new
+ promotion.rules << Spree::Promotion::Rules::FirstOrder.new
+ promotion.save!
+ promotion.destroy
+ end
+
+ it 'deletes actions' do
+ expect(Spree::PromotionAction.count).to eq(0)
+ end
+
+ it 'deletes rules' do
+ expect(Spree::PromotionRule.count).to eq(0)
+ end
+ end
+
+ describe '#save' do
+ let(:promotion) { Spree::Promotion.create(name: 'delete me') }
+
+ before do
+ promotion.actions << Spree::Promotion::Actions::CreateAdjustment.new
+ promotion.rules << Spree::Promotion::Rules::FirstOrder.new
+ promotion.save!
+ end
+
+ it 'deeply autosaves records and preferences' do
+ promotion.actions[0].calculator.preferred_flat_percent = 10
+ promotion.save!
+ expect(Spree::Calculator.first.preferred_flat_percent).to eq(10)
+ end
+ end
+
+ describe '#activate' do
+ before do
+ @action1 = Spree::Promotion::Actions::CreateAdjustment.create!
+ @action2 = Spree::Promotion::Actions::CreateAdjustment.create!
+ allow(@action1).to receive_messages perform: true
+ allow(@action2).to receive_messages perform: true
+
+ promotion.promotion_actions = [@action1, @action2]
+ promotion.created_at = 2.days.ago
+
+ @user = stub_model(Spree::LegacyUser, email: 'spree@example.com')
+ @order = Spree::Order.create user: @user
+ @payload = { order: @order, user: @user }
+ end
+
+ it 'checks path if present' do
+ promotion.path = 'content/cvv'
+ @payload[:path] = 'content/cvv'
+ expect(@action1).to receive(:perform).with(@payload)
+ expect(@action2).to receive(:perform).with(@payload)
+ promotion.activate(@payload)
+ end
+
+ it 'does not perform actions against an order in a finalized state' do
+ expect(@action1).not_to receive(:perform).with(@payload)
+
+ @order.state = 'complete'
+ promotion.activate(@payload)
+
+ @order.state = 'awaiting_return'
+ promotion.activate(@payload)
+
+ @order.state = 'returned'
+ promotion.activate(@payload)
+ end
+
+ it 'does activate if newer then order' do
+ expect(@action1).to receive(:perform).with(@payload)
+ promotion.created_at = Time.current + 2
+ expect(promotion.activate(@payload)).to be true
+ end
+
+ context 'keeps track of the orders' do
+ context 'when activated' do
+ it 'assigns the order' do
+ expect(promotion.orders).to be_empty
+ expect(promotion.activate(@payload)).to be true
+ expect(promotion.orders.first).to eql @order
+ end
+ end
+
+ context 'when not activated' do
+ it 'will not assign the order' do
+ @order.state = 'complete'
+ expect(promotion.orders).to be_empty
+ expect(promotion.activate(@payload)).to be_falsey
+ expect(promotion.orders).to be_empty
+ end
+ end
+ end
+ end
+
+ context '#usage_limit_exceeded' do
+ let(:promotable) { double('Promotable') }
+
+ it 'does not have its usage limit exceeded with no usage limit' do
+ promotion.usage_limit = 0
+ expect(promotion.usage_limit_exceeded?(promotable)).to be false
+ end
+
+ it 'has its usage limit exceeded' do
+ promotion.usage_limit = 2
+ allow(promotion).to receive_messages(adjusted_credits_count: 2)
+ expect(promotion.usage_limit_exceeded?(promotable)).to be true
+
+ allow(promotion).to receive_messages(adjusted_credits_count: 3)
+ expect(promotion.usage_limit_exceeded?(promotable)).to be true
+ end
+ end
+
+ context '#expired' do
+ it 'is not exipired' do
+ expect(promotion).not_to be_expired
+ end
+
+ it "is expired if it hasn't started yet" do
+ promotion.starts_at = Time.current + 1.day
+ expect(promotion).to be_expired
+ end
+
+ it 'is expired if it has already ended' do
+ promotion.expires_at = Time.current - 1.day
+ expect(promotion).to be_expired
+ end
+
+ it 'is not expired if it has started already' do
+ promotion.starts_at = Time.current - 1.day
+ expect(promotion).not_to be_expired
+ end
+
+ it 'is not expired if it has not ended yet' do
+ promotion.expires_at = Time.current + 1.day
+ expect(promotion).not_to be_expired
+ end
+
+ it 'is not expired if current time is within starts_at and expires_at range' do
+ promotion.starts_at = Time.current - 1.day
+ promotion.expires_at = Time.current + 1.day
+ expect(promotion).not_to be_expired
+ end
+
+ it 'is not expired if usage limit is not exceeded' do
+ promotion.usage_limit = 2
+ allow(promotion).to receive_messages(credits_count: 1)
+ expect(promotion).not_to be_expired
+ end
+ end
+
+ context '#credits_count' do
+ let!(:promotion) do
+ promotion = Spree::Promotion.new
+ promotion.name = 'Foo'
+ promotion.code = 'XXX'
+ promotion.tap(&:save)
+ end
+
+ let!(:action) do
+ calculator = Spree::Calculator::FlatRate.new
+ action_params = { promotion: promotion, calculator: calculator }
+ action = Spree::Promotion::Actions::CreateAdjustment.create(action_params)
+ promotion.actions << action
+ action
+ end
+
+ let!(:adjustment) do
+ order = create(:order)
+ Spree::Adjustment.create!(
+ order: order,
+ adjustable: order,
+ source: action,
+ amount: 10,
+ label: 'Promotional adjustment'
+ )
+ end
+
+ it 'counts eligible adjustments' do
+ adjustment.update_column(:eligible, true)
+ expect(promotion.credits_count).to eq(1)
+ end
+
+ # Regression test for #4112
+ it 'does not count ineligible adjustments' do
+ adjustment.update_column(:eligible, false)
+ expect(promotion.credits_count).to eq(0)
+ end
+ end
+
+ context '#adjusted_credits_count' do
+ let(:order) { create :order }
+ let(:line_item) { create :line_item, order: order }
+ let(:promotion) { Spree::Promotion.create name: 'promo', code: '10off' }
+ let(:order_action) do
+ action = Spree::Promotion::Actions::CreateAdjustment.create(calculator: Spree::Calculator::FlatPercentItemTotal.new)
+ promotion.actions << action
+ action
+ end
+ let(:item_action) do
+ action = Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: Spree::Calculator::FlatPercentItemTotal.new)
+ promotion.actions << action
+ action
+ end
+ let(:order_adjustment) do
+ Spree::Adjustment.create!(
+ source: order_action,
+ amount: 10,
+ adjustable: order,
+ order: order,
+ label: 'Promotional adjustment'
+ )
+ end
+ let(:item_adjustment) do
+ Spree::Adjustment.create!(
+ source: item_action,
+ amount: 10,
+ adjustable: line_item,
+ order: order,
+ label: 'Promotional adjustment'
+ )
+ end
+
+ it 'counts order level adjustments' do
+ expect(order_adjustment.adjustable).to eq(order)
+ expect(promotion.credits_count).to eq(1)
+ expect(promotion.adjusted_credits_count(order)).to eq(0)
+ end
+
+ it 'counts item level adjustments' do
+ expect(item_adjustment.adjustable).to eq(line_item)
+ expect(promotion.credits_count).to eq(1)
+ expect(promotion.adjusted_credits_count(order)).to eq(0)
+ end
+ end
+
+ context '#products' do
+ let(:product) { create(:product) }
+ let(:promotion) { create(:promotion) }
+
+ context 'when it has product rules with products associated' do
+ let(:promotion_rule) { create(:promotion_rule, promotion: promotion, type: 'Spree::Promotion::Rules::Product') }
+
+ before do
+ promotion.promotion_rules << promotion_rule
+ product.product_promotion_rules.create(promotion_rule_id: promotion_rule.id)
+ end
+
+ it 'has products' do
+ expect(promotion.reload.products.size).to eq(1)
+ end
+ end
+
+ context "when there's no product rule associated" do
+ it 'does not have products but still return an empty array' do
+ expect(promotion.products).to be_blank
+ end
+ end
+ end
+
+ context '#eligible?' do
+ subject { promotion.eligible?(promotable) }
+
+ let(:promotable) { create :order }
+
+ context 'when promotion is expired' do
+ before { promotion.expires_at = Time.current - 10.days }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when promotable is a Spree::LineItem' do
+ let(:promotable) { create :line_item }
+ let(:product) { promotable.product }
+
+ before do
+ product.promotionable = promotionable
+ end
+
+ context 'and product is promotionable' do
+ let(:promotionable) { true }
+
+ it { is_expected.to be true }
+ end
+
+ context 'and product is not promotionable' do
+ let(:promotionable) { false }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ context 'when promotable is a Spree::Order' do
+ let(:promotable) { create :order }
+
+ context 'and it is empty' do
+ it { is_expected.to be true }
+ end
+
+ context 'and it contains items' do
+ let!(:line_item) { create(:line_item, order: promotable) }
+
+ context 'and the items are all non-promotionable' do
+ before do
+ line_item.product.update_column(:promotionable, false)
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'and at least one item is promotionable' do
+ it { is_expected.to be true }
+ end
+ end
+ end
+ end
+
+ context '#eligible_rules' do
+ let(:promotable) { double('Promotable') }
+
+ it 'true if there are no rules' do
+ expect(promotion.eligible_rules(promotable)).to eq []
+ end
+
+ it 'true if there are no applicable rules' do
+ promotion.promotion_rules = [stub_model(Spree::PromotionRule, eligible?: true, applicable?: false)]
+ allow(promotion.promotion_rules).to receive(:for).and_return([])
+ expect(promotion.eligible_rules(promotable)).to eq []
+ end
+
+ context "with 'all' match policy" do
+ let(:promo1) { Spree::PromotionRule.create! }
+ let(:promo2) { Spree::PromotionRule.create! }
+
+ before { promotion.match_policy = 'all' }
+
+ context 'when all rules are eligible' do
+ before do
+ allow(promo1).to receive_messages(eligible?: true, applicable?: true)
+ allow(promo2).to receive_messages(eligible?: true, applicable?: true)
+
+ promotion.promotion_rules = [promo1, promo2]
+ allow(promotion.promotion_rules).to receive(:for).and_return(promotion.promotion_rules)
+ end
+
+ it 'returns the eligible rules' do
+ expect(promotion.eligible_rules(promotable)).to eq [promo1, promo2]
+ end
+ it 'does set anything to eligiblity errors' do
+ promotion.eligible_rules(promotable)
+ expect(promotion.eligibility_errors).to be_nil
+ end
+ end
+
+ context 'when any of the rules is not eligible' do
+ let(:errors) { double(ActiveModel::Errors, empty?: false) }
+
+ before do
+ allow(promo1).to receive_messages(eligible?: true, applicable?: true, eligibility_errors: nil)
+ allow(promo2).to receive_messages(eligible?: false, applicable?: true, eligibility_errors: errors)
+
+ promotion.promotion_rules = [promo1, promo2]
+ allow(promotion.promotion_rules).to receive(:for).and_return(promotion.promotion_rules)
+ end
+
+ it 'returns nil' do
+ expect(promotion.eligible_rules(promotable)).to be_nil
+ end
+ it 'sets eligibility errors to the first non-nil one' do
+ promotion.eligible_rules(promotable)
+ expect(promotion.eligibility_errors).to eq errors
+ end
+ end
+ end
+
+ context "with 'any' match policy" do
+ let(:promotion) { Spree::Promotion.create(name: 'Promo', match_policy: 'any') }
+ let(:promotable) { double('Promotable') }
+
+ it 'has eligible rules if any of the rules are eligible' do
+ allow_any_instance_of(Spree::PromotionRule).to receive_messages(applicable?: true)
+ true_rule = Spree::PromotionRule.create(promotion: promotion)
+ allow(true_rule).to receive_messages(eligible?: true)
+ allow(promotion).to receive_messages(rules: [true_rule])
+ allow(promotion).to receive_message_chain(:rules, :for).and_return([true_rule])
+ expect(promotion.eligible_rules(promotable)).to eq [true_rule]
+ end
+
+ context 'when none of the rules are eligible' do
+ let(:promo) { Spree::PromotionRule.create! }
+ let(:errors) { double ActiveModel::Errors, empty?: false }
+
+ before do
+ allow(promo).to receive_messages(eligible?: false, applicable?: true, eligibility_errors: errors)
+
+ promotion.promotion_rules = [promo]
+ allow(promotion.promotion_rules).to receive(:for).and_return(promotion.promotion_rules)
+ end
+
+ it 'returns nil' do
+ expect(promotion.eligible_rules(promotable)).to be_nil
+ end
+ it 'sets eligibility errors to the first non-nil one' do
+ promotion.eligible_rules(promotable)
+ expect(promotion.eligibility_errors).to eq errors
+ end
+ end
+ end
+ end
+
+ describe '#line_item_actionable?' do
+ subject { promotion.line_item_actionable?(order, line_item) }
+
+ let(:order) { double Spree::Order }
+ let(:line_item) { double Spree::LineItem }
+ let(:true_rule) { double Spree::PromotionRule, eligible?: true, applicable?: true, actionable?: true }
+ let(:false_rule) { double Spree::PromotionRule, eligible?: true, applicable?: true, actionable?: false }
+ let(:rules) { [] }
+
+ before do
+ allow(promotion).to receive(:rules) { rules }
+ allow(rules).to receive(:for) { rules }
+ end
+
+ context 'when the order is eligible for promotion' do
+ context 'when there are no rules' do
+ it { is_expected.to be true }
+ end
+
+ context 'when there are rules' do
+ context 'when the match policy is all' do
+ before { promotion.match_policy = 'all' }
+
+ context 'when all rules allow action on the line item' do
+ let(:rules) { [true_rule] }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when at least one rule does not allow action on the line item' do
+ let(:rules) { [true_rule, false_rule] }
+
+ it { is_expected.not_to be true }
+ end
+ end
+
+ context 'when the match policy is any' do
+ before { promotion.match_policy = 'any' }
+
+ context 'when at least one rule allows action on the line item' do
+ let(:rules) { [true_rule, false_rule] }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when no rules allow action on the line item' do
+ let(:rules) { [false_rule] }
+
+ it { is_expected.not_to be true }
+ end
+ end
+ end
+ end
+
+ context 'when the order is not eligible for the promotion' do
+ before { promotion.starts_at = Time.current + 2.days }
+
+ it { is_expected.not_to be true }
+ end
+ end
+
+ # regression for #4059
+ # admin form posts the code and path as empty string
+ describe 'normalize blank values for code & path' do
+ it 'will save blank value as nil value instead' do
+ promotion = Spree::Promotion.create(name: 'A promotion', code: '', path: '')
+ expect(promotion.code).to be_nil
+ expect(promotion.path).to be_nil
+ end
+ end
+
+ # Regression test for #4081
+ describe '#with_coupon_code' do
+ context 'and code stored in uppercase' do
+ let!(:promotion) { create(:promotion, :with_order_adjustment, code: 'MY-COUPON-123') }
+
+ it 'finds the code with lowercase' do
+ expect(Spree::Promotion.with_coupon_code('my-coupon-123')).to eql promotion
+ end
+ end
+
+ context 'when promotion has no actions' do
+ let!(:promotion_without_actions) { create(:promotion, code: 'MY-COUPON-123').actions.clear }
+
+ it 'then returns the one with an action' do
+ expect(Spree::Promotion.with_coupon_code('MY-COUPON-123')).to be_nil
+ end
+ end
+ end
+
+ describe '#used_by?' do
+ subject { promotion.used_by? user, [excluded_order] }
+
+ let(:promotion) { create :promotion, :with_order_adjustment }
+ let(:user) { create :user }
+ let(:order) { create :order_with_line_items, user: user }
+ let(:excluded_order) { create :order_with_line_items, user: user }
+
+ before do
+ order.user_id = user.id
+ order.save!
+ end
+
+ context 'when the user has used this promo' do
+ before do
+ promotion.activate(order: order)
+ order.update_with_updater!
+ order.completed_at = Time.current
+ order.save!
+ end
+
+ context 'when the order is complete' do
+ it { is_expected.to be true }
+
+ context 'when the promotion was not eligible' do
+ let(:adjustment) { order.adjustments.first }
+
+ before do
+ adjustment.eligible = false
+ adjustment.save!
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'when the only matching order is the excluded order' do
+ let(:excluded_order) { order }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ context 'when the order is not complete' do
+ let(:order) { create :order, user: user }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ context 'when the user has not used this promo' do
+ it { is_expected.to be false }
+ end
+ end
+
+ describe 'adding items to the cart' do
+ let(:order) { create :order }
+ let(:line_item) { create :line_item, order: order }
+ let(:promo) { create :promotion_with_item_adjustment, adjustment_rate: 5, code: 'promo' }
+ let(:variant) { create :variant }
+
+ it 'updates the promotions for new line items' do
+ expect(line_item.adjustments).to be_empty
+ expect(order.adjustment_total).to eq 0
+
+ promo.activate(order: order)
+ order.update_with_updater!
+
+ expect(line_item.adjustments.size).to eq(1)
+ expect(order.adjustment_total).to eq(-5)
+
+ other_line_item = Spree::Cart::AddItem.call(order: order, variant: variant, options: { currency: order.currency }).value
+
+ expect(other_line_item).not_to eq line_item
+ expect(other_line_item.adjustments.size).to eq(1)
+ expect(order.adjustment_total).to eq(-10)
+ end
+ end
+
+ describe '#generate_code' do
+ let(:promotion) { create(:promotion, code: 'spree123') }
+
+ context 'with generate_code' do
+ it 'has a generated code' do
+ promotion.generate_code = true
+ expect(promotion.code).not_to eq 'spree123'
+ end
+ end
+
+ context 'without generate_code' do
+ it 'has a generated code' do
+ expect(promotion.code).to eq 'spree123'
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/refund_reason_spec.rb b/core/spec/models/spree/refund_reason_spec.rb
new file mode 100644
index 00000000000..4f2f2555ad1
--- /dev/null
+++ b/core/spec/models/spree/refund_reason_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Spree::RefundReason do
+ describe 'Class Methods' do
+ describe '.return_processing_reason' do
+ context 'default refund reason present' do
+ let!(:default_refund_reason) { create(:default_refund_reason) }
+
+ it { expect(described_class.return_processing_reason).to eq(default_refund_reason) }
+ end
+
+ context 'default refund reason not present' do
+ it { expect(described_class.return_processing_reason).to eq(nil) }
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/refund_spec.rb b/core/spec/models/spree/refund_spec.rb
new file mode 100644
index 00000000000..dcd9dddabfa
--- /dev/null
+++ b/core/spec/models/spree/refund_spec.rb
@@ -0,0 +1,193 @@
+require 'spec_helper'
+
+describe Spree::Refund, type: :model do
+ describe 'create' do
+ subject { create(:refund, payment: payment, amount: amount, reason: refund_reason, transaction_id: nil) }
+
+ let(:amount) { 100.0 }
+ let(:amount_in_cents) { amount * 100 }
+
+ let(:authorization) { generate(:refund_transaction_id) }
+
+ let(:payment) { create(:payment, amount: payment_amount, payment_method: payment_method) }
+ let(:payment_amount) { amount * 2 }
+ let(:payment_method) { create(:credit_card_payment_method) }
+
+ let(:refund_reason) { create(:refund_reason) }
+
+ let(:gateway_response) do
+ ActiveMerchant::Billing::Response.new(
+ gateway_response_success,
+ gateway_response_message,
+ gateway_response_params,
+ gateway_response_options
+ )
+ end
+ let(:gateway_response_success) { true }
+ let(:gateway_response_message) { '' }
+ let(:gateway_response_params) { {} }
+ let(:gateway_response_options) { {} }
+
+ before do
+ allow(payment.payment_method).
+ to receive(:credit).
+ with(amount_in_cents, payment.source, payment.transaction_id, originator: an_instance_of(Spree::Refund)).
+ and_return(gateway_response)
+ end
+
+ context 'transaction id exists on creation' do
+ subject { create(:refund, payment: payment, amount: amount, reason: refund_reason, transaction_id: transaction_id) }
+
+ let(:transaction_id) { '12kfjas0' }
+
+ it 'creates a refund record' do
+ expect { subject }.to change { Spree::Refund.count }.by(1)
+ end
+
+ it 'maintains the transaction id' do
+ expect(subject.reload.transaction_id).to eq transaction_id
+ end
+
+ it 'saves the amount' do
+ expect(subject.reload.amount).to eq amount
+ end
+
+ it 'creates a log entry' do
+ expect(subject.log_entries).to be_present
+ end
+
+ it 'does not attempt to process a transaction' do
+ expect(payment.payment_method).not_to receive(:credit)
+ subject
+ end
+ end
+
+ context 'processing is successful' do
+ let(:gateway_response_options) { { authorization: authorization } }
+
+ it 'creates a refund' do
+ expect { subject }.to change { Spree::Refund.count }.by(1)
+ end
+
+ it 'return the newly created refund' do
+ expect(subject).to be_a(Spree::Refund)
+ end
+
+ it 'saves the returned authorization value' do
+ expect(subject.reload.transaction_id).to eq authorization
+ end
+
+ it 'saves the passed amount as the refund amount' do
+ expect(subject.amount).to eq amount
+ end
+
+ it 'creates a log entry' do
+ expect(subject.log_entries).to be_present
+ end
+
+ it 'attempts to process a transaction' do
+ expect(payment.payment_method).to receive(:credit).once
+ subject
+ end
+
+ it 'updates the payment total' do
+ expect(payment.order.updater).to receive(:update)
+ subject
+ end
+ end
+
+ context 'processing fails' do
+ let(:gateway_response_success) { false }
+ let(:gateway_response_message) { 'failure message' }
+
+ it 'raises error and not create a refund' do
+ expect do
+ expect { subject }.to raise_error(Spree::Core::GatewayError, gateway_response_message)
+ end.not_to change { Spree::Refund.count }
+ end
+ end
+
+ context 'without payment profiles supported' do
+ before do
+ allow(payment.payment_method).to receive(:payment_profiles_supported?).and_return(false)
+ end
+
+ it 'does not supply the payment source' do
+ expect(payment.payment_method).
+ to receive(:credit).
+ with(amount * 100, payment.transaction_id, originator: an_instance_of(Spree::Refund)).
+ and_return(gateway_response)
+
+ subject
+ end
+ end
+
+ context 'with payment profiles supported' do
+ before do
+ allow(payment.payment_method).to receive(:payment_profiles_supported?).and_return(true)
+ end
+
+ it 'supplies the payment source' do
+ expect(payment.payment_method).
+ to receive(:credit).
+ with(amount_in_cents, payment.source, payment.transaction_id, originator: an_instance_of(Spree::Refund)).
+ and_return(gateway_response)
+
+ subject
+ end
+ end
+
+ context 'with an activemerchant gateway connection error' do
+ before do
+ message = double('gateway_error')
+ expect(payment.payment_method).to receive(:credit).with(
+ amount_in_cents,
+ payment.source,
+ payment.transaction_id,
+ originator: an_instance_of(Spree::Refund)
+ ).and_raise(ActiveMerchant::ConnectionError.new(message, nil))
+ end
+
+ it 'raises Spree::Core::GatewayError' do
+ expect { subject }.to raise_error(Spree::Core::GatewayError, Spree.t(:unable_to_connect_to_gateway))
+ end
+ end
+
+ context 'with amount too large' do
+ let(:payment_amount) { 10 }
+ let(:amount) { payment_amount * 2 }
+
+ it 'is invalid' do
+ expect { subject }.to raise_error { |error|
+ expect(error).to be_a(ActiveRecord::RecordInvalid)
+ expect(error.record.errors.full_messages).to eq ["Amount #{I18n.t('activerecord.errors.models.spree/refund.attributes.amount.greater_than_allowed')}"]
+ }
+ end
+ end
+ end
+
+ describe 'total_amount_reimbursed_for' do
+ subject { Spree::Refund.total_amount_reimbursed_for(reimbursement) }
+
+ let(:customer_return) { reimbursement.customer_return }
+ let(:reimbursement) { create(:reimbursement) }
+ let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) }
+
+ context 'with reimbursements performed' do
+ before { reimbursement.perform! }
+
+ it 'returns the total amount' do
+ amount = Spree::Refund.total_amount_reimbursed_for(reimbursement)
+ expect(amount).to be > 0
+ expect(amount).to eq reimbursement.total
+ end
+ end
+
+ context 'without reimbursements performed' do
+ it 'returns zero' do
+ amount = Spree::Refund.total_amount_reimbursed_for(reimbursement)
+ expect(amount).to eq 0
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement/credit_spec.rb b/core/spec/models/spree/reimbursement/credit_spec.rb
new file mode 100644
index 00000000000..6e4e6935740
--- /dev/null
+++ b/core/spec/models/spree/reimbursement/credit_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+module Spree
+ describe Reimbursement::Credit, type: :model do
+ context 'class methods' do
+ describe '.total_amount_reimbursed_for' do
+ subject { Spree::Reimbursement::Credit.total_amount_reimbursed_for(reimbursement) }
+
+ let(:reimbursement) { create(:reimbursement) }
+ let(:credit_double) { double(amount: 99.99) }
+
+ before { allow(reimbursement).to receive(:credits).and_return([credit_double, credit_double]) }
+
+ it 'sums the amounts of all of the reimbursements credits' do
+ expect(subject).to eq BigDecimal('199.98')
+ end
+ end
+ end
+
+ describe '#description' do
+ let(:credit) { Spree::Reimbursement::Credit.new(amount: 100, creditable: mock_model(Spree::PaymentMethod::Check)) }
+
+ it "is the creditable's class name" do
+ expect(credit.description).to eq 'Check'
+ end
+ end
+
+ describe '#display_amount' do
+ let(:credit) { Spree::Reimbursement::Credit.new(amount: 100) }
+
+ it 'is a money object' do
+ expect(credit.display_amount).to eq Spree::Money.new(100, currency: 'USD')
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement/reimbursement_type_engine_spec.rb b/core/spec/models/spree/reimbursement/reimbursement_type_engine_spec.rb
new file mode 100644
index 00000000000..66fedb2e504
--- /dev/null
+++ b/core/spec/models/spree/reimbursement/reimbursement_type_engine_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+module Spree
+ describe Reimbursement::ReimbursementTypeEngine, type: :model do
+ describe '#calculate_reimbursement_types' do
+ subject { reimbursement_type_engine.calculate_reimbursement_types }
+
+ let(:return_item) { create(:return_item) }
+ let(:return_items) { [return_item] }
+ let(:reimbursement_type_engine) { Spree::Reimbursement::ReimbursementTypeEngine.new(return_items) }
+ let(:expired_reimbursement_type) { Spree::ReimbursementType::OriginalPayment }
+ let(:override_reimbursement_type) { Spree::ReimbursementType::OriginalPayment.new }
+ let(:preferred_reimbursement_type) { Spree::ReimbursementType::OriginalPayment.new }
+ let(:calculated_reimbursement_types) { subject }
+ let(:all_reimbursement_types) do
+ [
+ reimbursement_type_engine.default_reimbursement_type,
+ reimbursement_type_engine.exchange_reimbursement_type,
+ expired_reimbursement_type,
+ override_reimbursement_type,
+ preferred_reimbursement_type
+ ]
+ end
+
+ shared_examples_for 'reimbursement type hash' do
+ it 'contain all keys that respond to reimburse' do
+ expect(calculated_reimbursement_types.keys).to all(respond_to(:reimburse))
+ end
+ end
+
+ before do
+ reimbursement_type_engine.expired_reimbursement_type = expired_reimbursement_type
+ allow(return_item.inventory_unit.shipment).to receive(:shipped_at).and_return(Date.yesterday)
+ allow(return_item).to receive(:exchange_required?).and_return(false)
+ end
+
+ context 'the return item requires exchange' do
+ before { allow(return_item).to receive(:exchange_required?).and_return(true) }
+
+ it 'returns a hash with the exchange reimbursement type associated to the return items' do
+ expect(calculated_reimbursement_types[reimbursement_type_engine.exchange_reimbursement_type]).to eq(return_items)
+ end
+
+ it 'the return items are not included in any of the other reimbursement types' do
+ (all_reimbursement_types - [reimbursement_type_engine.exchange_reimbursement_type]).each do |r_type|
+ expect(calculated_reimbursement_types[r_type]).to eq([])
+ end
+ end
+
+ it_behaves_like 'reimbursement type hash'
+ end
+
+ context 'the return item does not require exchange' do
+ context 'the return item has an override reimbursement type' do
+ before { allow(return_item).to receive(:override_reimbursement_type).and_return(override_reimbursement_type) }
+
+ it 'returns a hash with the override reimbursement type associated to the return items' do
+ expect(calculated_reimbursement_types[override_reimbursement_type.class]).to eq(return_items)
+ end
+
+ it 'the return items are not included in any of the other reimbursement types' do
+ (all_reimbursement_types - [override_reimbursement_type.class]).each do |r_type|
+ expect(calculated_reimbursement_types[r_type]).to eq([])
+ end
+ end
+
+ it_behaves_like 'reimbursement type hash'
+ end
+
+ context 'the return item does not have an override reimbursement type' do
+ context 'the return item has a preferred reimbursement type' do
+ before { allow(return_item).to receive(:preferred_reimbursement_type).and_return(preferred_reimbursement_type) }
+
+ context 'the reimbursement type is not valid for the return item' do
+ before { expect(reimbursement_type_engine).to receive(:valid_preferred_reimbursement_type?).and_return(false) }
+
+ it 'returns a hash with no return items associated to the preferred reimbursement type' do
+ expect(calculated_reimbursement_types[preferred_reimbursement_type]).to eq([])
+ end
+
+ it 'the return items are not included in any of the other reimbursement types' do
+ (all_reimbursement_types - [preferred_reimbursement_type]).each do |r_type|
+ expect(calculated_reimbursement_types[r_type]).to eq([])
+ end
+ end
+
+ it_behaves_like 'reimbursement type hash'
+ end
+
+ context 'the reimbursement type is valid for the return item' do
+ it 'returns a hash with the expired reimbursement type associated to the return items' do
+ expect(calculated_reimbursement_types[preferred_reimbursement_type.class]).to eq(return_items)
+ end
+
+ it 'the return items are not included in any of the other reimbursement types' do
+ (all_reimbursement_types - [preferred_reimbursement_type.class]).each do |r_type|
+ expect(calculated_reimbursement_types[r_type]).to eq([])
+ end
+ end
+
+ it_behaves_like 'reimbursement type hash'
+ end
+ end
+
+ context 'the return item does not have a preferred reimbursement type' do
+ context 'the return item is past the time constraint' do
+ before { allow(reimbursement_type_engine).to receive(:past_reimbursable_time_period?).and_return(true) }
+
+ it 'returns a hash with the expired reimbursement type associated to the return items' do
+ expect(calculated_reimbursement_types[expired_reimbursement_type]).to eq(return_items)
+ end
+
+ it 'the return items are not included in any of the other reimbursement types' do
+ (all_reimbursement_types - [expired_reimbursement_type]).each do |r_type|
+ expect(calculated_reimbursement_types[r_type]).to eq([])
+ end
+ end
+
+ it_behaves_like 'reimbursement type hash'
+ end
+
+ context 'the return item is within the time constraint' do
+ it 'returns a hash with the default reimbursement type associated to the return items' do
+ expect(calculated_reimbursement_types[reimbursement_type_engine.default_reimbursement_type]).to eq(return_items)
+ end
+
+ it 'the return items are not included in any of the other reimbursement types' do
+ (all_reimbursement_types - [reimbursement_type_engine.default_reimbursement_type]).each do |r_type|
+ expect(calculated_reimbursement_types[r_type]).to eq([])
+ end
+ end
+
+ it_behaves_like 'reimbursement type hash'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement/reimbursement_type_validator_spec.rb b/core/spec/models/spree/reimbursement/reimbursement_type_validator_spec.rb
new file mode 100644
index 00000000000..873b8e1f602
--- /dev/null
+++ b/core/spec/models/spree/reimbursement/reimbursement_type_validator_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+module Spree
+ describe Reimbursement::ReimbursementTypeValidator, type: :model do
+ class DummyClass
+ include Spree::Reimbursement::ReimbursementTypeValidator
+
+ class_attribute :expired_reimbursement_type
+ self.expired_reimbursement_type = Spree::ReimbursementType::Credit
+
+ class_attribute :refund_time_constraint
+ self.refund_time_constraint = 90.days
+ end
+
+ let(:return_item) do
+ create(
+ :return_item,
+ preferred_reimbursement_type: preferred_reimbursement_type
+ )
+ end
+ let(:dummy) { DummyClass.new }
+ let(:preferred_reimbursement_type) { Spree::ReimbursementType::Credit.new }
+
+ describe '#valid_preferred_reimbursement_type?' do
+ subject { dummy.valid_preferred_reimbursement_type?(return_item) }
+
+ before do
+ allow(dummy).to receive(:past_reimbursable_time_period?).and_return(true)
+ end
+
+ context 'is valid' do
+ it 'if it is not past the reimbursable time period' do
+ allow(dummy).to receive(:past_reimbursable_time_period?).and_return(false)
+ expect(subject).to be true
+ end
+
+ it 'if the return items preferred method of reimbursement is the expired method of reimbursement' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'is invalid' do
+ it 'if the return item is past the eligible time period and the preferred method of reimbursement is not the expired method of reimbursement' do
+ return_item.preferred_reimbursement_type =
+ Spree::ReimbursementType::OriginalPayment.new
+ expect(subject).to be false
+ end
+ end
+ end
+
+ describe '#past_reimbursable_time_period?' do
+ subject { dummy.past_reimbursable_time_period?(return_item) }
+
+ before do
+ allow(return_item).to receive_message_chain(:inventory_unit, :shipment, :shipped_at).and_return(shipped_at)
+ end
+
+ context 'it has not shipped' do
+ let(:shipped_at) { nil }
+
+ it 'is not past the reimbursable time period' do
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'it has shipped and it is more recent than the time constraint' do
+ let(:shipped_at) { (dummy.refund_time_constraint - 1.day).ago }
+
+ it 'is not past the reimbursable time period' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'it has shipped and it is further in the past than the time constraint' do
+ let(:shipped_at) { (dummy.refund_time_constraint + 1.day).ago }
+
+ it 'is past the reimbursable time period' do
+ expect(subject).to be true
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement_performer_spec.rb b/core/spec/models/spree/reimbursement_performer_spec.rb
new file mode 100644
index 00000000000..4fea69ec01f
--- /dev/null
+++ b/core/spec/models/spree/reimbursement_performer_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Spree::ReimbursementPerformer, type: :model do
+ let(:reimbursement) { create(:reimbursement, return_items_count: 1) }
+ let(:return_item) { reimbursement.return_items.first }
+ let(:reimbursement_type) { double('ReimbursementType') }
+ let(:reimbursement_type_hash) { { reimbursement_type => [return_item] } }
+ let(:reimbursement_type_engine) { Spree::Reimbursement::ReimbursementTypeEngine }
+
+ before do
+ allow_any_instance_of(reimbursement_type_engine).to receive(:calculate_reimbursement_types).and_return(reimbursement_type_hash)
+ end
+
+ describe '.simulate' do
+ it 'reimburses each calculated reimbursement types with the correct return items as a simulation' do
+ expect(reimbursement_type).to receive(:reimburse).with(reimbursement, [return_item], true)
+ Spree::ReimbursementPerformer.simulate(reimbursement)
+ end
+ end
+
+ describe '.perform' do
+ it 'reimburses each calculated reimbursement types with the correct return items as a performance' do
+ expect(reimbursement_type).to receive(:reimburse).with(reimbursement, [return_item], false)
+ Spree::ReimbursementPerformer.perform(reimbursement)
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement_spec.rb b/core/spec/models/spree/reimbursement_spec.rb
new file mode 100644
index 00000000000..2f4f3289321
--- /dev/null
+++ b/core/spec/models/spree/reimbursement_spec.rb
@@ -0,0 +1,198 @@
+require 'spec_helper'
+
+describe Spree::Reimbursement, type: :model do
+ describe '#display_total' do
+ subject { reimbursement.display_total }
+
+ let(:total) { 100.50 }
+ let(:currency) { 'USD' }
+ let(:order) { Spree::Order.new(currency: currency) }
+ let(:reimbursement) { Spree::Reimbursement.new(total: total, order: order) }
+
+ it 'returns the value as a Spree::Money instance' do
+ expect(subject).to eq Spree::Money.new(total)
+ end
+
+ it "uses the order's currency" do
+ expect(subject.money.currency.to_s).to eq currency
+ end
+ end
+
+ describe '#perform!' do
+ subject { reimbursement.perform! }
+
+ let!(:adjustments) { [] } # placeholder to ensure it gets run prior the "before" at this level
+
+ let!(:tax_rate) { nil }
+ let!(:tax_zone) { create(:zone_with_country, default_tax: true) }
+
+ let(:order) { create(:order_with_line_items, state: 'payment', line_items_count: 1, line_items_price: line_items_price, shipment_cost: 0) }
+ let(:line_items_price) { BigDecimal(10) }
+ let(:line_item) { order.line_items.first }
+ let(:inventory_unit) { line_item.inventory_units.first }
+ let(:payment) { build(:payment, amount: payment_amount, order: order, state: 'completed') }
+ let(:payment_amount) { order.total }
+ let(:customer_return) { build(:customer_return, return_items: [return_item]) }
+ let(:return_item) { build(:return_item, inventory_unit: inventory_unit) }
+
+ let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) }
+
+ let(:reimbursement) { create(:reimbursement, customer_return: customer_return, order: order, return_items: [return_item]) }
+
+ let(:store_credit_reimbursement_type) { create(:reimbursement_type, name: 'StoreCredit', type: 'Spree::ReimbursementType::StoreCredit') }
+
+ before do
+ order.shipments.each do |shipment|
+ shipment.inventory_units.update_all state: 'shipped'
+ shipment.update_column('state', 'shipped')
+ end
+ order.reload
+ order.update_with_updater!
+ if payment
+ payment.save!
+ order.next! # confirm
+ end
+ order.next! # completed
+ customer_return.save!
+ return_item.accept!
+ end
+
+ it 'refunds the total amount' do
+ subject
+ expect(reimbursement.unpaid_amount).to eq 0
+ end
+
+ it 'creates a refund' do
+ expect do
+ subject
+ end.to change { Spree::Refund.count }.by(1)
+ expect(Spree::Refund.last.amount).to eq order.total
+ end
+
+ context 'with additional tax' do
+ let!(:tax_rate) { create(:tax_rate, name: 'Sales Tax', amount: 0.10, included_in_price: false, zone: tax_zone) }
+
+ it 'saves the additional tax and refunds the total' do
+ expect do
+ subject
+ end.to change { Spree::Refund.count }.by(1)
+ return_item.reload
+ expect(return_item.additional_tax_total).to be > 0
+ expect(return_item.additional_tax_total).to eq line_item.additional_tax_total
+ expect(reimbursement.total).to eq line_item.pre_tax_amount + line_item.additional_tax_total
+ expect(Spree::Refund.last.amount).to eq line_item.pre_tax_amount + line_item.additional_tax_total
+ end
+ end
+
+ context 'with included tax' do
+ let!(:tax_rate) { create(:tax_rate, name: 'VAT Tax', amount: 0.1, included_in_price: true, zone: tax_zone) }
+
+ it 'saves the included tax and refunds the total' do
+ expect do
+ subject
+ end.to change { Spree::Refund.count }.by(1)
+ return_item.reload
+ expect(return_item.included_tax_total).to be > 0
+ expect(return_item.included_tax_total).to eq line_item.included_tax_total
+ expect(reimbursement.total).to eq (line_item.pre_tax_amount + line_item.included_tax_total).round(2)
+ expect(Spree::Refund.last.amount).to eq (line_item.pre_tax_amount + line_item.included_tax_total).round(2)
+ end
+ end
+
+ context 'when reimbursement cannot be fully performed' do
+ let!(:non_return_refund) { create(:refund, amount: 1, payment: payment) }
+
+ it 'raises IncompleteReimbursement error' do
+ expect { subject }.to raise_error(Spree::Reimbursement::IncompleteReimbursementError)
+ end
+ end
+
+ context 'when reimbursement is performed using store credits' do
+ it 'succeeds' do
+ reimbursement.return_items.last.update(preferred_reimbursement_type_id: store_credit_reimbursement_type.id)
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when exchange is required' do
+ let(:exchange_variant) { build(:variant) }
+
+ before { return_item.exchange_variant = exchange_variant }
+
+ it 'generates an exchange shipment for the order for the exchange items' do
+ expect { subject }.to change { order.reload.shipments.count }.by 1
+ expect(order.shipments.last.inventory_units.first.variant).to eq exchange_variant
+ end
+ end
+
+ it 'triggers the reimbursement mailer to be sent' do
+ expect(Spree::ReimbursementMailer).to receive(:reimbursement_email).with(reimbursement.id) { double(deliver_later: true) }
+ subject
+ end
+ end
+
+ describe '#return_items_requiring_exchange' do
+ it 'returns only the return items that require an exchange' do
+ return_items = [double(exchange_required?: true), double(exchange_required?: true), double(exchange_required?: false)]
+ allow(subject).to receive(:return_items) { return_items }
+ expect(subject.return_items_requiring_exchange).to eq return_items.take(2)
+ end
+ end
+
+ describe '#calculated_total' do
+ context 'with return item amounts that would round up if added' do
+ subject { reimbursement.calculated_total }
+
+ let(:reimbursement) { Spree::Reimbursement.new }
+
+ before do
+ reimbursement.return_items << Spree::ReturnItem.new(pre_tax_amount: 10.003)
+ reimbursement.return_items << Spree::ReturnItem.new(pre_tax_amount: 10.003)
+ end
+
+ it 'rounds down' do
+ expect(subject).to eq 20
+ end
+ end
+
+ context 'with a return item amount that should round up' do
+ subject { reimbursement.calculated_total }
+
+ let(:reimbursement) { Spree::Reimbursement.new }
+
+ before do
+ reimbursement.return_items << Spree::ReturnItem.new(pre_tax_amount: 19.998)
+ end
+
+ it 'rounds up' do
+ expect(subject).to eq 20
+ end
+ end
+ end
+
+ describe '.build_from_customer_return' do
+ subject { Spree::Reimbursement.build_from_customer_return(customer_return) }
+
+ let(:customer_return) { create(:customer_return, line_items_count: 5) }
+
+ let!(:pending_return_item) { customer_return.return_items.first.tap { |ri| ri.update!(acceptance_status: 'pending') } }
+ let!(:accepted_return_item) { customer_return.return_items.second.tap(&:accept!) }
+ let!(:rejected_return_item) { customer_return.return_items.third.tap(&:reject!) }
+ let!(:manual_intervention_return_item) { customer_return.return_items.fourth.tap(&:require_manual_intervention!) }
+ let!(:already_reimbursed_return_item) { customer_return.return_items.fifth }
+
+ let!(:previous_reimbursement) { create(:reimbursement, order: customer_return.order, return_items: [already_reimbursed_return_item]) }
+
+ it 'connects to the accepted return items' do
+ expect(subject.return_items.to_a).to eq [accepted_return_item]
+ end
+
+ it 'connects to the order' do
+ expect(subject.order).to eq customer_return.order
+ end
+
+ it 'connects to the customer_return' do
+ expect(subject.customer_return).to eq customer_return
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement_tax_calculator_spec.rb b/core/spec/models/spree/reimbursement_tax_calculator_spec.rb
new file mode 100644
index 00000000000..3352dd624c0
--- /dev/null
+++ b/core/spec/models/spree/reimbursement_tax_calculator_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Spree::ReimbursementTaxCalculator, type: :model do
+ subject do
+ Spree::ReimbursementTaxCalculator.call(reimbursement)
+ end
+
+ let!(:tax_rate) { nil }
+
+ let(:reimbursement) { create(:reimbursement, return_items_count: 1) }
+ let(:return_item) { reimbursement.return_items.first }
+ let(:line_item) { return_item.inventory_unit.line_item }
+
+ context 'without taxes' do
+ let!(:tax_rate) { nil }
+
+ it 'leaves the return items additional_tax_total and included_tax_total at zero' do
+ subject
+
+ expect(return_item.additional_tax_total).to eq 0
+ expect(return_item.included_tax_total).to eq 0
+ end
+ end
+
+ context 'with additional tax' do
+ let!(:tax_rate) do
+ create :tax_rate,
+ name: 'Sales Tax',
+ amount: 0.10,
+ included_in_price: false,
+ tax_category: create(:tax_category),
+ zone: create(:zone_with_country, default_tax: true)
+ end
+
+ it 'sets additional_tax_total on the return items' do
+ subject
+ return_item.reload
+
+ expect(return_item.additional_tax_total).to be > 0
+ expect(return_item.additional_tax_total).to eq line_item.additional_tax_total
+ end
+ end
+
+ context 'with included tax' do
+ let!(:tax_rate) do
+ create :tax_rate,
+ name: 'VAT Tax',
+ amount: 0.10,
+ included_in_price: true,
+ tax_category: create(:tax_category),
+ zone: create(:zone_with_country, default_tax: true)
+ end
+
+ it 'sets included_tax_total on the return items' do
+ subject
+ return_item.reload
+
+ expect(return_item.included_tax_total).to be > 0
+ expect(return_item.included_tax_total).to eq line_item.included_tax_total
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement_type/credit_spec.rb b/core/spec/models/spree/reimbursement_type/credit_spec.rb
new file mode 100644
index 00000000000..366f55e682e
--- /dev/null
+++ b/core/spec/models/spree/reimbursement_type/credit_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+module Spree
+ describe ReimbursementType::Credit, type: :model do
+ subject { Spree::ReimbursementType::Credit.reimburse(reimbursement, [return_item], simulate) }
+
+ let(:reimbursement) { create(:reimbursement, return_items_count: 1) }
+ let(:return_item) { reimbursement.return_items.first }
+ let(:payment) { reimbursement.order.payments.first }
+ let(:simulate) { false }
+ let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) }
+ let(:creditable) { DummyCreditable.new(amount: 99.99) }
+
+ class DummyCreditable < Spree::Base
+ attr_accessor :amount
+ self.table_name = 'spree_payments' # Your creditable class should not use this table
+ end
+
+ before do
+ reimbursement.update!(total: reimbursement.calculated_total)
+ allow(Spree::StoreCredit).to receive(:new).and_return(creditable)
+ end
+
+ describe '.reimburse' do
+ context 'simulate is true' do
+ let(:simulate) { true }
+
+ it 'creates one readonly lump credit for all outstanding balance payable to the customer' do
+ expect(subject.map(&:class)).to eq [Spree::Reimbursement::Credit]
+ expect(subject.map(&:readonly?)).to eq [true]
+ expect(subject.sum(&:amount)).to eq reimbursement.return_items.to_a.sum(&:total)
+ end
+
+ it 'does not save to the database' do
+ expect { subject }.not_to change { Spree::Reimbursement::Credit.count }
+ end
+ end
+
+ context 'simulate is false' do
+ let(:simulate) { false }
+
+ before do
+ expect(creditable).to receive(:save).and_return(true)
+ end
+
+ it 'creates one lump credit for all outstanding balance payable to the customer' do
+ expect { subject }.to change { Spree::Reimbursement::Credit.count }.by(1)
+ expect(subject.sum(&:amount)).to eq reimbursement.return_items.to_a.sum(&:total)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement_type/exchange_spec.rb b/core/spec/models/spree/reimbursement_type/exchange_spec.rb
new file mode 100644
index 00000000000..7a4533a2d07
--- /dev/null
+++ b/core/spec/models/spree/reimbursement_type/exchange_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+module Spree
+ describe ReimbursementType::Exchange, type: :model do
+ describe '.reimburse' do
+ subject { Spree::ReimbursementType::Exchange.reimburse(reimbursement, return_items, simulate) }
+
+ let(:reimbursement) { create(:reimbursement, return_items_count: 1) }
+ let(:return_items) { reimbursement.return_items }
+ let(:new_exchange) { double('Exchange') }
+ let(:simulate) { true }
+
+ context 'return items are supplied' do
+ before do
+ expect(Spree::Exchange).to receive(:new).with(reimbursement.order, return_items).and_return(new_exchange)
+ end
+
+ context 'simulate is true' do
+ it 'does not perform an exchange and returns the exchange object' do
+ expect(new_exchange).not_to receive(:perform!)
+ expect(subject).to eq [new_exchange]
+ end
+ end
+
+ context 'simulate is false' do
+ let(:simulate) { false }
+
+ it 'performs an exchange and returns the exchange object' do
+ expect(new_exchange).to receive(:perform!)
+ expect(subject).to eq [new_exchange]
+ end
+ end
+ end
+
+ context 'no return items are supplied' do
+ let(:return_items) { [] }
+
+ it 'does not perform an exchange and returns an empty array' do
+ expect(new_exchange).not_to receive(:perform!)
+ expect(subject).to eq []
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement_type/original_payment_spec.rb b/core/spec/models/spree/reimbursement_type/original_payment_spec.rb
new file mode 100644
index 00000000000..f2f40bff892
--- /dev/null
+++ b/core/spec/models/spree/reimbursement_type/original_payment_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+module Spree
+ describe ReimbursementType::OriginalPayment, type: :model do
+ subject { Spree::ReimbursementType::OriginalPayment.reimburse(reimbursement, [return_item], simulate) }
+
+ let(:reimbursement) { create(:reimbursement, return_items_count: 1) }
+ let(:return_item) { reimbursement.return_items.first }
+ let(:payment) { reimbursement.order.payments.first }
+ let(:simulate) { false }
+ let!(:default_refund_reason) { Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false) }
+
+ before { reimbursement.update!(total: reimbursement.calculated_total) }
+
+ describe '.reimburse' do
+ context 'simulate is true' do
+ let(:simulate) { true }
+
+ it 'returns an array of readonly refunds' do
+ expect(subject.map(&:class)).to eq [Spree::Refund]
+ expect(subject.map(&:readonly?)).to eq [true]
+ end
+ end
+
+ context 'simulate is false' do
+ it 'performs the refund' do
+ expect do
+ subject
+ end.to change { payment.refunds.count }.by(1)
+ expect(payment.refunds.sum(:amount)).to eq reimbursement.return_items.to_a.sum(&:total)
+ end
+ end
+
+ context 'when no credit is allowed on the payment' do
+ before do
+ expect_any_instance_of(Spree::Payment).to receive(:credit_allowed).and_return 0
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to eq []
+ end
+ end
+
+ context 'when a payment is negative' do
+ before do
+ expect_any_instance_of(Spree::Payment).to receive(:amount).and_return(-100)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to eq []
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/reimbursement_type/store_credit_spec.rb b/core/spec/models/spree/reimbursement_type/store_credit_spec.rb
new file mode 100644
index 00000000000..ef1577d55c1
--- /dev/null
+++ b/core/spec/models/spree/reimbursement_type/store_credit_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+module Spree
+ describe ReimbursementType::StoreCredit do
+ subject do
+ Spree::ReimbursementType::StoreCredit.reimburse(reimbursement, [return_item, return_item2],
+ simulate)
+ end
+
+ let(:reimbursement) { create(:reimbursement, return_items_count: 2) }
+ let(:return_item) { reimbursement.return_items.first }
+ let(:return_item2) { reimbursement.return_items.last }
+ let(:payment) { reimbursement.order.payments.first }
+ let(:simulate) { false }
+ let!(:default_refund_reason) do
+ Spree::RefundReason.find_or_create_by!(name: Spree::RefundReason::RETURN_PROCESSING_REASON,
+ mutable: false)
+ end
+
+ let!(:primary_credit_type) { create(:primary_credit_type) }
+ let!(:created_by_user) { create(:user, email: Spree::StoreCredit::DEFAULT_CREATED_BY_EMAIL) }
+ let!(:default_reimbursement_category) { create(:store_credit_category) }
+
+ before { reimbursement.update!(total: reimbursement.calculated_total) }
+
+ describe '.reimburse' do
+ context 'simulate is true' do
+ let(:simulate) { true }
+
+ context 'for store credits that the customer used' do
+ before do
+ allow(reimbursement).to receive_message_chain('order.payments.completed.store_credits').and_return([payment])
+ end
+
+ it 'creates readonly refunds for all store credit payments' do
+ expect(subject.map(&:class)).to eq [Spree::Refund]
+ expect(subject.map(&:readonly?)).to eq [true]
+ end
+
+ it 'does not save to the database' do
+ expect { subject }.not_to change { payment.refunds.count }
+ end
+ end
+
+ context 'for return items that were not paid for with store credit' do
+ context 'creates one readonly lump credit for all outstanding balance payable to the customer' do
+ it 'creates a credit that is read only' do
+ expect(subject.map(&:class)).to eq [Spree::Reimbursement::Credit]
+ expect(subject.map(&:readonly?)).to eq [true]
+ end
+
+ it 'creates a credit which amounts to the sum of the return items rounded down' do
+ allow(return_item).to receive(:total).and_return(10.0076)
+ allow(return_item2).to receive(:total).and_return(10.0023)
+ expect(subject.sum(&:amount)).to eq 20.0
+ end
+ end
+
+ it 'does not save to the database' do
+ expect { subject }.not_to change { Spree::Reimbursement::Credit.count }
+ end
+ end
+ end
+
+ context 'simulate is false' do
+ let(:simulate) { false }
+
+ context 'for store credits that the customer used' do
+ before do
+ allow(reimbursement).to receive_message_chain('order.payments.completed.store_credits').and_return([payment])
+ end
+
+ it 'performs refunds for all store credit payments' do
+ expect { subject }.to change { payment.refunds.count }.by(1)
+ expect(payment.refunds.sum(:amount)).to eq reimbursement.return_items.to_a.sum(&:total)
+ end
+ end
+
+ context 'for return items that were not paid for with store credit' do
+ before do
+ allow(Spree::ReimbursementType::StoreCredit).to receive(:store_credit_payments).and_return([])
+ end
+
+ it 'creates one lump credit for all outstanding balance payable to the customer' do
+ expect { subject }.to change { Spree::Reimbursement::Credit.count }.by(1)
+ expect(subject.sum(&:amount)).to eq reimbursement.return_items.to_a.sum(&:total)
+ end
+
+ it "creates a store credit with the same currency as the reimbursement's order" do
+ expect { subject }.to change { Spree::StoreCredit.count }.by(1)
+ expect(Spree::StoreCredit.last.currency).to eq reimbursement.order.currency
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_authorization_spec.rb b/core/spec/models/spree/return_authorization_spec.rb
new file mode 100644
index 00000000000..0202e5d5f74
--- /dev/null
+++ b/core/spec/models/spree/return_authorization_spec.rb
@@ -0,0 +1,234 @@
+require 'spec_helper'
+
+describe Spree::ReturnAuthorization, type: :model do
+ let(:order) { create(:shipped_order) }
+ let(:stock_location) { create(:stock_location) }
+ let(:rma_reason) { create(:return_authorization_reason) }
+ let(:inventory_unit_1) { order.inventory_units.first }
+
+ let(:variant) { order.variants.first }
+ let(:return_authorization) do
+ Spree::ReturnAuthorization.new(order: order,
+ stock_location_id: stock_location.id,
+ return_authorization_reason_id: rma_reason.id)
+ end
+
+ context 'save' do
+ let(:order) { Spree::Order.create }
+
+ it 'is invalid when order has no inventory units' do
+ return_authorization.save
+ expect(return_authorization.errors[:order]).to eq(['has no shipped units'])
+ end
+
+ context 'expedited exchanges are configured' do
+ subject { create(:return_authorization, order: order, return_items: [exchange_return_item, return_item]) }
+
+ let(:order) { create(:shipped_order, line_items_count: 2) }
+ let(:exchange_return_item) { create(:exchange_return_item, inventory_unit: order.inventory_units.first) }
+ let(:return_item) { create(:return_item, inventory_unit: order.inventory_units.last) }
+
+ before do
+ @expediteted_exchanges_config = Spree::Config[:expedited_exchanges]
+ Spree::Config[:expedited_exchanges] = true
+ @pre_exchange_hooks = subject.class.pre_expedited_exchange_hooks
+ end
+
+ after do
+ Spree::Config[:expedited_exchanges] = @expediteted_exchanges_config
+ subject.class.pre_expedited_exchange_hooks = Array(@pre_exchange_hooks)
+ end
+
+ context 'no items to exchange' do
+ subject { create(:return_authorization, order: order) }
+
+ it 'does not create a reimbursement' do
+ expect { subject.save }.not_to change { Spree::Reimbursement.count }
+ end
+ end
+
+ context 'items to exchange' do
+ it 'calls pre_expedited_exchange hooks with the return items to exchange' do
+ hook = double(:as_null_object)
+ expect(hook).to receive(:call).with [exchange_return_item]
+ subject.class.pre_expedited_exchange_hooks = [hook]
+ subject.save
+ end
+
+ it 'attempts to accept all return items requiring exchange' do
+ expect(exchange_return_item).to receive :attempt_accept
+ expect(return_item).not_to receive :attempt_accept
+ subject.save
+ end
+
+ it 'performs an exchange reimbursement for the exchange return items' do
+ subject.save
+ reimbursement = Spree::Reimbursement.last
+ expect(reimbursement.order).to eq subject.order
+ expect(reimbursement.return_items).to eq [exchange_return_item]
+ expect(exchange_return_item.reload.exchange_shipments).to be_present
+ end
+
+ context 'the reimbursement fails' do
+ before do
+ allow_any_instance_of(Spree::Reimbursement).to receive(:save).and_return(false)
+ allow_any_instance_of(Spree::Reimbursement).to receive(:errors) { double(full_messages: 'foo') }
+ end
+
+ it 'puts errors on the return authorization' do
+ subject.save
+ expect(subject.errors[:base]).to include 'foo'
+ end
+ end
+ end
+ end
+ end
+
+ describe 'whitelisted_ransackable_attributes' do
+ it { expect(Spree::ReturnAuthorization.whitelisted_ransackable_attributes).to eq(%w(memo number state)) }
+ end
+
+ context '#currency' do
+ before { allow(order).to receive(:currency).and_return('ABC') }
+
+ it 'returns the order currency' do
+ expect(return_authorization.currency).to eq('ABC')
+ end
+ end
+
+ describe '#pre_tax_total' do
+ subject { return_authorization.pre_tax_total }
+
+ let(:pre_tax_amount_1) { 15.0 }
+ let!(:return_item_1) { create(:return_item, return_authorization: return_authorization, pre_tax_amount: pre_tax_amount_1) }
+
+ let(:pre_tax_amount_2) { 50.0 }
+ let!(:return_item_2) { create(:return_item, return_authorization: return_authorization, pre_tax_amount: pre_tax_amount_2) }
+
+ let(:pre_tax_amount_3) { 5.0 }
+ let!(:return_item_3) { create(:return_item, return_authorization: return_authorization, pre_tax_amount: pre_tax_amount_3) }
+
+ it "sums it's associated return_item's pre-tax amounts" do
+ expect(subject).to eq (pre_tax_amount_1 + pre_tax_amount_2 + pre_tax_amount_3)
+ end
+ end
+
+ describe '#display_pre_tax_total' do
+ it 'returns a Spree::Money' do
+ allow(return_authorization).to receive_messages(pre_tax_total: 21.22)
+ expect(return_authorization.display_pre_tax_total).to eq(Spree::Money.new(21.22))
+ end
+ end
+
+ describe '#refundable_amount' do
+ subject { return_authorization.refundable_amount }
+
+ let(:weighted_line_item_pre_tax_amount) { 5.0 }
+ let(:line_item_count) { return_authorization.order.line_items.count }
+
+ before do
+ return_authorization.order.line_items.update_all(pre_tax_amount: weighted_line_item_pre_tax_amount)
+ return_authorization.order.update_attribute(:promo_total, promo_total)
+ end
+
+ context 'no promotions' do
+ let(:promo_total) { 0.0 }
+
+ it 'returns the pre-tax line item total' do
+ expect(subject).to eq (weighted_line_item_pre_tax_amount * line_item_count)
+ end
+ end
+
+ context 'promotions' do
+ let(:promo_total) { -10.0 }
+
+ it 'returns the pre-tax line item total minus the order level promotion value' do
+ expect(subject).to eq (weighted_line_item_pre_tax_amount * line_item_count) + promo_total
+ end
+ end
+ end
+
+ describe '#customer_returned_items?' do
+ subject { return_authorization.customer_returned_items? }
+
+ before do
+ allow_any_instance_of(Spree::Order).to receive_messages(return!: true)
+ end
+
+ context 'has associated customer returns' do
+ let(:customer_return) { create(:customer_return) }
+ let(:return_authorization) { customer_return.return_authorizations.first }
+
+ it 'returns true' do
+ expect(subject).to eq true
+ end
+ end
+
+ context 'does not have associated customer returns' do
+ let(:return_authorization) { create(:return_authorization) }
+
+ it 'returns false' do
+ expect(subject).to eq false
+ end
+ end
+ end
+
+ describe 'cancel_return_items' do
+ subject do
+ return_authorization.cancel!
+ end
+
+ let(:return_authorization) { create(:return_authorization, return_items: return_items) }
+ let(:return_items) { [return_item] }
+ let(:return_item) { create(:return_item) }
+
+ it 'cancels the associated return items' do
+ subject
+ expect(return_item.reception_status).to eq 'cancelled'
+ end
+
+ context 'some return items cannot be cancelled' do
+ let(:return_items) { [return_item, return_item_2] }
+ let(:return_item_2) { create(:return_item, reception_status: 'received') }
+
+ it 'cancels those that can be cancelled' do
+ subject
+ expect(return_item.reception_status).to eq 'cancelled'
+ expect(return_item_2.reception_status).to eq 'received'
+ end
+ end
+ end
+
+ describe '#can_cancel?' do
+ subject { create(:return_authorization, return_items: return_items).can_cancel? }
+
+ let(:return_items) { [return_item_1, return_item_2] }
+ let(:return_item_1) { create(:return_item) }
+ let(:return_item_2) { create(:return_item) }
+
+ context 'all items can be cancelled' do
+ it 'returns true' do
+ expect(subject).to eq true
+ end
+ end
+
+ context 'at least one return item can be cancelled' do
+ let(:return_item_2) { create(:return_item, reception_status: 'received') }
+
+ it { is_expected.to eq true }
+ end
+
+ context 'no items can be cancelled' do
+ let(:return_item_1) { create(:return_item, reception_status: 'received') }
+ let(:return_item_2) { create(:return_item, reception_status: 'received') }
+
+ it { is_expected.to eq false }
+ end
+
+ context 'when return_authorization has no return_items' do
+ let(:return_items) { [] }
+
+ it { is_expected.to eq true }
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_item/eligibility_validator/default_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/default_spec.rb
new file mode 100644
index 00000000000..5d81e1eea00
--- /dev/null
+++ b/core/spec/models/spree/return_item/eligibility_validator/default_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Spree::ReturnItem::EligibilityValidator::Default, type: :model do
+ let(:return_item) { create(:return_item) }
+ let(:validator) { Spree::ReturnItem::EligibilityValidator::Default.new(return_item) }
+
+ let(:time_eligibility_class) { double('TimeEligibilityValidatorClass') }
+ let(:rma_eligibility_class) { double('RMAEligibilityValidatorClass') }
+
+ let(:time_eligibility_instance) { double(errors: time_error) }
+ let(:rma_eligibility_instance) { double(errors: rma_error) }
+
+ let(:time_error) { {} }
+ let(:rma_error) { {} }
+
+ before do
+ validator.permitted_eligibility_validators = [time_eligibility_class, rma_eligibility_class]
+
+ expect(time_eligibility_class).to receive(:new).and_return(time_eligibility_instance)
+ expect(rma_eligibility_class).to receive(:new).and_return(rma_eligibility_instance)
+ end
+
+ describe '#eligible_for_return?' do
+ subject { validator.eligible_for_return? }
+
+ it 'checks that all permitted eligibility validators are eligible for return' do
+ expect(time_eligibility_instance).to receive(:eligible_for_return?).and_return(true)
+ expect(rma_eligibility_instance).to receive(:eligible_for_return?).and_return(true)
+
+ expect(subject).to be true
+ end
+ end
+
+ describe '#requires_manual_intervention?' do
+ subject { validator.requires_manual_intervention? }
+
+ context 'any of the permitted eligibility validators require manual intervention' do
+ it 'returns true' do
+ expect(time_eligibility_instance).to receive(:requires_manual_intervention?).and_return(false)
+ expect(rma_eligibility_instance).to receive(:requires_manual_intervention?).and_return(true)
+
+ expect(subject).to be true
+ end
+ end
+
+ context 'no permitted eligibility validators require manual intervention' do
+ it 'returns false' do
+ expect(time_eligibility_instance).to receive(:requires_manual_intervention?).and_return(false)
+ expect(rma_eligibility_instance).to receive(:requires_manual_intervention?).and_return(false)
+
+ expect(subject).to be false
+ end
+ end
+ end
+
+ describe '#errors' do
+ subject { validator.errors }
+
+ context 'the validator errors are empty' do
+ it 'returns an empty hash' do
+ expect(subject).to eq({})
+ end
+ end
+
+ context 'the validators have errors' do
+ let(:time_error) { { time: time_error_text } }
+ let(:rma_error) { { rma: rma_error_text } }
+
+ let(:time_error_text) { 'Time eligibility error' }
+ let(:rma_error_text) { 'RMA eligibility error' }
+
+ it 'gathers all errors from permitted eligibility validators into a single errors hash' do
+ expect(subject).to eq(time: time_error_text, rma: rma_error_text)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_item/eligibility_validator/inventory_shipped_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/inventory_shipped_spec.rb
new file mode 100644
index 00000000000..9004a3fcc6d
--- /dev/null
+++ b/core/spec/models/spree/return_item/eligibility_validator/inventory_shipped_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Spree::ReturnItem::EligibilityValidator::InventoryShipped do
+ let(:return_item) { create(:return_item) }
+ let(:validator) { described_class.new(return_item) }
+
+ describe '#eligible_for_return?' do
+ subject { validator.eligible_for_return? }
+
+ before { allow(return_item.inventory_unit).to receive(:shipped?).and_return(true) }
+
+ context 'the associated inventory unit is shipped' do
+ it 'returns true' do
+ expect(subject).to eq true
+ end
+ end
+
+ context 'the associated inventory unit is not shipped' do
+ before { allow(return_item.inventory_unit).to receive(:shipped?).and_return(false) }
+
+ it 'returns false' do
+ expect(subject).to eq false
+ end
+
+ it 'sets an error' do
+ subject
+ expect(validator.errors[:inventory_unit_shipped]).to eq Spree.t('return_item_inventory_unit_ineligible')
+ end
+ end
+ end
+
+ describe '#requires_manual_intervention?' do
+ subject { validator.requires_manual_intervention? }
+
+ context 'not eligible for return' do
+ before do
+ allow(return_item.inventory_unit).to receive(:shipped?).and_return(false)
+ validator.eligible_for_return?
+ end
+
+ it 'returns true if errors were added' do
+ expect(subject).to eq true
+ end
+ end
+
+ context 'eligible for return' do
+ before do
+ allow(return_item.inventory_unit).to receive(:shipped?).and_return(true)
+ validator.eligible_for_return?
+ end
+
+ it 'returns false if no errors were added' do
+ expect(subject).to eq false
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_item/eligibility_validator/no_reimbursements_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/no_reimbursements_spec.rb
new file mode 100644
index 00000000000..eba3e01e36d
--- /dev/null
+++ b/core/spec/models/spree/return_item/eligibility_validator/no_reimbursements_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe Spree::ReturnItem::EligibilityValidator::NoReimbursements do
+ let(:validator) { described_class.new(return_item) }
+
+ describe '#eligible_for_return?' do
+ subject { validator.eligible_for_return? }
+
+ context 'inventory unit has already been reimbursed' do
+ let(:reimbursement) { create(:reimbursement) }
+ let(:return_item) { reimbursement.return_items.last }
+
+ it 'returns false' do
+ expect(subject).to eq false
+ end
+
+ it 'sets an error' do
+ subject
+ expect(validator.errors[:inventory_unit_reimbursed]).to eq Spree.t('return_item_inventory_unit_reimbursed')
+ end
+ end
+
+ context 'inventory unit has not been reimbursed' do
+ let(:return_item) { create(:return_item) }
+
+ it 'returns true' do
+ expect(subject).to eq true
+ end
+ end
+ end
+
+ describe '#requires_manual_intervention?' do
+ subject { validator.requires_manual_intervention? }
+
+ context 'not eligible for return' do
+ let(:reimbursement) { create(:reimbursement) }
+ let(:return_item) { reimbursement.return_items.last }
+
+ before do
+ validator.eligible_for_return?
+ end
+
+ it 'returns true if errors were added' do
+ expect(subject).to eq true
+ end
+ end
+
+ context 'eligible for return' do
+ let(:return_item) { create(:return_item) }
+
+ before do
+ validator.eligible_for_return?
+ end
+
+ it 'returns false if no errors were added' do
+ expect(subject).to eq false
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_item/eligibility_validator/order_completed_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/order_completed_spec.rb
new file mode 100644
index 00000000000..8779d748818
--- /dev/null
+++ b/core/spec/models/spree/return_item/eligibility_validator/order_completed_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Spree::ReturnItem::EligibilityValidator::OrderCompleted do
+ let(:inventory_unit) { create(:inventory_unit, order: order) }
+ let(:return_item) { create(:return_item, inventory_unit: inventory_unit) }
+ let(:validator) { described_class.new(return_item) }
+
+ describe '#eligible_for_return?' do
+ subject { validator.eligible_for_return? }
+
+ context 'the order was completed' do
+ let(:order) { create(:completed_order_with_totals) }
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'the order is not completed' do
+ let(:order) { create(:order) }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'sets an error' do
+ subject
+ expect(validator.errors[:order_not_completed]).to eq Spree.t('return_item_order_not_completed')
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_item/eligibility_validator/rma_required_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/rma_required_spec.rb
new file mode 100644
index 00000000000..74b81f021f9
--- /dev/null
+++ b/core/spec/models/spree/return_item/eligibility_validator/rma_required_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Spree::ReturnItem::EligibilityValidator::RMARequired, type: :model do
+ let(:return_item) { create(:return_item) }
+ let(:validator) { Spree::ReturnItem::EligibilityValidator::RMARequired.new(return_item) }
+
+ describe '#eligible_for_return?' do
+ subject { validator.eligible_for_return? }
+
+ context 'there is an rma on the return item' do
+ it 'returns true' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'there is no rma on the return item' do
+ before { allow(return_item).to receive(:return_authorization).and_return(nil) }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'sets an error' do
+ subject
+ expect(validator.errors[:rma_required]).to eq Spree.t('return_item_rma_ineligible')
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_item/eligibility_validator/time_since_purchase_spec.rb b/core/spec/models/spree/return_item/eligibility_validator/time_since_purchase_spec.rb
new file mode 100644
index 00000000000..155cc3d59b9
--- /dev/null
+++ b/core/spec/models/spree/return_item/eligibility_validator/time_since_purchase_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Spree::ReturnItem::EligibilityValidator::TimeSincePurchase, type: :model do
+ let(:inventory_unit) { create(:inventory_unit, order: create(:shipped_order)) }
+ let(:return_item) { create(:return_item, inventory_unit: inventory_unit) }
+ let(:validator) { Spree::ReturnItem::EligibilityValidator::TimeSincePurchase.new(return_item) }
+
+ describe '#eligible_for_return?' do
+ subject { validator.eligible_for_return? }
+
+ context 'it is within the return timeframe' do
+ it 'returns true' do
+ completed_at = return_item.inventory_unit.order.completed_at - (Spree::Config[:return_eligibility_number_of_days].days / 2)
+ return_item.inventory_unit.order.update_attributes(completed_at: completed_at)
+ expect(subject).to be true
+ end
+ end
+
+ context 'it is past the return timeframe' do
+ before do
+ completed_at = return_item.inventory_unit.order.completed_at - Spree::Config[:return_eligibility_number_of_days].days - 1.day
+ return_item.inventory_unit.order.update_attributes(completed_at: completed_at)
+ end
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'sets an error' do
+ subject
+ expect(validator.errors[:number_of_days]).to eq Spree.t('return_item_time_period_ineligible')
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_item/exchange_variant_eligibility/same_option_value_spec.rb b/core/spec/models/spree/return_item/exchange_variant_eligibility/same_option_value_spec.rb
new file mode 100644
index 00000000000..fc60942de86
--- /dev/null
+++ b/core/spec/models/spree/return_item/exchange_variant_eligibility/same_option_value_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+module Spree
+ module ReturnItem::ExchangeVariantEligibility
+ describe SameOptionValue, type: :model do
+ describe '.eligible_variants' do
+ subject { SameOptionValue.eligible_variants(variant.reload) }
+
+ let(:color_option_type) { create(:option_type, name: 'color') }
+ let(:waist_option_type) { create(:option_type, name: 'waist') }
+ let(:inseam_option_type) { create(:option_type, name: 'inseam') }
+
+ let(:blue_option_value) { create(:option_value, name: 'blue', option_type: color_option_type) }
+ let(:red_option_value) { create(:option_value, name: 'red', option_type: color_option_type) }
+
+ let(:three_two_waist_option_value) { create(:option_value, name: 32, option_type: waist_option_type) }
+ let(:three_four_waist_option_value) { create(:option_value, name: 34, option_type: waist_option_type) }
+
+ let(:three_zero_inseam_option_value) { create(:option_value, name: 30, option_type: inseam_option_type) }
+ let(:three_one_inseam_option_value) { create(:option_value, name: 31, option_type: inseam_option_type) }
+
+ let(:product) { create(:product, option_types: [color_option_type, waist_option_type, inseam_option_type]) }
+
+ let!(:variant) { create(:variant, product: product, option_values: [blue_option_value, three_two_waist_option_value, three_zero_inseam_option_value]) }
+ let!(:same_option_values_variant) { create(:variant, product: product, option_values: [blue_option_value, three_two_waist_option_value, three_one_inseam_option_value]) }
+ let!(:different_color_option_value_variant) { create(:variant, product: product, option_values: [red_option_value, three_two_waist_option_value, three_one_inseam_option_value]) }
+ let!(:different_waist_option_value_variant) { create(:variant, product: product, option_values: [blue_option_value, three_four_waist_option_value, three_one_inseam_option_value]) }
+
+ before do
+ @original_option_type_restrictions = SameOptionValue.option_type_restrictions
+ SameOptionValue.option_type_restrictions = ['color', 'waist']
+ end
+
+ after { SameOptionValue.option_type_restrictions = @original_option_type_restrictions }
+
+ it 'returns all other variants for the same product with the same option value for the specified option type' do
+ Spree::StockItem.update_all(count_on_hand: 10)
+
+ expect(subject.sort).to eq [variant, same_option_values_variant].sort
+ end
+
+ it 'does not return variants for another product' do
+ other_product_variant = create(:variant)
+ expect(subject).not_to include other_product_variant
+ end
+
+ context 'no option value restrictions are specified' do
+ before do
+ @original_option_type_restrictions = SameOptionValue.option_type_restrictions
+ SameOptionValue.option_type_restrictions = []
+ end
+
+ after { SameOptionValue.option_type_restrictions = @original_option_type_restrictions }
+
+ it 'returns all variants for the product' do
+ Spree::StockItem.update_all(count_on_hand: 10)
+
+ expect(subject.sort).to eq [variant, same_option_values_variant, different_waist_option_value_variant, different_color_option_value_variant].sort
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_item/exchange_variant_eligibility/same_product_spec.rb b/core/spec/models/spree/return_item/exchange_variant_eligibility/same_product_spec.rb
new file mode 100644
index 00000000000..48fe215a31e
--- /dev/null
+++ b/core/spec/models/spree/return_item/exchange_variant_eligibility/same_product_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+module Spree
+ module ReturnItem::ExchangeVariantEligibility
+ describe SameProduct, type: :model do
+ describe '.eligible_variants' do
+ context 'product has no variants' do
+ it 'returns the master variant for the same product' do
+ product = create(:product)
+ product.master.stock_items.first.update_column(:count_on_hand, 10)
+
+ expect(SameProduct.eligible_variants(product.master)).to eq [product.master]
+ end
+ end
+
+ context 'product has variants' do
+ it 'returns all variants for the same product' do
+ product = create(:product, variants: Array.new(3) { create(:variant) })
+ product.variants.map { |v| v.stock_items.first.update_column(:count_on_hand, 10) }
+
+ expect(SameProduct.eligible_variants(product.variants.first).sort).to eq product.variants.sort
+ end
+ end
+
+ it 'does not return variants for another product' do
+ variant = create(:variant)
+ other_product_variant = create(:variant)
+ expect(SameProduct.eligible_variants(variant)).not_to include other_product_variant
+ end
+
+ it 'only returns variants that are on hand or backorderable' do
+ product = create(:product, variants: Array.new(3) { create(:variant) })
+ in_stock_variant = product.variants.first
+ backorderable_variant = product.variants.second
+ not_backorderable_variant = product.variants.third
+
+ in_stock_variant.stock_items.first.update_column(:count_on_hand, 10)
+ not_backorderable_variant.stock_items.first.update_column(:backorderable, false)
+
+ expect(SameProduct.eligible_variants(in_stock_variant)).to include(in_stock_variant)
+ expect(SameProduct.eligible_variants(in_stock_variant)).to include(backorderable_variant)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/return_item_spec.rb b/core/spec/models/spree/return_item_spec.rb
new file mode 100644
index 00000000000..52790aaf31c
--- /dev/null
+++ b/core/spec/models/spree/return_item_spec.rb
@@ -0,0 +1,758 @@
+require 'spec_helper'
+
+shared_examples 'an invalid state transition' do |status, expected_status|
+ let(:status) { status }
+
+ it "cannot transition to #{expected_status}" do
+ expect { subject }.to raise_error(StateMachines::InvalidTransition)
+ end
+end
+
+describe Spree::ReturnItem, type: :model do
+ all_reception_statuses = Spree::ReturnItem.state_machines[:reception_status].states.map(&:name).map(&:to_s)
+ all_acceptance_statuses = Spree::ReturnItem.state_machines[:acceptance_status].states.map(&:name).map(&:to_s)
+
+ before do
+ allow_any_instance_of(Spree::Order).to receive_messages(return!: true)
+ end
+
+ describe '#receive!' do
+ subject { return_item.receive! }
+
+ let(:now) { Time.current }
+ let(:inventory_unit) { create(:inventory_unit, state: 'shipped') }
+ let(:return_item) { create(:return_item, inventory_unit: inventory_unit) }
+
+ before do
+ inventory_unit.update_attributes!(state: 'shipped')
+ return_item.update_attributes!(reception_status: 'awaiting')
+ allow(return_item).to receive(:eligible_for_return?).and_return(true)
+ end
+
+ it 'returns the inventory unit' do
+ subject
+ expect(inventory_unit.reload.state).to eq 'returned'
+ end
+
+ it 'attempts to accept the return item' do
+ expect(return_item).to receive(:attempt_accept)
+ subject
+ end
+
+ context 'with a stock location' do
+ let(:stock_item) { inventory_unit.find_stock_item }
+ let!(:customer_return) { create(:customer_return_without_return_items, return_items: [return_item], stock_location_id: inventory_unit.shipment.stock_location_id) }
+
+ before do
+ inventory_unit.update_attributes!(state: 'shipped')
+ return_item.update_attributes!(reception_status: 'awaiting')
+ end
+
+ it 'increases the count on hand' do
+ expect { subject }.to change { stock_item.reload.count_on_hand }.by(1)
+ end
+
+ context 'when the variant is not resellable' do
+ before { return_item.update_attributes(resellable: false) }
+
+ it { expect { subject }.not_to change { stock_item.reload.count_on_hand } }
+ end
+
+ context 'when variant does not track inventory' do
+ before do
+ inventory_unit.update_attributes!(state: 'shipped')
+ inventory_unit.variant.update_attributes!(track_inventory: false)
+ return_item.update_attributes!(reception_status: 'awaiting')
+ end
+
+ it 'does not increase the count on hand' do
+ expect { subject }.not_to change { stock_item.reload.count_on_hand }
+ end
+ end
+
+ context 'when the restock_inventory preference is false' do
+ before do
+ Spree::Config[:restock_inventory] = false
+ end
+
+ it 'does not increase the count on hand' do
+ expect { subject }.not_to change { stock_item.reload.count_on_hand }
+ end
+ end
+ end
+ end
+
+ describe '#display_pre_tax_amount' do
+ let(:pre_tax_amount) { 21.22 }
+ let(:return_item) { build(:return_item, pre_tax_amount: pre_tax_amount) }
+
+ it 'returns a Spree::Money' do
+ expect(return_item.display_pre_tax_amount).to eq(Spree::Money.new(pre_tax_amount))
+ end
+ end
+
+ describe '.default_refund_amount_calculator' do
+ it 'defaults to the default refund amount calculator' do
+ expect(Spree::ReturnItem.refund_amount_calculator).to eq Spree::Calculator::Returns::DefaultRefundAmount
+ end
+ end
+
+ describe 'pre_tax_amount calculations on create' do
+ let(:inventory_unit) { build(:inventory_unit) }
+
+ before { subject.save! }
+
+ context 'pre tax amount is not specified' do
+ subject { build(:return_item, inventory_unit: inventory_unit) }
+
+ context 'not an exchange' do
+ it { expect(subject.pre_tax_amount).to eq Spree::Calculator::Returns::DefaultRefundAmount.new.compute(subject) }
+ end
+
+ context 'an exchange' do
+ subject { build(:exchange_return_item) }
+
+ it { expect(subject.pre_tax_amount).to eq 0.0 }
+ end
+ end
+
+ context 'pre tax amount is specified' do
+ subject { build(:return_item, inventory_unit: inventory_unit, pre_tax_amount: 100) }
+
+ it { expect(subject.pre_tax_amount).to eq 100 }
+ end
+ end
+
+ describe '.from_inventory_unit' do
+ subject { Spree::ReturnItem.from_inventory_unit(inventory_unit) }
+
+ let(:inventory_unit) { build(:inventory_unit) }
+
+ context 'with a cancelled return item' do
+ let!(:return_item) { create(:return_item, inventory_unit: inventory_unit, reception_status: 'cancelled') }
+
+ it { is_expected.not_to be_persisted }
+ end
+
+ context 'with a non-cancelled return item' do
+ let!(:return_item) { create(:return_item, inventory_unit: inventory_unit) }
+
+ it { is_expected.to be_persisted }
+ end
+ end
+
+ describe 'reception_status state_machine' do
+ subject(:return_item) { create(:return_item) }
+
+ it 'starts off in the awaiting state' do
+ expect(return_item).to be_awaiting
+ end
+ end
+
+ describe 'acceptance_status state_machine' do
+ subject(:return_item) { create(:return_item) }
+
+ it 'starts off in the pending state' do
+ expect(return_item).to be_pending
+ end
+ end
+
+ describe '#receive' do
+ subject { return_item.receive! }
+
+ let(:inventory_unit) { create(:inventory_unit, order: create(:shipped_order)) }
+ let(:return_item) { create(:return_item, reception_status: status, inventory_unit: inventory_unit) }
+
+ context 'awaiting status' do
+ let(:status) { 'awaiting' }
+
+ before do
+ expect(return_item.inventory_unit).to receive(:return!)
+ subject
+ end
+
+ it 'transitions successfully' do
+ expect(return_item).to be_received
+ end
+ end
+
+ (all_reception_statuses - ['awaiting']).each do |invalid_transition_status|
+ context "return_item has a reception status of #{invalid_transition_status}" do
+ it_behaves_like 'an invalid state transition', invalid_transition_status, 'received'
+ end
+ end
+ end
+
+ describe '#cancel' do
+ subject { return_item.cancel! }
+
+ let(:return_item) { create(:return_item, reception_status: status) }
+
+ context 'awaiting status' do
+ let(:status) { 'awaiting' }
+
+ before { subject }
+
+ it 'transitions successfully' do
+ expect(return_item).to be_cancelled
+ end
+ end
+
+ (all_reception_statuses - ['awaiting']).each do |invalid_transition_status|
+ context "return_item has a reception status of #{invalid_transition_status}" do
+ it_behaves_like 'an invalid state transition', invalid_transition_status, 'cancelled'
+ end
+ end
+ end
+
+ describe '#give' do
+ subject { return_item.give! }
+
+ let(:return_item) { create(:return_item, reception_status: status) }
+
+ context 'awaiting status' do
+ let(:status) { 'awaiting' }
+
+ before { subject }
+
+ it 'transitions successfully' do
+ expect(return_item).to be_given_to_customer
+ end
+ end
+
+ (all_reception_statuses - ['awaiting']).each do |invalid_transition_status|
+ context "return_item has a reception status of #{invalid_transition_status}" do
+ it_behaves_like 'an invalid state transition', invalid_transition_status, 'give_to_customer'
+ end
+ end
+ end
+
+ describe '#attempt_accept' do
+ subject { return_item.attempt_accept! }
+
+ let(:return_item) { create(:return_item, acceptance_status: status) }
+ let(:validator_errors) { {} }
+ let(:validator_double) { double(errors: validator_errors) }
+
+ before do
+ allow(return_item).to receive(:validator).and_return(validator_double)
+ end
+
+ context 'pending status' do
+ let(:status) { 'pending' }
+
+ before do
+ allow(return_item).to receive(:eligible_for_return?).and_return(true)
+ subject
+ end
+
+ it 'transitions successfully' do
+ expect(return_item).to be_accepted
+ end
+
+ it 'has no acceptance status errors' do
+ expect(return_item.acceptance_status_errors).to be_empty
+ end
+ end
+
+ (all_acceptance_statuses - ['accepted', 'pending']).each do |invalid_transition_status|
+ context "return_item has an acceptance status of #{invalid_transition_status}" do
+ it_behaves_like 'an invalid state transition', invalid_transition_status, 'accepted'
+ end
+ end
+
+ context 'not eligible for return' do
+ let(:status) { 'pending' }
+ let(:validator_errors) { { number_of_days: 'Return Item is outside the eligible time period' } }
+
+ before do
+ allow(return_item).to receive(:eligible_for_return?).and_return(false)
+ end
+
+ context 'manual intervention required' do
+ before do
+ allow(return_item).to receive(:requires_manual_intervention?).and_return(true)
+ subject
+ end
+
+ it 'transitions to manual intervention required' do
+ expect(return_item).to be_manual_intervention_required
+ end
+
+ it 'sets the acceptance status errors' do
+ expect(return_item.acceptance_status_errors).to eq validator_errors
+ end
+ end
+
+ context 'manual intervention not required' do
+ before do
+ allow(return_item).to receive(:requires_manual_intervention?).and_return(false)
+ subject
+ end
+
+ it 'transitions to rejected' do
+ expect(return_item).to be_rejected
+ end
+
+ it 'sets the acceptance status errors' do
+ expect(return_item.acceptance_status_errors).to eq validator_errors
+ end
+ end
+ end
+ end
+
+ describe '#reject' do
+ subject { return_item.reject! }
+
+ let(:return_item) { create(:return_item, acceptance_status: status) }
+
+ context 'pending status' do
+ let(:status) { 'pending' }
+
+ before { subject }
+
+ it 'transitions successfully' do
+ expect(return_item).to be_rejected
+ end
+
+ it 'has no acceptance status errors' do
+ expect(return_item.acceptance_status_errors).to be_empty
+ end
+ end
+
+ (all_acceptance_statuses - ['accepted', 'pending', 'manual_intervention_required']).each do |invalid_transition_status|
+ context "return_item has an acceptance status of #{invalid_transition_status}" do
+ it_behaves_like 'an invalid state transition', invalid_transition_status, 'rejected'
+ end
+ end
+ end
+
+ describe '#accept' do
+ subject { return_item.accept! }
+
+ let(:return_item) { create(:return_item, acceptance_status: status) }
+
+ context 'pending status' do
+ let(:status) { 'pending' }
+
+ before { subject }
+
+ it 'transitions successfully' do
+ expect(return_item).to be_accepted
+ end
+
+ it 'has no acceptance status errors' do
+ expect(return_item.acceptance_status_errors).to be_empty
+ end
+ end
+
+ (all_acceptance_statuses - ['accepted', 'pending', 'manual_intervention_required']).each do |invalid_transition_status|
+ context "return_item has an acceptance status of #{invalid_transition_status}" do
+ it_behaves_like 'an invalid state transition', invalid_transition_status, 'accepted'
+ end
+ end
+ end
+
+ describe '#require_manual_intervention' do
+ subject { return_item.require_manual_intervention! }
+
+ let(:return_item) { create(:return_item, acceptance_status: status) }
+
+ context 'pending status' do
+ let(:status) { 'pending' }
+
+ before { subject }
+
+ it 'transitions successfully' do
+ expect(return_item).to be_manual_intervention_required
+ end
+
+ it 'has no acceptance status errors' do
+ expect(return_item.acceptance_status_errors).to be_empty
+ end
+ end
+
+ (all_acceptance_statuses - ['accepted', 'pending', 'manual_intervention_required']).each do |invalid_transition_status|
+ context "return_item has an acceptance status of #{invalid_transition_status}" do
+ it_behaves_like 'an invalid state transition', invalid_transition_status, 'manual_intervention_required'
+ end
+ end
+ end
+
+ describe 'validity for reimbursements' do
+ subject { return_item }
+
+ let(:return_item) { create(:return_item, acceptance_status: acceptance_status) }
+ let(:acceptance_status) { 'pending' }
+
+ before { return_item.reimbursement = build(:reimbursement) }
+
+ context 'when acceptance_status is accepted' do
+ let(:acceptance_status) { 'accepted' }
+
+ it 'is valid' do
+ expect(subject).to be_valid
+ end
+ end
+
+ context 'when acceptance_status is accepted' do
+ let(:acceptance_status) { 'pending' }
+
+ it 'is valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages).to eq(reimbursement: [I18n.t(:cannot_be_associated_unless_accepted, scope: 'activerecord.errors.models.spree/return_item.attributes.reimbursement')])
+ end
+ end
+ end
+
+ describe '#exchange_requested?' do
+ context 'exchange variant exists' do
+ before { allow(subject).to receive(:exchange_variant) { mock_model(Spree::Variant) } }
+
+ it { expect(subject.exchange_requested?).to eq true }
+ end
+
+ context 'exchange variant does not exist' do
+ before { allow(subject).to receive(:exchange_variant).and_return(nil) }
+
+ it { expect(subject.exchange_requested?).to eq false }
+ end
+ end
+
+ describe '#exchange_processed?' do
+ context 'exchange inventory unit exists' do
+ before { allow(subject).to receive(:exchange_inventory_units) { [mock_model(Spree::InventoryUnit)] } }
+
+ it { expect(subject.exchange_processed?).to eq true }
+ end
+
+ context 'exchange inventory unit does not exist' do
+ before { allow(subject).to receive(:exchange_inventory_units).and_return([]) }
+
+ it { expect(subject.exchange_processed?).to eq false }
+ end
+ end
+
+ describe '#exchange_required?' do
+ context 'exchange has been requested and not yet processed' do
+ before do
+ allow(subject).to receive(:exchange_requested?).and_return(true)
+ allow(subject).to receive(:exchange_processed?).and_return(false)
+ end
+
+ it { expect(subject.exchange_required?).to be true }
+ end
+
+ context 'exchange has not been requested' do
+ before { allow(subject).to receive(:exchange_requested?).and_return(false) }
+
+ it { expect(subject.exchange_required?).to be false }
+ end
+
+ context 'exchange has been requested and processed' do
+ before do
+ allow(subject).to receive(:exchange_requested?).and_return(true)
+ allow(subject).to receive(:exchange_processed?).and_return(true)
+ end
+
+ it { expect(subject.exchange_required?).to be false }
+ end
+ end
+
+ describe '#eligible_exchange_variants' do
+ it 'uses the exchange variant calculator to compute possible variants to exchange for' do
+ return_item = build(:return_item)
+ expect(Spree::ReturnItem.exchange_variant_engine).to receive(:eligible_variants).with(return_item.variant)
+ return_item.eligible_exchange_variants
+ end
+ end
+
+ describe '.exchange_variant_engine' do
+ it 'defaults to the same product calculator' do
+ expect(Spree::ReturnItem.exchange_variant_engine).to eq Spree::ReturnItem::ExchangeVariantEligibility::SameProduct
+ end
+ end
+
+ describe 'exchange pre_tax_amount' do
+ let(:return_item) { build(:return_item) }
+
+ context 'the return item is intended to be exchanged' do
+ before do
+ return_item.inventory_unit.variant.update_column(:track_inventory, false)
+ return_item.exchange_variant = return_item.inventory_unit.variant
+ end
+
+ it do
+ return_item.pre_tax_amount = 5.0
+ return_item.save!
+ expect(return_item.reload.pre_tax_amount).to eq 0.0
+ end
+ end
+
+ context 'the return item is not intended to be exchanged' do
+ it do
+ return_item.pre_tax_amount = 5.0
+ return_item.save!
+ expect(return_item.reload.pre_tax_amount).to eq 5.0
+ end
+ end
+ end
+
+ describe '#build_default_exchange_inventory_unit' do
+ subject { return_item.build_default_exchange_inventory_unit }
+
+ let(:return_item) { build(:return_item) }
+
+ context 'the return item is intended to be exchanged' do
+ before { allow(return_item).to receive(:exchange_variant).and_return(mock_model(Spree::Variant)) }
+
+ context 'an exchange inventory unit already exists' do
+ before do
+ allow(return_item).to receive(:exchange_inventory_units).and_return([mock_model(Spree::InventoryUnit)])
+ end
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'no exchange inventory unit exists' do
+ it 'builds a pending inventory unit with references to the return item, variant, and previous inventory unit' do
+ expect(subject.variant).to eq return_item.exchange_variant
+ expect(subject.pending).to eq true
+ expect(subject).not_to be_persisted
+ expect(subject.original_return_item).to eq return_item
+ expect(subject.line_item).to eq return_item.inventory_unit.line_item
+ expect(subject.order).to eq return_item.inventory_unit.order
+ end
+ end
+ end
+
+ context 'the return item is not intended to be exchanged' do
+ it { expect(subject).to be_nil }
+ end
+ end
+
+ describe '#exchange_shipments' do
+ it "returns the exchange inventory unit's shipment" do
+ inventory_unit = build(:inventory_unit)
+ subject.exchange_inventory_units << inventory_unit
+ expect(subject.exchange_shipments).to include inventory_unit.shipment
+ end
+ end
+
+ describe '#shipment' do
+ it "returns the inventory unit's shipment" do
+ inventory_unit = build(:inventory_unit)
+ subject.inventory_unit = inventory_unit
+ expect(subject.shipment).to eq inventory_unit.shipment
+ end
+ end
+
+ describe 'inventory_unit uniqueness' do
+ subject do
+ build(:return_item, return_authorization: old_return_item.return_authorization,
+ inventory_unit: old_return_item.inventory_unit)
+ end
+
+ let!(:old_return_item) { create(:return_item, reception_status: old_reception_status) }
+ let(:old_reception_status) { 'awaiting' }
+
+ context 'with other awaiting return items exist for the same inventory unit' do
+ let(:old_reception_status) { 'awaiting' }
+
+ it 'cancels the others' do
+ expect do
+ subject.save!
+ end.to change { old_return_item.reload.reception_status }.from('awaiting').to('cancelled')
+ end
+
+ it 'does not cancel itself' do
+ subject.save!
+ expect(subject).to be_awaiting
+ end
+ end
+
+ context 'with other cancelled return items exist for the same inventory unit' do
+ let(:old_reception_status) { 'cancelled' }
+
+ it 'succeeds' do
+ expect { subject.save! }.not_to raise_error
+ end
+ end
+
+ context 'with other received return items exist for the same inventory unit' do
+ let(:old_reception_status) { 'received' }
+
+ it 'is invalid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.to_a).to eq ["Inventory unit #{subject.inventory_unit_id} has already been taken by return item #{old_return_item.id}"]
+ end
+ end
+
+ context 'with other given_to_customer return items exist for the same inventory unit' do
+ let(:old_reception_status) { 'given_to_customer' }
+
+ it 'is invalid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.to_a).to eq ["Inventory unit #{subject.inventory_unit_id} has already been taken by return item #{old_return_item.id}"]
+ end
+ end
+ end
+
+ describe 'valid exchange variant' do
+ subject { return_item }
+
+ before { subject.save }
+
+ context "return item doesn't have an exchange variant" do
+ let(:return_item) { create(:return_item) }
+
+ it 'is valid' do
+ expect(subject).to be_valid
+ end
+ end
+
+ context 'return item has an exchange variant' do
+ let(:return_item) { create(:exchange_return_item) }
+ let(:exchange_variant) { create(:on_demand_variant, product: return_item.inventory_unit.variant.product) }
+
+ context 'the exchange variant is eligible' do
+ before { return_item.exchange_variant = exchange_variant }
+
+ it 'is valid' do
+ expect(subject).to be_valid
+ end
+ end
+
+ context 'the exchange variant is not eligible' do
+ context 'new return item' do
+ let(:return_item) { build(:return_item) }
+ let(:exchange_variant) { create(:variant, product: return_item.inventory_unit.variant.product) }
+
+ before do
+ exchange_variant.stock_items.each do |item|
+ item.update_column(:backorderable, false)
+ end
+ return_item.exchange_variant = exchange_variant
+ end
+
+ it 'is invalid' do
+ expect(subject).not_to be_valid
+ end
+
+ it 'adds an error message about the invalid exchange variant' do
+ subject.valid?
+ expect(subject.errors.to_a).to eq ['Invalid exchange variant.']
+ end
+ end
+
+ context 'the exchange variant has been updated' do
+ before do
+ other_variant = create(:variant)
+ return_item.exchange_variant_id = other_variant.id
+ subject.valid?
+ end
+
+ it 'is invalid' do
+ expect(subject).not_to be_valid
+ end
+
+ it 'adds an error message about the invalid exchange variant' do
+ expect(subject.errors.to_a).to eq ['Invalid exchange variant.']
+ end
+ end
+
+ context 'the exchange variant has not been updated' do
+ before do
+ other_variant = create(:variant)
+ return_item.update_column(:exchange_variant_id, other_variant.id)
+ return_item.reload
+ subject.valid?
+ end
+
+ it 'is valid' do
+ expect(subject).to be_valid
+ end
+ end
+ end
+ end
+ end
+
+ describe 'included tax in total' do
+ let(:inventory_unit) { create(:inventory_unit, state: 'shipped') }
+ let(:return_item) do
+ create(
+ :return_item,
+ inventory_unit: inventory_unit,
+ included_tax_total: 10
+ )
+ end
+
+ it 'includes included tax total' do
+ expect(return_item.pre_tax_amount).to eq 10
+ expect(return_item.included_tax_total).to eq 10
+ expect(return_item.total).to eq 20
+ end
+ end
+
+ describe '#process_inventory_unit!' do
+ subject { return_item.send(:process_inventory_unit!) }
+
+ let(:inventory_unit) { create(:inventory_unit, state: 'shipped') }
+ let(:return_item) { create(:return_item, inventory_unit: inventory_unit, reception_status: 'awaiting') }
+ let!(:stock_item) { inventory_unit.find_stock_item }
+
+ before { return_item.update_attributes!(reception_status: 'awaiting') }
+
+ it { expect { subject }.to change(inventory_unit, :state).to('returned').from('shipped') }
+
+ context 'stock should restock' do
+ let(:stock_movement_attributes) do
+ {
+ stock_item_id: stock_item.id,
+ quantity: 1,
+ originator: return_item.return_authorization
+ }
+ end
+
+ it { expect(subject).to eq(Spree::StockMovement.find_by(stock_movement_attributes)) }
+ end
+
+ context 'stock should not restock' do
+ context 'return_item is not resellable' do
+ before { return_item.resellable = false }
+
+ it { expect(subject).to be_nil }
+ it { expect { subject }.not_to change { stock_item.reload.count_on_hand } }
+ end
+
+ context 'variant should not track inventory' do
+ before { return_item.variant.track_inventory = false }
+
+ it { expect(subject).to be_nil }
+ it { expect { subject }.not_to change { stock_item.reload.count_on_hand } }
+ end
+
+ context 'stock_item not present' do
+ before { stock_item.destroy }
+
+ it { expect(subject).to be_nil }
+ it { expect { subject }.not_to change { stock_item.reload.count_on_hand } }
+ end
+
+ context 'when restock inventory preference false' do
+ before { Spree::Config[:restock_inventory] = false }
+
+ it { expect(subject).to be_nil }
+ it { expect { subject }.not_to change { stock_item.reload.count_on_hand } }
+ end
+ end
+
+ describe '#currency' do
+ subject { return_item }
+
+ it 'responds to currency method' do
+ expect(subject.respond_to?(:currency)).to eq true
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/returns_calculator_spec.rb b/core/spec/models/spree/returns_calculator_spec.rb
new file mode 100644
index 00000000000..cdad35a47c2
--- /dev/null
+++ b/core/spec/models/spree/returns_calculator_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+module Spree
+ describe ReturnsCalculator, type: :model do
+ subject { ReturnsCalculator.new }
+
+ let(:return_item) { build(:return_item) }
+
+ it 'compute_shipment must be overridden' do
+ expect do
+ subject.compute(return_item)
+ end.to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/core/spec/models/spree/shipment_spec.rb b/core/spec/models/spree/shipment_spec.rb
new file mode 100644
index 00000000000..09298f95053
--- /dev/null
+++ b/core/spec/models/spree/shipment_spec.rb
@@ -0,0 +1,826 @@
+require 'spec_helper'
+require 'benchmark'
+
+describe Spree::Shipment, type: :model do
+ let(:order) do
+ mock_model Spree::Order, backordered?: false,
+ canceled?: false,
+ can_ship?: true,
+ currency: 'USD',
+ number: 'S12345',
+ paid?: false,
+ touch_later: false
+ end
+ let(:shipping_method) { create(:shipping_method, name: 'UPS') }
+ let(:shipment) do
+ shipment = Spree::Shipment.new(cost: 1, state: 'pending', stock_location: create(:stock_location))
+ allow(shipment).to receive_messages order: order
+ allow(shipment).to receive_messages shipping_method: shipping_method
+ shipment.save
+ shipment
+ end
+
+ let(:variant) { mock_model(Spree::Variant) }
+ let(:line_item) { mock_model(Spree::LineItem, variant: variant) }
+
+ def create_shipment(order, stock_location)
+ order.shipments.create(stock_location_id: stock_location.id).inventory_units.create(
+ order_id: order.id,
+ variant_id: order.line_items.first.variant_id,
+ line_item_id: order.line_items.first.id
+ )
+ end
+
+ describe 'precision of pre_tax_amount' do
+ let(:line_item) { create :line_item, pre_tax_amount: 4.2051 }
+
+ it 'keeps four digits of precision even when reloading' do
+ # prevent it from updating pre_tax_amount
+ allow_any_instance_of(Spree::LineItem).to receive(:update_tax_charge)
+ expect(line_item.reload.pre_tax_amount).to eq(4.2051)
+ end
+ end
+
+ # Regression test for #4063
+ context 'number generation' do
+ before do
+ allow(order).to receive :update_with_updater!
+ end
+
+ it 'generates a number containing a letter + 11 numbers' do
+ expect(shipment.number[0]).to eq('H')
+ expect(/\d{11}/.match(shipment.number)).not_to be_nil
+ expect(shipment.number.length).to eq(12)
+ end
+ end
+
+ it 'is backordered if one if its inventory_units is backordered' do
+ allow(shipment).to receive_messages(inventory_units: [
+ mock_model(Spree::InventoryUnit, backordered?: false),
+ mock_model(Spree::InventoryUnit, backordered?: true)
+ ])
+ expect(shipment).to be_backordered
+ end
+
+ context '#determine_state' do
+ it 'returns canceled if order is canceled?' do
+ allow(order).to receive_messages canceled?: true
+ expect(shipment.determine_state(order)).to eq 'canceled'
+ end
+
+ it 'returns pending unless order.can_ship?' do
+ allow(order).to receive_messages can_ship?: false
+ expect(shipment.determine_state(order)).to eq 'pending'
+ end
+
+ it 'returns pending if backordered' do
+ allow(shipment).to receive_messages inventory_units: [mock_model(Spree::InventoryUnit, backordered?: true)]
+ expect(shipment.determine_state(order)).to eq 'pending'
+ end
+
+ it 'returns shipped when already shipped' do
+ allow(shipment).to receive_messages state: 'shipped'
+ expect(shipment.determine_state(order)).to eq 'shipped'
+ end
+
+ it 'returns pending when unpaid' do
+ expect(shipment.determine_state(order)).to eq 'pending'
+ end
+
+ it 'returns ready when paid' do
+ allow(order).to receive_messages paid?: true
+ expect(shipment.determine_state(order)).to eq 'ready'
+ end
+
+ it 'returns ready when Config.auto_capture_on_dispatch' do
+ Spree::Config.auto_capture_on_dispatch = true
+ expect(shipment.determine_state(order)).to eq 'ready'
+ end
+ end
+
+ context 'display_amount' do
+ it 'retuns a Spree::Money' do
+ allow(shipment).to receive(:cost).and_return(21.22)
+ expect(shipment.display_amount).to eq(Spree::Money.new(21.22))
+ end
+ end
+
+ context 'display_final_price' do
+ it 'retuns a Spree::Money' do
+ allow(shipment).to receive(:final_price).and_return(21.22)
+ expect(shipment.display_final_price).to eq(Spree::Money.new(21.22))
+ end
+ end
+
+ context 'display_item_cost' do
+ it 'retuns a Spree::Money' do
+ allow(shipment).to receive(:item_cost).and_return(21.22)
+ expect(shipment.display_item_cost).to eq(Spree::Money.new(21.22))
+ end
+ end
+
+ context '#item_cost' do
+ it 'equals shipment line items amount with tax' do
+ order = create(:order_with_line_item_quantity, line_items_quantity: 2)
+
+ stock_location = create(:stock_location)
+
+ create_shipment(order, stock_location)
+ create_shipment(order, stock_location)
+
+ create :tax_adjustment, adjustable: order.line_items.first, order: order
+
+ expect(order.shipments.first.item_cost).to eq(11.0)
+ expect(order.shipments.last.item_cost).to eq(11.0)
+ end
+
+ it 'equals line items final amount with tax' do
+ shipment = create(:shipment, order: create(:order_with_line_item_quantity, line_items_quantity: 2))
+ create :tax_adjustment, adjustable: shipment.order.line_items.first, order: shipment.order
+ expect(shipment.item_cost).to eq(22.0)
+ end
+ end
+
+ it '#discounted_cost' do
+ shipment = create(:shipment)
+ shipment.cost = 10
+ shipment.promo_total = -1
+ expect(shipment.discounted_cost).to eq(9)
+ end
+
+ it '#tax_total with included taxes' do
+ shipment = Spree::Shipment.new
+ expect(shipment.tax_total).to eq(0)
+ shipment.included_tax_total = 10
+ expect(shipment.tax_total).to eq(10)
+ end
+
+ it '#tax_total with additional taxes' do
+ shipment = Spree::Shipment.new
+ expect(shipment.tax_total).to eq(0)
+ shipment.additional_tax_total = 10
+ expect(shipment.tax_total).to eq(10)
+ end
+
+ it '#final_price' do
+ shipment = Spree::Shipment.new
+ shipment.cost = 10
+ shipment.adjustment_total = -2
+ shipment.included_tax_total = 1
+ expect(shipment.final_price).to eq(8)
+ end
+
+ context '#free?' do
+ let!(:order) { create(:order) }
+ let!(:shipment) { create(:shipment, cost: 10, order: order) }
+ let(:free_shipping_promotion) { create(:free_shipping_promotion, code: 'freeship') }
+
+ it 'returns true if final_price is equal to 0' do
+ shipment.adjustment_total = -10
+ expect(shipment.free?).to eq(true)
+ end
+
+ it 'returns when Free Shipping promotion is applied' do
+ order.coupon_code = free_shipping_promotion.code
+ Spree::PromotionHandler::Coupon.new(order).apply
+ expect(order.promotions).to include(free_shipping_promotion)
+ expect(shipment.free?).to eq(true)
+ end
+ end
+
+ context 'manifest' do
+ let(:order) { Spree::Order.create }
+ let(:variant) { create(:variant) }
+ let!(:line_item) { Spree::Cart::AddItem.call(order: order, variant: variant).value }
+ let!(:shipment) { order.create_proposed_shipments.first }
+
+ it 'returns variant expected' do
+ expect(shipment.manifest.first.variant).to eq variant
+ end
+
+ context 'variant was removed' do
+ before { variant.destroy }
+
+ it 'still returns variant expected' do
+ expect(shipment.manifest.first.variant).to eq variant
+ end
+ end
+ end
+
+ context 'shipping_rates' do
+ let(:shipment) { create(:shipment) }
+ let(:shipping_method1) { create(:shipping_method) }
+ let(:shipping_method2) { create(:shipping_method) }
+ let(:shipping_rates) do
+ [
+ Spree::ShippingRate.new(shipping_method: shipping_method1, cost: 10.00, selected: true),
+ Spree::ShippingRate.new(shipping_method: shipping_method2, cost: 20.00)
+ ]
+ end
+
+ it 'returns shipping_method from selected shipping_rate' do
+ shipment.shipping_rates.delete_all
+ shipment.shipping_rates.create shipping_method: shipping_method1, cost: 10.00, selected: true
+ expect(shipment.shipping_method).to eq shipping_method1
+ end
+
+ context 'refresh_rates' do
+ let(:mock_estimator) { double('estimator', shipping_rates: shipping_rates) }
+
+ before { allow(shipment).to receive(:can_get_rates?).and_return(true) }
+
+ it 'requests new rates, and maintain shipping_method selection' do
+ expect(Spree::Stock::Estimator).to receive(:new).with(shipment.order).and_return(mock_estimator)
+ allow(shipment).to receive_messages(shipping_method: shipping_method2)
+
+ expect(shipment.refresh_rates).to eq(shipping_rates)
+ expect(shipment.reload.selected_shipping_rate.shipping_method_id).to eq(shipping_method2.id)
+ end
+
+ it 'handles no shipping_method selection' do
+ expect(Spree::Stock::Estimator).to receive(:new).with(shipment.order).and_return(mock_estimator)
+ allow(shipment).to receive_messages(shipping_method: nil)
+ expect(shipment.refresh_rates).to eq(shipping_rates)
+ expect(shipment.reload.selected_shipping_rate).not_to be_nil
+ end
+
+ it 'does not refresh if shipment is shipped' do
+ expect(Spree::Stock::Estimator).not_to receive(:new)
+ shipment.shipping_rates.delete_all
+ allow(shipment).to receive_messages(shipped?: true)
+ expect(shipment.refresh_rates).to eq([])
+ end
+
+ it "can't get rates without a shipping address" do
+ shipment.order.ship_address = nil
+ expect(shipment.refresh_rates).to eq([])
+ end
+
+ context 'to_package' do
+ let(:inventory_units) do
+ [build(:inventory_unit, line_item: line_item, variant: variant, state: 'on_hand'),
+ build(:inventory_unit, line_item: line_item, variant: variant, state: 'backordered')]
+ end
+
+ before do
+ allow(shipment).to receive(:inventory_units) { inventory_units }
+ allow(inventory_units).to receive_message_chain(:includes, :joins).and_return inventory_units
+ end
+
+ it 'uses symbols for states when adding contents to package' do
+ package = shipment.to_package
+ expect(package.on_hand.count).to eq 1
+ expect(package.backordered.count).to eq 1
+ end
+ end
+ end
+ end
+
+ context '#update!' do
+ shared_examples_for 'immutable once shipped' do
+ it 'remains in shipped state once shipped' do
+ shipment.state = 'shipped'
+ expect(shipment).to receive(:update_columns).with(state: 'shipped', updated_at: kind_of(Time))
+ shipment.update!(order)
+ end
+ end
+
+ shared_examples_for 'pending if backordered' do
+ it 'has a state of pending if backordered' do
+ allow(shipment).to receive_messages(inventory_units: [mock_model(Spree::InventoryUnit, backordered?: true)])
+ expect(shipment).to receive(:update_columns).with(state: 'pending', updated_at: kind_of(Time))
+ shipment.update!(order)
+ end
+ end
+
+ context 'when order cannot ship' do
+ before { allow(order).to receive_messages can_ship?: false }
+
+ it "results in a 'pending' state" do
+ expect(shipment).to receive(:update_columns).with(state: 'pending', updated_at: kind_of(Time))
+ shipment.update!(order)
+ end
+ end
+
+ context 'when order is paid' do
+ before { allow(order).to receive_messages paid?: true }
+
+ it "results in a 'ready' state" do
+ expect(shipment).to receive(:update_columns).with(state: 'ready', updated_at: kind_of(Time))
+ shipment.update!(order)
+ end
+ it_behaves_like 'immutable once shipped'
+ it_behaves_like 'pending if backordered'
+ end
+
+ context 'when order has balance due' do
+ before { allow(order).to receive_messages paid?: false }
+
+ it "results in a 'pending' state" do
+ shipment.state = 'ready'
+ expect(shipment).to receive(:update_columns).with(state: 'pending', updated_at: kind_of(Time))
+ shipment.update!(order)
+ end
+ it_behaves_like 'immutable once shipped'
+ it_behaves_like 'pending if backordered'
+ end
+
+ context 'when order has a credit owed' do
+ before { allow(order).to receive_messages payment_state: 'credit_owed', paid?: true }
+
+ it "results in a 'ready' state" do
+ shipment.state = 'pending'
+ expect(shipment).to receive(:update_columns).with(state: 'ready', updated_at: kind_of(Time))
+ shipment.update!(order)
+ end
+ it_behaves_like 'immutable once shipped'
+ it_behaves_like 'pending if backordered'
+ end
+
+ context 'when shipment state changes to shipped' do
+ before do
+ allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email)
+ allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state)
+ end
+
+ it 'calls after_ship' do
+ shipment.state = 'pending'
+ expect(shipment).to receive :after_ship
+ allow(shipment).to receive_messages determine_state: 'shipped'
+ expect(shipment).to receive(:update_columns).with(state: 'shipped', updated_at: kind_of(Time))
+ shipment.update!(order)
+ end
+
+ context 'when using the default shipment handler' do
+ it "calls the 'perform' method" do
+ shipment.state = 'pending'
+ allow(shipment).to receive_messages determine_state: 'shipped'
+ expect_any_instance_of(Spree::ShipmentHandler).to receive(:perform)
+ shipment.update!(order)
+ end
+ end
+
+ context 'when using a custom shipment handler' do
+ before do
+ Spree::ShipmentHandler::UPS = Class.new do
+ def initialize(_shipment)
+ true
+ end
+
+ def perform
+ true
+ end
+ end
+ end
+
+ after do
+ Spree::ShipmentHandler.send(:remove_const, :UPS)
+ end
+
+ it "calls the custom handler's 'perform' method" do
+ shipment.state = 'pending'
+ allow(shipment).to receive_messages determine_state: 'shipped'
+ expect_any_instance_of(Spree::ShipmentHandler::UPS).to receive(:perform)
+ shipment.update!(order)
+ end
+ end
+
+ # Regression test for #4347
+ context 'with adjustments' do
+ before do
+ shipment.adjustments << Spree::Adjustment.create(order: order, label: 'Label', amount: 5)
+ end
+
+ it 'transitions to shipped' do
+ shipment.update_column(:state, 'ready')
+ expect { shipment.ship! }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ context 'when order is completed' do
+ after { Spree::Config.set track_inventory_levels: true }
+
+ before do
+ allow(order).to receive_messages completed?: true
+ allow(order).to receive_messages canceled?: false
+ end
+
+ context 'with inventory tracking' do
+ before { Spree::Config.set track_inventory_levels: true }
+
+ it 'validates with inventory' do
+ shipment.inventory_units = [create(:inventory_unit)]
+ expect(shipment.valid?).to be true
+ end
+ end
+
+ context 'without inventory tracking' do
+ before { Spree::Config.set track_inventory_levels: false }
+
+ it 'validates with no inventory' do
+ expect(shipment.valid?).to be true
+ end
+ end
+ end
+
+ context '#cancel' do
+ it 'cancels the shipment' do
+ allow(shipment.order).to receive(:update_with_updater!)
+
+ shipment.state = 'pending'
+ expect(shipment).to receive(:after_cancel)
+ shipment.cancel!
+ expect(shipment.state).to eq 'canceled'
+ end
+
+ it 'restocks the items' do
+ inventory_unit = mock_model(Spree::InventoryUnit, state: 'on_hand', line_item: line_item, variant: variant, quantity: 1)
+ allow(shipment).to receive(:inventory_units).and_return([inventory_unit])
+ shipment.stock_location = mock_model(Spree::StockLocation)
+ expect(shipment.stock_location).to receive(:restock).with(variant, 1, shipment)
+ shipment.after_cancel
+ end
+
+ context 'with backordered inventory units' do
+ let(:order) { create(:order) }
+ let(:variant) { create(:variant) }
+ let(:other_order) { create(:order) }
+
+ before do
+ Spree::Cart::AddItem.call(order: order, variant: variant)
+ order.create_proposed_shipments
+
+ Spree::Cart::AddItem.call(order: other_order, variant: variant)
+ other_order.create_proposed_shipments
+ end
+
+ it "doesn't fill backorders when restocking inventory units" do
+ shipment = order.shipments.first
+ expect(shipment.inventory_units.count).to eq 1
+ expect(shipment.inventory_units.first).to be_backordered
+
+ other_shipment = other_order.shipments.first
+ expect(other_shipment.inventory_units.count).to eq 1
+ expect(other_shipment.inventory_units.first).to be_backordered
+
+ expect do
+ shipment.cancel!
+ end.not_to change { other_shipment.inventory_units.first.state }
+ end
+ end
+ end
+
+ context '#resume' do
+ it 'transitions state to ready if the order is ready' do
+ allow(shipment.order).to receive(:update_with_updater!)
+
+ shipment.state = 'canceled'
+ expect(shipment).to receive(:determine_state).and_return('ready')
+ expect(shipment).to receive(:after_resume)
+ shipment.resume!
+ expect(shipment.state).to eq 'ready'
+ end
+
+ it 'transitions state to pending if the order is not ready' do
+ allow(shipment.order).to receive(:update_with_updater!)
+
+ shipment.state = 'canceled'
+ expect(shipment).to receive(:determine_state).and_return('pending')
+ expect(shipment).to receive(:after_resume)
+ shipment.resume!
+ # Shipment is pending because order is already paid
+ expect(shipment.state).to eq 'pending'
+ end
+
+ it 'unstocks them items' do
+ inventory_unit = mock_model(Spree::InventoryUnit, quantity: 1, line_item: line_item, variant: variant)
+ allow(shipment).to receive(:inventory_units).and_return([inventory_unit])
+ shipment.stock_location = mock_model(Spree::StockLocation)
+ expect(shipment.stock_location).to receive(:unstock).with(variant, 1, shipment)
+ shipment.after_resume
+ end
+ end
+
+ context '#ship' do
+ context 'when the shipment is canceled' do
+ let(:shipment_with_inventory_units) { create(:shipment, order: create(:order_with_line_items), state: 'canceled') }
+ let(:subject) { shipment_with_inventory_units.ship! }
+
+ before do
+ allow(order).to receive(:update_with_updater!)
+ allow(shipment_with_inventory_units).to receive_messages(require_inventory: false, update_order: true)
+ end
+
+ it 'unstocks them items' do
+ allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state)
+ allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email)
+
+ expect(shipment_with_inventory_units.stock_location).to receive(:unstock)
+ subject
+ end
+ end
+
+ ['ready', 'canceled'].each do |state|
+ context "from #{state}" do
+ before do
+ allow(order).to receive(:update_with_updater!)
+ allow(shipment).to receive_messages(require_inventory: false, update_order: true, state: state)
+ end
+
+ it 'updates shipped_at timestamp' do
+ allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state)
+ allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email)
+
+ shipment.ship!
+ expect(shipment.shipped_at).not_to be_nil
+ # Ensure value is persisted
+ shipment.reload
+ expect(shipment.shipped_at).not_to be_nil
+ end
+
+ it 'sends a shipment email' do
+ mail_message = double 'Mail::Message'
+ shipment_id = nil
+ expect(Spree::ShipmentMailer).to receive(:shipped_email) { |*args|
+ shipment_id = args[0]
+ mail_message
+ }
+ expect(mail_message).to receive :deliver_later
+ allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state)
+
+ shipment.ship!
+ expect(shipment_id).to eq(shipment.id)
+ end
+
+ it 'finalizes adjustments' do
+ allow_any_instance_of(Spree::ShipmentHandler).to receive(:update_order_shipment_state)
+ allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email)
+
+ expect(shipment.adjustments).to all(receive(:finalize!))
+ shipment.ship!
+ end
+ end
+ end
+ end
+
+ context '#ready' do
+ context 'with Config.auto_capture_on_dispatch == false' do
+ # Regression test for #2040
+ it 'cannot ready a shipment for an order if the order is unpaid' do
+ allow(order).to receive_messages(paid?: false)
+ assert !shipment.can_ready?
+ end
+ end
+
+ context 'with Config.auto_capture_on_dispatch == true' do
+ before do
+ Spree::Config[:auto_capture_on_dispatch] = true
+ @order = create :completed_order_with_pending_payment
+ @shipment = @order.shipments.first
+ @shipment.cost = @order.ship_total
+ end
+
+ it 'shipments ready for an order if the order is unpaid' do
+ expect(@shipment.ready?).to be true
+ end
+
+ it 'tells the order to process payment in #after_ship' do
+ expect(@shipment).to receive(:process_order_payments)
+ @shipment.ship!
+ end
+
+ context 'order has pending payments' do
+ let(:payment) do
+ payment = @order.payments.first
+ payment.update_attribute :state, 'pending'
+ payment
+ end
+
+ before do
+ calculator = @shipment.shipping_method.calculator
+ calculator.set_preference(:amount, @shipment.cost)
+ calculator.save!
+ end
+
+ it 'can fully capture an authorized payment' do
+ payment.update_attribute(:amount, @order.total)
+
+ expect(payment.amount).to eq payment.uncaptured_amount
+ @shipment.ship!
+ expect(payment.reload.uncaptured_amount.to_f).to eq 0
+ end
+
+ it 'can partially capture an authorized payment' do
+ payment.update_attribute(:amount, @order.total + 50)
+
+ expect(payment.amount).to eq payment.uncaptured_amount
+ @shipment.ship!
+ expect(payment.captured_amount).to eq @order.total
+ expect(payment.captured_amount).to eq payment.amount - 50
+ expect(payment.order.payments.pending.first.amount).to eq 50
+ end
+ end
+ end
+ end
+
+ context 'updates cost when selected shipping rate is present' do
+ let(:shipment) { create(:shipment) }
+
+ before { allow(shipment).to receive_message_chain :selected_shipping_rate, cost: 5 }
+
+ it 'updates shipment totals' do
+ shipment.update_amounts
+ expect(shipment.reload.cost).to eq(5)
+ end
+
+ it 'factors in additional adjustments to adjustment total' do
+ shipment.adjustments.create!(
+ order: order,
+ label: 'Additional',
+ amount: 5,
+ included: false,
+ state: 'closed'
+ )
+ shipment.update_amounts
+ expect(shipment.reload.adjustment_total).to eq(5)
+ end
+
+ it 'does not factor in included adjustments to adjustment total' do
+ shipment.adjustments.create!(
+ order: order,
+ label: 'Included',
+ amount: 5,
+ included: true,
+ state: 'closed'
+ )
+ shipment.update_amounts
+ expect(shipment.reload.adjustment_total).to eq(0)
+ end
+ end
+
+ context 'changes shipping rate via general update' do
+ let(:order) do
+ Spree::Order.create(
+ payment_total: 100, payment_state: 'paid', total: 100, item_total: 100
+ )
+ end
+
+ let(:shipment) { Spree::Shipment.create order_id: order.id, stock_location: create(:stock_location) }
+
+ let(:shipping_rate) do
+ Spree::ShippingRate.create shipment_id: shipment.id, cost: 10
+ end
+
+ before do
+ shipment.update_attributes_and_order selected_shipping_rate_id: shipping_rate.id
+ end
+
+ it 'updates everything around order shipment total and state' do
+ expect(shipment.cost.to_f).to eq 10
+ expect(shipment.state).to eq 'pending'
+ expect(shipment.order.total.to_f).to eq 110
+ expect(shipment.order.payment_state).to eq 'balance_due'
+ end
+ end
+
+ context 'after_save' do
+ context 'line item changes' do
+ before do
+ shipment.cost = shipment.cost + 10
+ end
+
+ it 'triggers adjustment total recalculation' do
+ expect(shipment).to receive(:recalculate_adjustments)
+ shipment.save
+ end
+
+ it 'does not trigger adjustment recalculation if shipment has shipped' do
+ shipment.state = 'shipped'
+ expect(shipment).not_to receive(:recalculate_adjustments)
+ shipment.save
+ end
+ end
+
+ context 'line item does not change' do
+ it 'does not trigger adjustment total recalculation' do
+ expect(shipment).not_to receive(:recalculate_adjustments)
+ shipment.save
+ end
+ end
+ end
+
+ context 'currency' do
+ it 'returns the order currency' do
+ expect(shipment.currency).to eq(order.currency)
+ end
+ end
+
+ context 'nil costs' do
+ it 'sets cost to 0' do
+ shipment = Spree::Shipment.new
+ shipment.valid?
+ expect(shipment.cost).to eq 0
+ end
+ end
+
+ context '#tracking_url' do
+ it 'uses shipping method to determine url' do
+ expect(shipping_method).to receive(:build_tracking_url).with('1Z12345').and_return(:some_url)
+ shipment.tracking = '1Z12345'
+
+ expect(shipment.tracking_url).to eq(:some_url)
+ end
+ end
+
+ context '#transfer_to_location' do
+ # Order with 2 line items in order to be able to split one shipment into 2
+ let(:order) { create(:completed_order_with_totals, line_items_count: 2) }
+ let(:stock_location) { create(:stock_location) }
+ let(:variant) { order.line_items.first.variant }
+
+ before do
+ shipping_method = order.shipments.first.shipping_method
+ shipping_method.calculator.preferences[:amount] = order.shipments.first.cost
+ shipping_method.calculator.save!
+ end
+
+ it 'creates new shipment for same order' do
+ shipment = order.shipments.first
+
+ expect { shipment.transfer_to_location(variant, 1, stock_location) }.
+ to change { order.reload.shipments.size }.from(1).to(2)
+ end
+
+ it 'sets the given stock location for new shipment' do
+ shipment = order.shipments.first
+ shipment.transfer_to_location(variant, 1, stock_location)
+
+ new_shipment = order.reload.shipments.last
+
+ expect(new_shipment.stock_location).not_to eq(shipment.stock_location)
+ end
+
+ it 'sets proper costs for new shipment' do
+ shipment = order.shipments.first
+ shipment.transfer_to_location(variant, 1, shipment.stock_location)
+
+ new_shipment = order.reload.shipments.last
+ # Cost must be the same since both come from the same stock location
+ expect(new_shipment.cost).to eq(shipment.cost)
+ end
+
+ it 'updates `order.shipment_total` to the sum of shipments cost' do
+ shipment = order.shipments.first
+ shipment.transfer_to_location(variant, 1, shipment.stock_location)
+
+ order.reload
+ expect(order.shipment_total).to eq(order.shipments.sum(&:cost))
+ end
+ end
+
+ context 'set up new inventory units' do
+ # let(:line_item) { double(
+ let(:variant) { double('Variant', id: 9) }
+
+ let(:inventory_units) { double }
+
+ let(:params) do
+ { variant_id: variant.id, state: 'on_hand', order_id: order.id, line_item_id: line_item.id, quantity: 1 }
+ end
+
+ before { allow(shipment).to receive_messages inventory_units: inventory_units }
+
+ it 'associates variant and order' do
+ expect(inventory_units).to receive(:create).with(params)
+ shipment.set_up_inventory('on_hand', variant, order, line_item)
+ end
+ end
+
+ # Regression test for #3349
+ context '#destroy' do
+ it 'destroys linked shipping_rates' do
+ reflection = Spree::Shipment.reflect_on_association(:shipping_rates)
+ expect(reflection.options[:dependent]).to be(:delete_all)
+ end
+ end
+
+ # Regression test for #4072 (kinda)
+ # The need for this was discovered in the research for #4702
+ context 'state changes' do
+ before do
+ # Must be stubbed so transition can succeed
+ allow(order).to receive_messages paid?: true
+ end
+
+ it 'are logged to the database' do
+ expect(shipment.state_changes).to be_empty
+ expect(shipment.ready!).to be true
+ expect(shipment.state_changes.count).to eq(1)
+ state_change = shipment.state_changes.first
+ expect(state_change.previous_state).to eq('pending')
+ expect(state_change.next_state).to eq('ready')
+ end
+ end
+end
diff --git a/core/spec/models/spree/shipping_calculator_spec.rb b/core/spec/models/spree/shipping_calculator_spec.rb
new file mode 100644
index 00000000000..27e63e1d077
--- /dev/null
+++ b/core/spec/models/spree/shipping_calculator_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+module Spree
+ describe ShippingCalculator, type: :model do
+ subject { ShippingCalculator.new }
+
+ let(:variant1) { build(:variant, price: 10) }
+ let(:variant2) { build(:variant, price: 20) }
+
+ let(:package) do
+ build(:stock_package, variants_contents: { variant1 => 2, variant2 => 1 })
+ end
+
+ it 'computes with a shipment' do
+ shipment = mock_model(Spree::Shipment)
+ expect(subject).to receive(:compute_shipment).with(shipment)
+ subject.compute(shipment)
+ end
+
+ it 'computes with a package' do
+ expect(subject).to receive(:compute_package).with(package)
+ subject.compute(package)
+ end
+
+ it 'compute_shipment must be overridden' do
+ expect do
+ subject.compute_shipment(shipment)
+ end.to raise_error(NameError)
+ end
+
+ it 'compute_package must be overridden' do
+ expect do
+ subject.compute_package(package)
+ end.to raise_error(NotImplementedError)
+ end
+
+ it 'checks availability for a package' do
+ expect(subject.available?(package)).to be true
+ end
+
+ it 'calculates totals for content_items' do
+ expect(subject.send(:total, package.contents)).to eq 40.00
+ end
+ end
+end
diff --git a/core/spec/models/spree/shipping_category_spec.rb b/core/spec/models/spree/shipping_category_spec.rb
new file mode 100644
index 00000000000..799d4e42d27
--- /dev/null
+++ b/core/spec/models/spree/shipping_category_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Spree::ShippingCategory, type: :model do
+ describe '#validations' do
+ it 'has a valid factory' do
+ expect(FactoryBot.build(:shipping_category)).to be_valid
+ end
+
+ it 'requires name' do
+ expect(FactoryBot.build(:shipping_category, name: '')).not_to be_valid
+ end
+
+ it 'validates uniqueness' do
+ FactoryBot.create(:shipping_category, name: 'Test')
+ expect(FactoryBot.build(:shipping_category, name: 'Test')).not_to be_valid
+ end
+ end
+end
diff --git a/core/spec/models/spree/shipping_method_spec.rb b/core/spec/models/spree/shipping_method_spec.rb
new file mode 100644
index 00000000000..bb5731e264d
--- /dev/null
+++ b/core/spec/models/spree/shipping_method_spec.rb
@@ -0,0 +1,127 @@
+require 'spec_helper'
+
+class DummyShippingCalculator < Spree::ShippingCalculator
+end
+
+describe Spree::ShippingMethod, type: :model do
+ let(:shipping_method) { create(:shipping_method) }
+ let(:frontend_shipping_method) { create :shipping_method, display_on: 'front_end' }
+ let(:backend_shipping_method) { create :shipping_method, display_on: 'back_end' }
+ let(:front_and_back_end_shipping_method) { create :shipping_method, display_on: 'both' }
+
+ context 'calculators' do
+ it "rejects calculators that don't inherit from Spree::ShippingCalculator" do
+ allow(Spree::ShippingMethod).to receive_message_chain(:spree_calculators, :shipping_methods).and_return([
+ Spree::Calculator::Shipping::FlatPercentItemTotal,
+ Spree::Calculator::Shipping::PriceSack,
+ Spree::Calculator::DefaultTax,
+ DummyShippingCalculator # included as regression test for https://github.com/spree/spree/issues/3109
+ ])
+
+ expect(Spree::ShippingMethod.calculators).to eq([Spree::Calculator::Shipping::FlatPercentItemTotal, Spree::Calculator::Shipping::PriceSack, DummyShippingCalculator])
+ expect(Spree::ShippingMethod.calculators).not_to eq([Spree::Calculator::DefaultTax])
+ end
+ end
+
+ # Regression test for #4492
+ context '#shipments' do
+ let!(:shipping_method) { create(:shipping_method) }
+ let!(:shipment) do
+ shipment = create(:shipment)
+ shipment.shipping_rates.create!(shipping_method: shipping_method)
+ shipment
+ end
+
+ it 'can gather all the related shipments' do
+ expect(shipping_method.shipments).to include(shipment)
+ end
+ end
+
+ context 'validations' do
+ before { subject.valid? }
+
+ it 'validates presence of name' do
+ expect(subject.error_on(:name).size).to eq(1)
+ end
+
+ context 'shipping category' do
+ context 'is required' do
+ it { expect(subject.error_on(:base).size).to eq(1) }
+ it 'adds error to base' do
+ expect(subject.error_on(:base)).to include(I18n.t(:required_shipping_category,
+ scope: [
+ :activerecord, :errors, :models,
+ 'spree/shipping_method', :attributes, :base
+ ]))
+ end
+ end
+
+ context 'one associated' do
+ before { subject.shipping_categories.push create(:shipping_category) }
+
+ it { expect(subject.error_on(:base).size).to eq(0) }
+ end
+ end
+ end
+
+ context 'factory' do
+ it 'sets calculable correctly' do
+ expect(shipping_method.calculator.calculable).to eq(shipping_method)
+ end
+ end
+
+ context 'generating tracking URLs' do
+ context 'shipping method has a tracking URL mask on file' do
+ let(:tracking_url) { 'https://track-o-matic.com/:tracking' }
+
+ before { allow(subject).to receive(:tracking_url) { tracking_url } }
+
+ context 'tracking number has spaces' do
+ let(:tracking_numbers) { ['1234 5678 9012 3456', 'a bcdef'] }
+ let(:expectations) { %w[https://track-o-matic.com/1234%205678%209012%203456 https://track-o-matic.com/a%20bcdef] }
+
+ it "returns a single URL with '%20' in lieu of spaces" do
+ tracking_numbers.each_with_index do |num, i|
+ expect(subject.build_tracking_url(num)).to eq(expectations[i])
+ end
+ end
+ end
+ end
+ end
+
+ # Regression test for #4320
+ context 'soft deletion' do
+ let(:shipping_method) { create(:shipping_method) }
+
+ it 'soft-deletes when destroy is called' do
+ shipping_method.destroy
+ expect(shipping_method.deleted_at).not_to be_blank
+ end
+ end
+
+ describe '#available_to_display?' do
+ context 'when available on frontend' do
+ it { expect(frontend_shipping_method.available_to_display?(Spree::ShippingMethod::DISPLAY_ON_FRONT_END)).to be true }
+ it { expect(backend_shipping_method.available_to_display?(Spree::ShippingMethod::DISPLAY_ON_FRONT_END)).to be false }
+ it { expect(front_and_back_end_shipping_method.available_to_display?(Spree::ShippingMethod::DISPLAY_ON_FRONT_END)).to be true }
+ end
+
+ context 'when available on backend' do
+ it { expect(frontend_shipping_method.available_to_display?(Spree::ShippingMethod::DISPLAY_ON_BACK_END)).to be false }
+ it { expect(backend_shipping_method.available_to_display?(Spree::ShippingMethod::DISPLAY_ON_BACK_END)).to be true }
+ it { expect(front_and_back_end_shipping_method.available_to_display?(Spree::ShippingMethod::DISPLAY_ON_BACK_END)).to be true }
+ end
+ end
+
+ describe '#frontend?' do
+ it { expect(frontend_shipping_method.send(:frontend?)).to be true }
+ it { expect(backend_shipping_method.send(:frontend?)).to be false }
+ it { expect(front_and_back_end_shipping_method.send(:frontend?)).to be true }
+ end
+
+ describe '#backend?' do
+ it { expect(frontend_shipping_method.send(:backend?)).to be false }
+ it { expect(backend_shipping_method.send(:backend?)).to be true }
+ it { expect(front_and_back_end_shipping_method.send(:backend?)).to be true }
+ end
+end
diff --git a/core/spec/models/spree/shipping_rate_spec.rb b/core/spec/models/spree/shipping_rate_spec.rb
new file mode 100644
index 00000000000..bdab736fe61
--- /dev/null
+++ b/core/spec/models/spree/shipping_rate_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+
+describe Spree::ShippingRate, type: :model do
+ let(:shipment) { create(:shipment) }
+ let(:shipping_method) { create(:shipping_method) }
+ let(:shipping_rate) do
+ Spree::ShippingRate.new shipment: shipment,
+ shipping_method: shipping_method,
+ cost: 10
+ end
+
+ context '#display_price' do
+ context 'when tax included in price' do
+ let!(:default_zone) { create(:zone, default_tax: true) }
+ let(:default_tax_rate) do
+ create :tax_rate,
+ name: 'VAT',
+ amount: 0.1,
+ included_in_price: true,
+ zone: default_zone
+ end
+
+ context 'when the tax rate is from the default zone' do
+ before { shipping_rate.tax_rate = default_tax_rate }
+
+ it 'shows correct tax amount' do
+ expect(shipping_rate.display_price.to_s).
+ to eq("$10.00 (incl. $0.91 #{default_tax_rate.name})")
+ end
+
+ context 'when cost is zero' do
+ before do
+ shipping_rate.cost = 0
+ end
+
+ it 'shows no tax amount' do
+ expect(shipping_rate.display_price.to_s).to eq('$0.00')
+ end
+ end
+ end
+
+ context 'when the tax rate is from another zone' do
+ let!(:non_default_zone) { create(:zone, default_tax: false) }
+
+ let(:non_default_tax_rate) do
+ create :tax_rate,
+ name: 'VAT',
+ amount: 0.2,
+ included_in_price: true,
+ zone: non_default_zone
+ end
+
+ before { shipping_rate.tax_rate = non_default_tax_rate }
+
+ it "deducts the other zone's VAT from the calculated shipping rate" do
+ expect(shipping_rate.display_price.to_s).
+ to eq("$10.00 (incl. $1.67 #{non_default_tax_rate.name})")
+ end
+
+ context 'when cost is zero' do
+ before do
+ shipping_rate.cost = 0
+ end
+
+ it 'shows no tax amount' do
+ expect(shipping_rate.display_price.to_s).to eq('$0.00')
+ end
+ end
+ end
+ end
+
+ context 'when tax is additional to price' do
+ let(:tax_rate) { create(:tax_rate, name: 'Sales Tax', amount: 0.1) }
+
+ before { shipping_rate.tax_rate = tax_rate }
+
+ it 'shows correct tax amount' do
+ expect(shipping_rate.display_price.to_s).
+ to eq("$10.00 (+ $1.00 #{tax_rate.name})")
+ end
+
+ context 'when cost is zero' do
+ before do
+ shipping_rate.cost = 0
+ end
+
+ it 'shows no tax amount' do
+ expect(shipping_rate.display_price.to_s).to eq('$0.00')
+ end
+ end
+ end
+
+ context 'when the currency is JPY' do
+ let(:shipping_rate) { Spree::ShippingRate.new(cost: 205) }
+
+ before { allow(shipping_rate).to receive_messages(currency: 'JPY') }
+
+ it 'displays the price in yen' do
+ expect(shipping_rate.display_price.to_s).to eq('Â¥205')
+ end
+ end
+ end
+
+ # Regression test for #3829
+ context '#shipping_method' do
+ it 'can be retrieved' do
+ expect(shipping_rate.shipping_method.reload).to eq(shipping_method)
+ end
+
+ it 'can be retrieved even when deleted' do
+ shipping_method.update_column(:deleted_at, Time.current)
+ shipping_rate.save
+ shipping_rate.reload
+ expect(shipping_rate.shipping_method).to eq(shipping_method)
+ end
+ end
+
+ context '#tax_rate' do
+ let!(:tax_rate) { create(:tax_rate) }
+
+ before do
+ shipping_rate.tax_rate = tax_rate
+ end
+
+ it 'can be retrieved' do
+ expect(shipping_rate.tax_rate.reload).to eq(tax_rate)
+ end
+
+ it 'can be retrieved even when deleted' do
+ tax_rate.update_column(:deleted_at, Time.current)
+ shipping_rate.save
+ shipping_rate.reload
+ expect(shipping_rate.tax_rate).to eq(tax_rate)
+ end
+ end
+
+ context '#tax_amount' do
+ context 'without tax rate' do
+ it 'returns 0.0' do
+ expect(shipping_rate.tax_amount).to eq(0.0)
+ end
+ end
+ end
+
+ context '#final_price' do
+ let(:free_shipping_promotion) { create(:free_shipping_promotion, code: 'freeship') }
+ let(:order) { shipment.order }
+
+ it 'returns 0 if free shipping promotion is applied' do
+ order.coupon_code = free_shipping_promotion.code
+ Spree::PromotionHandler::Coupon.new(order).apply
+ expect(order.promotions).to include(free_shipping_promotion)
+ expect(shipping_rate.final_price).to eq(0.0)
+ end
+
+ it 'returns 0 if cost is lesser than the discount amount' do
+ allow_any_instance_of(Spree::ShippingRate).to receive_messages(discount_amount: -20.0)
+ expect(shipping_rate.final_price).to eq(0.0)
+ end
+
+ it 'returns cost minus discount amount' do
+ allow_any_instance_of(Spree::ShippingRate).to receive_messages(discount_amount: -5.0)
+ expect(shipping_rate.final_price).to eq(5.0)
+ end
+ end
+end
diff --git a/core/spec/models/spree/state_spec.rb b/core/spec/models/spree/state_spec.rb
new file mode 100644
index 00000000000..22a510f80eb
--- /dev/null
+++ b/core/spec/models/spree/state_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Spree::State, type: :model do
+ it 'can find a state by name or abbr' do
+ state = create(:state, name: 'California', abbr: 'CA')
+ expect(Spree::State.find_all_by_name_or_abbr('California')).to include(state)
+ expect(Spree::State.find_all_by_name_or_abbr('CA')).to include(state)
+ end
+
+ it 'can find all states group by country id' do
+ state = create(:state)
+ expect(Spree::State.states_group_by_country_id).to eq(state.country_id.to_s => [[state.id, state.name]])
+ end
+
+ describe 'whitelisted_ransackable_attributes' do
+ it { expect(Spree::State.whitelisted_ransackable_attributes).to eq(%w(abbr)) }
+ end
+end
diff --git a/core/spec/models/spree/stock/availability_validator_spec.rb b/core/spec/models/spree/stock/availability_validator_spec.rb
new file mode 100644
index 00000000000..cac97a7deb8
--- /dev/null
+++ b/core/spec/models/spree/stock/availability_validator_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe AvailabilityValidator, type: :model do
+ subject { described_class.new }
+
+ let!(:line_item) { double(quantity: 5, variant_id: 1, variant: double.as_null_object, errors: double('errors'), inventory_units: []) }
+ let(:inventory_unit) { double('InventoryUnit') }
+ let(:inventory_units) { [inventory_unit] }
+
+ before do
+ allow(inventory_unit).to receive_messages(pending?: false)
+ allow(inventory_unit).to receive_messages(quantity: 5)
+ end
+
+ it 'is valid when supply is sufficient' do
+ allow_any_instance_of(Stock::Quantifier).to receive_messages(can_supply?: true)
+ expect(line_item).not_to receive(:errors)
+ subject.validate(line_item)
+ end
+
+ it 'is invalid when supply is insufficent' do
+ allow_any_instance_of(Stock::Quantifier).to receive_messages(can_supply?: false)
+ expect(line_item.errors).to receive(:[]).with(:quantity).and_return []
+ subject.validate(line_item)
+ end
+
+ it 'considers existing inventory_units sufficient' do
+ allow_any_instance_of(Stock::Quantifier).to receive_messages(can_supply?: false)
+ expect(line_item).not_to receive(:errors)
+ allow(line_item).to receive_messages(inventory_units: inventory_units)
+ subject.validate(line_item)
+ end
+
+ it 'is valid when the quantity is zero' do
+ expect(line_item).to receive(:quantity).and_return(0)
+ expect(line_item.errors).not_to receive(:[]).with(:quantity)
+ subject.validate(line_item)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/content_item_spec.rb b/core/spec/models/spree/stock/content_item_spec.rb
new file mode 100644
index 00000000000..482f9ef463e
--- /dev/null
+++ b/core/spec/models/spree/stock/content_item_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe ContentItem, type: :model do
+ subject { ContentItem.new(build(:inventory_unit, variant: variant)) }
+
+ let(:variant) { build(:variant, weight: 25.0) }
+
+ context '#volume' do
+ it 'calculate the total volume of the variant' do
+ expect(subject.volume).to eq variant.volume * subject.quantity
+ end
+ end
+
+ context '#dimension' do
+ it 'calculate the total dimension of the variant' do
+ expect(subject.dimension).to eq variant.dimension * subject.quantity
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/coordinator_spec.rb b/core/spec/models/spree/stock/coordinator_spec.rb
new file mode 100644
index 00000000000..6d34976e186
--- /dev/null
+++ b/core/spec/models/spree/stock/coordinator_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe Coordinator, type: :model do
+ subject { Coordinator.new(order) }
+
+ let(:order) { create(:order_with_line_items) }
+
+ context 'packages' do
+ it 'builds, prioritizes and estimates' do
+ expect(subject).to receive(:build_packages).ordered
+ expect(subject).to receive(:prioritize_packages).ordered
+ expect(subject).to receive(:estimate_packages).ordered
+ subject.packages
+ end
+ end
+
+ describe '#shipments' do
+ let(:packages) { [build(:stock_package_fulfilled), build(:stock_package_fulfilled)] }
+
+ before { allow(subject).to receive(:packages).and_return(packages) }
+
+ it 'turns packages into shipments' do
+ shipments = subject.shipments
+ expect(shipments.count).to eq packages.count
+ expect(shipments).to all(be_a(Shipment))
+ end
+
+ it "puts the order's ship address on the shipments" do
+ shipments = subject.shipments
+ shipments.each { |shipment| expect(shipment.address).to eq order.ship_address }
+ end
+ end
+
+ context 'build packages' do
+ let!(:stock_location1) { create(:stock_location, backorderable_default: false) }
+ let!(:stock_location2) { create(:stock_location, backorderable_default: false) }
+ let!(:product) { create(:product) }
+
+ let!(:order) do
+ product.stock_items.map { |stock_item| stock_item.adjust_count_on_hand(1) }
+ line_item = create(:line_item, product: product, quantity: 2)
+ line_item.order
+ end
+
+ it 'builds a package for every stock location' do
+ expect(subject.build_packages.count).to eq(StockLocation.count)
+ end
+
+ context 'missing stock items in stock location' do
+ let!(:another_location) { create(:stock_location, propagate_all_variants: false) }
+
+ it 'builds packages only for valid stock locations' do
+ expect(subject.build_packages.count).to eq(StockLocation.count - 1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/differentiator_spec.rb b/core/spec/models/spree/stock/differentiator_spec.rb
new file mode 100644
index 00000000000..4aebeec9001
--- /dev/null
+++ b/core/spec/models/spree/stock/differentiator_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe Differentiator, type: :model do
+ subject { Differentiator.new(order, packages) }
+
+ let(:variant1) { mock_model(Variant) }
+ let(:variant2) { mock_model(Variant) }
+
+ let(:line_item1) { build(:line_item, variant: variant1, quantity: 2) }
+ let(:line_item2) { build(:line_item, variant: variant2, quantity: 2) }
+
+ let(:stock_location) { mock_model(StockLocation) }
+
+ let(:inventory_unit1) { build(:inventory_unit, variant: variant1, line_item: line_item1) }
+ let(:inventory_unit2) { build(:inventory_unit, variant: variant2, line_item: line_item2) }
+
+ let(:order) { mock_model(Order, line_items: [line_item1, line_item2]) }
+
+ let(:package1) do
+ Package.new(stock_location).tap { |p| p.add(inventory_unit1) }
+ end
+
+ let(:package2) do
+ Package.new(stock_location).tap { |p| p.add(inventory_unit2) }
+ end
+
+ let(:packages) { [package1, package2] }
+
+ it { is_expected.to be_missing }
+
+ it 'calculates the missing items' do
+ expect(subject.missing[variant1]).to eq 1
+ expect(subject.missing[variant2]).to eq 1
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/estimator_spec.rb b/core/spec/models/spree/stock/estimator_spec.rb
new file mode 100644
index 00000000000..c1d4832e980
--- /dev/null
+++ b/core/spec/models/spree/stock/estimator_spec.rb
@@ -0,0 +1,209 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe Estimator, type: :model do
+ subject { Estimator.new(order) }
+
+ let!(:shipping_method) { create(:shipping_method) }
+ let(:package) { build(:stock_package, contents: inventory_units.map { |_i| ContentItem.new(inventory_unit) }) }
+ let(:order) { build(:order_with_line_items) }
+ let(:inventory_units) { order.inventory_units }
+
+ context '#shipping rates' do
+ before do
+ shipping_method.zones.first.members.create(zoneable: order.ship_address.country)
+ allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :available?).and_return(true)
+ allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :compute).and_return(4.00)
+ allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :preferences).and_return(currency: currency)
+ allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :marked_for_destruction?)
+
+ allow(package).to receive_messages(shipping_methods: [shipping_method])
+ end
+
+ let(:currency) { 'USD' }
+
+ shared_examples_for 'shipping rate matches' do
+ it 'returns shipping rates' do
+ shipping_rates = subject.shipping_rates(package)
+ expect(shipping_rates.first.cost).to eq 4.00
+ end
+ end
+
+ shared_examples_for "shipping rate doesn't match" do
+ it 'does not return shipping rates' do
+ shipping_rates = subject.shipping_rates(package)
+ expect(shipping_rates).to eq([])
+ end
+ end
+
+ context "when the order's ship address is in the same zone" do
+ it_behaves_like 'shipping rate matches'
+ end
+
+ context "when the order's ship address is in a different zone" do
+ before { shipping_method.zones.each { |z| z.members.delete_all } }
+
+ it_behaves_like "shipping rate doesn't match"
+ end
+
+ context 'when the calculator is not available for that order' do
+ before { allow_any_instance_of(ShippingMethod).to receive_message_chain(:calculator, :available?).and_return(false) }
+
+ it_behaves_like "shipping rate doesn't match"
+ end
+
+ context 'when the currency is nil' do
+ let(:currency) { nil }
+
+ it_behaves_like 'shipping rate matches'
+ end
+
+ context 'when the currency is an empty string' do
+ let(:currency) { '' }
+
+ it_behaves_like 'shipping rate matches'
+ end
+
+ context "when the current matches the order's currency" do
+ it_behaves_like 'shipping rate matches'
+ end
+
+ context "if the currency is different than the order's currency" do
+ let(:currency) { 'GBP' }
+
+ it_behaves_like "shipping rate doesn't match"
+ end
+
+ it 'sorts shipping rates by cost' do
+ shipping_methods = Array.new(3) { create(:shipping_method) }
+ allow(shipping_methods[0]).to receive_message_chain(:calculator, :compute).and_return(5.00)
+ allow(shipping_methods[1]).to receive_message_chain(:calculator, :compute).and_return(3.00)
+ allow(shipping_methods[2]).to receive_message_chain(:calculator, :compute).and_return(4.00)
+
+ allow(subject).to receive(:shipping_methods).and_return(shipping_methods)
+
+ expect(subject.shipping_rates(package).map(&:cost)).to eq %w[3.00 4.00 5.00].map(&BigDecimal.method(:new))
+ end
+
+ context 'general shipping methods' do
+ let(:shipping_methods) { Array.new(2) { create(:shipping_method) } }
+
+ it 'selects the most affordable shipping rate' do
+ allow(shipping_methods[0]).to receive_message_chain(:calculator, :compute).and_return(5.00)
+ allow(shipping_methods[1]).to receive_message_chain(:calculator, :compute).and_return(3.00)
+
+ allow(subject).to receive(:shipping_methods).and_return(shipping_methods)
+
+ expect(subject.shipping_rates(package).sort_by(&:cost).map(&:selected)).to eq [true, false]
+ end
+
+ it "selects the most affordable shipping rate and doesn't raise exception over nil cost" do
+ allow(shipping_methods[0]).to receive_message_chain(:calculator, :compute).and_return(1.00)
+ allow(shipping_methods[1]).to receive_message_chain(:calculator, :compute).and_return(nil)
+
+ allow(subject).to receive(:shipping_methods).and_return(shipping_methods)
+
+ subject.shipping_rates(package)
+ end
+ end
+
+ context 'involves backend only shipping methods' do
+ let(:backend_method) { create(:shipping_method, display_on: 'back_end') }
+ let(:generic_method) { create(:shipping_method) }
+
+ before do
+ allow(backend_method).to receive_message_chain(:calculator, :compute).and_return(0.00)
+ allow(generic_method).to receive_message_chain(:calculator, :compute).and_return(5.00)
+ allow(package).to receive(:shipping_methods).and_return([backend_method, generic_method])
+ end
+
+ it 'does not return backend rates at all' do
+ expect(subject.shipping_rates(package).map(&:shipping_method_id)).to eq([generic_method.id])
+ end
+
+ # regression for #3287
+ it "doesn't select backend rates even if they're more affordable" do
+ expect(subject.shipping_rates(package).map(&:selected)).to eq [true]
+ end
+ end
+
+ context 'includes tax adjustments if applicable' do
+ let!(:tax_rate) { create(:tax_rate, zone: order.tax_zone) }
+
+ before do
+ Spree::ShippingMethod.all.each do |sm|
+ sm.tax_category_id = tax_rate.tax_category_id
+ sm.save
+ end
+ package.shipping_methods.map(&:reload)
+ end
+
+ it 'links the shipping rate and the tax rate' do
+ shipping_rates = subject.shipping_rates(package)
+ expect(shipping_rates.first.tax_rate).to eq(tax_rate)
+ end
+ end
+
+ context 'VAT price calculation' do
+ let(:tax_category) { create :tax_category }
+ let!(:shipping_method) { create(:shipping_method, tax_category: tax_category) }
+
+ let(:default_zone) { create(:zone_with_country, default_tax: true) }
+ let!(:default_vat) do
+ create :tax_rate,
+ included_in_price: true,
+ zone: default_zone,
+ amount: 0.2,
+ tax_category: shipping_method.tax_category
+ end
+
+ context 'when the order does not have a tax zone' do
+ before { allow(order).to receive(:tax_zone).and_return nil }
+
+ it_behaves_like 'shipping rate matches'
+ end
+
+ context "when the order's tax zone is the default zone" do
+ before { allow(order).to receive(:tax_zone).and_return(default_zone) }
+
+ it_behaves_like 'shipping rate matches'
+ end
+
+ context "when the order's tax zone is a non-VAT zone" do
+ let!(:zone_without_vat) { create(:zone_with_country) }
+
+ before { allow(order).to receive(:tax_zone).and_return(zone_without_vat) }
+
+ it 'deducts the default VAT from the cost' do
+ shipping_rates = subject.shipping_rates(package)
+ # deduct default vat: 4.00 / 1.2 = 3.33 (rounded)
+ expect(shipping_rates.first.cost).to eq(3.33)
+ end
+ end
+
+ context "when the order's tax zone is a zone with VAT outside the default zone" do
+ let(:other_vat_zone) { create(:zone_with_country) }
+ let!(:other_vat) do
+ create :tax_rate,
+ included_in_price: true,
+ zone: other_vat_zone,
+ amount: 0.3,
+ tax_category: shipping_method.tax_category
+ end
+
+ before { allow(order).to receive(:tax_zone).and_return(other_vat_zone) }
+
+ it 'deducts the default vat and applies the foreign vat to calculate the price' do
+ shipping_rates = subject.shipping_rates(package)
+ #
+ # deduct default vat: 4.00 / 1.2 = 3.33 (rounded)
+ # apply foreign vat: 3.33 * 1.3 = 4.33 (rounded)
+ expect(shipping_rates.first.cost).to eq(4.33)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/inventory_unit_builder_spec.rb b/core/spec/models/spree/stock/inventory_unit_builder_spec.rb
new file mode 100644
index 00000000000..23e5c5454a9
--- /dev/null
+++ b/core/spec/models/spree/stock/inventory_unit_builder_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe InventoryUnitBuilder, type: :model do
+ subject { InventoryUnitBuilder.new(order) }
+
+ let(:line_item_1) { build(:line_item) }
+ let(:line_item_2) { build(:line_item, quantity: 2) }
+ let(:order) { build(:order, line_items: [line_item_1, line_item_2]) }
+
+ describe '#units' do
+ it "returns an inventory unit for each quantity for the order's line items" do
+ units = subject.units
+ expect(units.count).to eq 2
+ expect(units.first.line_item).to eq line_item_1
+ expect(units.first.variant).to eq line_item_1.variant
+ expect(units.first.quantity).to eq line_item_1.quantity
+
+ expect(units.second.line_item).to eq line_item_2
+ expect(units.second.variant).to eq line_item_2.variant
+ expect(units.second.quantity).to eq line_item_2.quantity
+ end
+
+ it 'builds the inventory units as pending' do
+ expect(subject.units.map(&:pending).uniq).to eq [true]
+ end
+
+ it 'sets the order_id on inventory units' do
+ expect(subject.units.map(&:order_id).uniq).to eq [order.id]
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/package_spec.rb b/core/spec/models/spree/stock/package_spec.rb
new file mode 100644
index 00000000000..553849c0998
--- /dev/null
+++ b/core/spec/models/spree/stock/package_spec.rb
@@ -0,0 +1,183 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe Package, type: :model do
+ subject { Package.new(stock_location) }
+
+ let(:variant) { build(:variant, weight: 25.0) }
+ let(:stock_location) { build(:stock_location) }
+ let(:order) { build(:order) }
+
+ def build_inventory_unit
+ build(:inventory_unit, variant: variant)
+ end
+
+ it 'calculates the weight of all the contents' do
+ 4.times { subject.add build_inventory_unit }
+ expect(subject.weight).to eq(100.0)
+ end
+
+ context 'currency' do
+ let(:unit) { build_inventory_unit }
+
+ before { subject.add unit }
+
+ it 'returns the currency based on the currency from the order' do
+ expect(subject.currency).to eql 'USD'
+ end
+ end
+
+ it 'filters by on_hand and backordered' do
+ 4.times { subject.add build_inventory_unit }
+ 3.times { subject.add build_inventory_unit, :backordered }
+ expect(subject.on_hand.count).to eq 4
+ expect(subject.backordered.count).to eq 3
+ end
+
+ it 'calculates the quantity by state' do
+ 4.times { subject.add build_inventory_unit }
+ 3.times { subject.add build_inventory_unit, :backordered }
+
+ expect(subject.quantity).to eq 7
+ expect(subject.quantity(:on_hand)).to eq 4
+ expect(subject.quantity(:backordered)).to eq 3
+ end
+
+ it 'returns nil for content item not found' do
+ unit = build_inventory_unit
+ item = subject.find_item(unit, :on_hand)
+ expect(item).to be_nil
+ end
+
+ it 'finds content item for an inventory unit' do
+ unit = build_inventory_unit
+ subject.add unit
+ item = subject.find_item(unit, :on_hand)
+ expect(item.quantity).to eq 1
+ end
+
+ # Contains regression test for #2804
+ it 'builds a list of shipping methods common to all categories' do
+ category1 = create(:shipping_category)
+ category2 = create(:shipping_category)
+ method1 = create(:shipping_method)
+ method2 = create(:shipping_method)
+ method1.shipping_categories = [category1, category2]
+ method2.shipping_categories = [category1]
+ variant1 = create(:product, shipping_category: category1).master
+ variant2 = create(:product, shipping_category: category2).master
+ contents = [ContentItem.new(build(:inventory_unit, variant_id: variant1.id)),
+ ContentItem.new(build(:inventory_unit, variant_id: variant1.id)),
+ ContentItem.new(build(:inventory_unit, variant_id: variant2.id))]
+
+ package = Package.new(stock_location, contents)
+ expect(package.shipping_methods).to eq([method1])
+ end
+
+ it 'builds an empty list of shipping methods when no categories' do
+ variant = mock_model(Variant, shipping_category: nil)
+ contents = [ContentItem.new(build(:inventory_unit, variant: variant))]
+ package = Package.new(stock_location, contents)
+ expect(package.shipping_methods).to be_empty
+ end
+
+ it 'can convert to a shipment' do
+ 2.times { subject.add build_inventory_unit }
+ subject.add build_inventory_unit, :backordered
+
+ shipping_method = build(:shipping_method)
+ subject.shipping_rates = [Spree::ShippingRate.new(shipping_method: shipping_method, cost: 10.00, selected: true)]
+
+ shipment = subject.to_shipment
+ expect(shipment.stock_location).to eq subject.stock_location
+ expect(shipment.inventory_units.size).to eq 3
+
+ first_unit = shipment.inventory_units.first
+ expect(first_unit.variant).to eq variant
+ expect(first_unit.state).to eq 'on_hand'
+ expect(first_unit).to be_pending
+
+ last_unit = shipment.inventory_units.last
+ expect(last_unit.variant).to eq variant
+ expect(last_unit.state).to eq 'backordered'
+
+ expect(shipment.shipping_method).to eq shipping_method
+ end
+
+ describe '#add_multiple' do
+ it 'adds multiple inventory units' do
+ expect { subject.add_multiple [build_inventory_unit, build_inventory_unit] }.to change(subject, :quantity).by(2)
+ end
+
+ it 'allows adding with a state' do
+ expect { subject.add_multiple [build_inventory_unit, build_inventory_unit], :backordered }.to change { subject.backordered.count }.by(2)
+ end
+
+ it 'defaults to adding with the on hand state' do
+ expect { subject.add_multiple [build_inventory_unit, build_inventory_unit] }.to change { subject.on_hand.count }.by(2)
+ end
+ end
+
+ describe '#remove' do
+ let(:unit) { build_inventory_unit }
+
+ context 'there is a content item for the inventory unit' do
+ before { subject.add unit }
+
+ it 'removes that content item' do
+ expect { subject.remove(unit) }.to change(subject, :quantity).by(-1)
+ expect(subject.contents.map(&:inventory_unit)).not_to include unit
+ end
+ end
+
+ context 'there is no content item for the inventory unit' do
+ it "doesn't change the set of content items" do
+ expect { subject.remove(unit) }.not_to change(subject, :quantity)
+ end
+ end
+ end
+
+ describe '#order' do
+ let(:unit) { build_inventory_unit }
+
+ context 'there is an inventory unit' do
+ before { subject.add unit }
+
+ it 'returns an order' do
+ expect(subject.order).to be_a_kind_of Spree::Order
+ expect(subject.order).to eq unit.order
+ end
+ end
+
+ context 'there is no inventory unit' do
+ it 'returns nil' do
+ expect(subject.order).to eq nil
+ end
+ end
+ end
+
+ context '#volume' do
+ it 'calculates the sum of the volume of all the items' do
+ contents = [ContentItem.new(build(:inventory_unit, variant: build(:variant))),
+ ContentItem.new(build(:inventory_unit, variant: build(:variant))),
+ ContentItem.new(build(:inventory_unit, variant: build(:variant))),
+ ContentItem.new(build(:inventory_unit, variant: build(:variant)))]
+ package = Package.new(stock_location, contents)
+ expect(package.volume).to eq contents.sum(&:volume)
+ end
+ end
+
+ context '#dimension' do
+ it 'calculates the sum of the dimension of all the items' do
+ contents = [ContentItem.new(build(:inventory_unit, variant: build(:variant))),
+ ContentItem.new(build(:inventory_unit, variant: build(:variant))),
+ ContentItem.new(build(:inventory_unit, variant: build(:variant))),
+ ContentItem.new(build(:inventory_unit, variant: build(:variant)))]
+ package = Package.new(stock_location, contents)
+ expect(package.dimension).to eq contents.sum(&:dimension)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/packer_spec.rb b/core/spec/models/spree/stock/packer_spec.rb
new file mode 100644
index 00000000000..da21a15ef11
--- /dev/null
+++ b/core/spec/models/spree/stock/packer_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe Packer, type: :model do
+ subject { Packer.new(stock_location, inventory_units) }
+
+ let(:inventory_units) { [InventoryUnit.new(variant: create(:variant))] }
+ let(:stock_location) { create(:stock_location) }
+
+ context 'packages' do
+ it 'builds an array of packages' do
+ packages = subject.packages
+ expect(packages).to be_a Array
+ expect(packages.first).to be_a Package
+ end
+
+ it 'allows users to set splitters to an empty array' do
+ packer = Packer.new(StockLocation.new, [], [])
+ expect(packer).not_to receive(:build_splitter)
+ packer.packages
+ end
+ end
+
+ context 'default_package' do
+ let!(:inventory_units) { Array.new(2) { InventoryUnit.new variant: create(:variant) } }
+
+ it 'contains all the items' do
+ package = subject.default_package
+ expect(package.contents.size).to eq 2
+ end
+
+ it 'variants are added as backordered without enough on_hand' do
+ expect(stock_location).to receive(:fill_status).twice.and_return(
+ *(Array.new(1, [1, 0]) + Array.new(1, [0, 1]))
+ )
+
+ package = subject.default_package
+ expect(package.on_hand.size).to eq 1
+ expect(package.backordered.size).to eq 1
+ end
+
+ context "location doesn't have order items in stock" do
+ let(:stock_location) { create(:stock_location, propagate_all_variants: false) }
+ let(:inventory_units) { [InventoryUnit.new(variant: create(:variant))] }
+ let(:packer) { Packer.new(stock_location, inventory_units) }
+
+ it 'builds an empty package' do
+ expect(packer.default_package.contents).to be_empty
+ end
+ end
+
+ context "doesn't track inventory levels" do
+ let(:inventory_units) { Array.new(2) { InventoryUnit.new(variant: create(:variant)) } }
+
+ before { Config.track_inventory_levels = false }
+
+ it "doesn't bother stock items status in stock location" do
+ expect(subject.stock_location).not_to receive(:fill_status)
+ subject.default_package
+ end
+
+ it 'still creates package with proper quantity' do
+ expect(subject.default_package.quantity).to eq 2
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/prioritizer_spec.rb b/core/spec/models/spree/stock/prioritizer_spec.rb
new file mode 100644
index 00000000000..a481ac498ae
--- /dev/null
+++ b/core/spec/models/spree/stock/prioritizer_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ describe Prioritizer, type: :model do
+ let(:variant) { build(:variant, weight: 25.0) }
+ let(:stock_location) { build(:stock_location) }
+ let(:order) { build(:order) }
+
+ def inventory_units
+ @inventory_units ||= []
+ end
+
+ def build_inventory_unit
+ build(:inventory_unit, variant: variant).tap do |unit|
+ inventory_units << unit
+ end
+ end
+
+ def pack
+ package = Package.new(stock_location)
+ yield(package) if block_given?
+ package
+ end
+
+ it 'keeps a single package' do
+ package1 = pack do |package|
+ package.add build_inventory_unit
+ package.add build_inventory_unit
+ end
+
+ packages = [package1]
+ prioritizer = Prioritizer.new(packages)
+ packages = prioritizer.prioritized_packages
+ expect(packages.size).to eq 1
+ end
+
+ it 'removes duplicate packages' do
+ package1 = pack do |package|
+ package.add build_inventory_unit
+ package.add build_inventory_unit
+ end
+
+ package2 = pack do |package|
+ package.add inventory_units.first
+ package.add inventory_units.last
+ end
+
+ packages = [package1, package2]
+ prioritizer = Prioritizer.new(packages)
+ packages = prioritizer.prioritized_packages
+ expect(packages.size).to eq 1
+ end
+
+ it 'split over 2 packages' do
+ package1 = pack do |package|
+ package.add build_inventory_unit
+ end
+ package2 = pack do |package|
+ package.add build_inventory_unit
+ end
+
+ packages = [package1, package2]
+ prioritizer = Prioritizer.new(packages)
+ packages = prioritizer.prioritized_packages
+ expect(packages.size).to eq 2
+ end
+
+ it '1st has some, 2nd has remaining' do
+ 5.times { build_inventory_unit }
+
+ package1 = pack do |package|
+ 2.times { |i| package.add inventory_units[i] }
+ end
+ package2 = pack do |package|
+ 5.times { |i| package.add inventory_units[i] }
+ end
+
+ packages = [package1, package2]
+ prioritizer = Prioritizer.new(packages)
+ packages = prioritizer.prioritized_packages
+ expect(packages.count).to eq 2
+ expect(packages[0].quantity).to eq 2
+ expect(packages[1].quantity).to eq 3
+ end
+
+ it '1st has backorder, 2nd has some' do
+ 5.times { build_inventory_unit }
+
+ package1 = pack do |package|
+ 5.times { |i| package.add inventory_units[i], :backordered }
+ end
+ package2 = pack do |package|
+ 2.times { |i| package.add inventory_units[i] }
+ end
+
+ packages = [package1, package2]
+ prioritizer = Prioritizer.new(packages)
+ packages = prioritizer.prioritized_packages
+
+ expect(packages[0].quantity(:backordered)).to eq 3
+ expect(packages[1].quantity(:on_hand)).to eq 2
+ end
+
+ it '1st has backorder, 2nd has all' do
+ 5.times { build_inventory_unit }
+
+ package1 = pack do |package|
+ 3.times { |i| package.add inventory_units[i], :backordered }
+ end
+ package2 = pack do |package|
+ 5.times { |i| package.add inventory_units[i] }
+ end
+
+ packages = [package1, package2]
+ prioritizer = Prioritizer.new(packages)
+ packages = prioritizer.prioritized_packages
+ expect(packages[0]).to eq package2
+ expect(packages[1]).to be_nil
+ expect(packages[0].quantity(:backordered)).to eq 0
+ expect(packages[0].quantity(:on_hand)).to eq 5
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/quantifier_spec.rb b/core/spec/models/spree/stock/quantifier_spec.rb
new file mode 100644
index 00000000000..9970121e866
--- /dev/null
+++ b/core/spec/models/spree/stock/quantifier_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+shared_examples_for 'unlimited supply' do
+ it 'can_supply? any amount' do
+ expect(subject.can_supply?(1)).to be true
+ expect(subject.can_supply?(101)).to be true
+ expect(subject.can_supply?(100_001)).to be true
+ end
+end
+
+module Spree
+ module Stock
+ describe Quantifier, type: :model do
+ subject { described_class.new(stock_item.variant) }
+
+ before(:all) { Spree::StockLocation.destroy_all } # FIXME: leaky database
+
+ let!(:stock_location) { create :stock_location_with_items }
+ let!(:stock_item) { stock_location.stock_items.order(:id).first }
+
+ specify { expect(subject.stock_items).to eq([stock_item]) }
+ specify { expect(subject.variant).to eq(stock_item.variant) }
+
+ context 'with a single stock location/item' do
+ it 'total_on_hand should match stock_item' do
+ expect(subject.total_on_hand).to eq(stock_item.count_on_hand)
+ end
+
+ context 'when variant is available' do
+ before do
+ allow(subject.variant).to receive(:available?).and_return(true)
+ end
+
+ context 'when track_inventory_levels is false' do
+ before { configure_spree_preferences { |config| config.track_inventory_levels = false } }
+
+ specify { expect(subject.total_on_hand).to eq(Float::INFINITY) }
+
+ it_behaves_like 'unlimited supply'
+ end
+
+ context 'when variant inventory tracking is off' do
+ before { stock_item.variant.track_inventory = false }
+
+ specify { expect(subject.total_on_hand).to eq(Float::INFINITY) }
+
+ it_behaves_like 'unlimited supply'
+ end
+
+ context 'when stock item allows backordering' do
+ specify { expect(subject.backorderable?).to be true }
+
+ it_behaves_like 'unlimited supply'
+ end
+
+ context 'when stock item prevents backordering' do
+ before { stock_item.update_attributes(backorderable: false) }
+
+ specify { expect(subject.backorderable?).to be false }
+
+ it 'can_supply? only upto total_on_hand' do
+ expect(subject.can_supply?(1)).to be true
+ expect(subject.can_supply?(10)).to be true
+ expect(subject.can_supply?(11)).to be false
+ end
+ end
+ end
+
+ context 'when variant is not available' do
+ before do
+ allow(subject.variant).to receive(:available?).and_return(false)
+ end
+
+ it { expect(subject.can_supply?).to be false }
+ end
+ end
+
+ context 'with multiple stock locations/items' do
+ let!(:stock_location_2) { create :stock_location }
+ let!(:stock_location_3) { create :stock_location, active: false }
+
+ before do
+ stock_location_2.stock_items.where(variant_id: stock_item.variant).update_all(count_on_hand: 5, backorderable: false)
+ stock_location_3.stock_items.where(variant_id: stock_item.variant).update_all(count_on_hand: 5, backorderable: false)
+ end
+
+ it 'total_on_hand should total all active stock_items' do
+ expect(subject.total_on_hand).to eq(15)
+ end
+
+ context 'when variant is available' do
+ before do
+ allow(subject.variant).to receive(:available?).and_return(true)
+ end
+
+ context 'when any stock item allows backordering' do
+ specify { expect(subject.backorderable?).to be true }
+
+ it_behaves_like 'unlimited supply'
+ end
+
+ context 'when all stock items prevent backordering' do
+ before { stock_item.update_attributes(backorderable: false) }
+
+ specify { expect(subject.backorderable?).to be false }
+
+ it 'can_supply? upto total_on_hand' do
+ expect(subject.can_supply?(1)).to be true
+ expect(subject.can_supply?(15)).to be true
+ expect(subject.can_supply?(16)).to be false
+ end
+ end
+ end
+
+ context 'when variant is not available' do
+ before do
+ allow(subject.variant).to receive(:available?).and_return(false)
+ end
+
+ it { expect(subject.can_supply?).to be false }
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/splitter/backordered_spec.rb b/core/spec/models/spree/stock/splitter/backordered_spec.rb
new file mode 100644
index 00000000000..a50e36477ed
--- /dev/null
+++ b/core/spec/models/spree/stock/splitter/backordered_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ module Splitter
+ describe Backordered, type: :model do
+ subject(:result) do
+ described_class.new(packer).split([package])
+ end
+
+ let(:packer) { build(:stock_packer) }
+ let(:package) { Package.new(packer.stock_location) }
+
+ before do
+ package.add_multiple(build_stubbed_list(:inventory_unit, 4, :without_assoc))
+ package.add_multiple(build_stubbed_list(:inventory_unit, 5, :without_assoc), :backordered)
+ end
+
+ it 'splits packages by status' do
+ expect(result.count).to eq 2
+
+ expect(result[0].quantity).to eq 4
+ expect(result[0].on_hand.count).to eq 4
+ expect(result[0].backordered.count).to eq 0
+
+ expect(result[1].quantity).to eq 5
+ expect(result[1].on_hand.count).to eq 0
+ expect(result[1].backordered.count).to eq 5
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/splitter/base_spec.rb b/core/spec/models/spree/stock/splitter/base_spec.rb
new file mode 100644
index 00000000000..cbfece6dc80
--- /dev/null
+++ b/core/spec/models/spree/stock/splitter/base_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ module Splitter
+ describe Base, type: :model do
+ let(:splitter1) { described_class.new(packer) }
+ let(:splitter2) { described_class.new(packer, splitter1) }
+
+ let(:packer) { build(:stock_packer) }
+
+ let(:packages) { [] }
+
+ describe 'continues to splitter chain' do
+ after { splitter2.split(packages) }
+
+ it { expect(splitter1).to receive(:split).with(packages) }
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/splitter/shipping_category_spec.rb b/core/spec/models/spree/stock/splitter/shipping_category_spec.rb
new file mode 100644
index 00000000000..0e33522c3be
--- /dev/null
+++ b/core/spec/models/spree/stock/splitter/shipping_category_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ module Splitter
+ describe ShippingCategory, type: :model do
+ subject(:result) do
+ described_class.new(packer).split(packages)
+ end
+
+ let(:packer) { build(:stock_packer) }
+
+ let(:packages) { [package1, package2] }
+ let(:package1) { Spree::Stock::Package.new(packer.stock_location) }
+ let(:package2) { Spree::Stock::Package.new(packer.stock_location) }
+
+ let(:variant1) { build_stubbed(:variant, product: product1) }
+ let(:product1) { build_stubbed(:product, shipping_category: shipping_category_1) }
+ let(:variant2) { build_stubbed(:variant, product: product2) }
+ let(:product2) { build_stubbed(:product, shipping_category: shipping_category_2) }
+
+ let(:shipping_category_1) { build_stubbed(:shipping_category, name: 'A') }
+ let(:shipping_category_2) { build_stubbed(:shipping_category, name: 'B') }
+
+ before do
+ package1.add_multiple(build_stubbed_list(:inventory_unit, 4, :without_assoc, variant: variant1))
+ package1.add_multiple(build_stubbed_list(:inventory_unit, 8, :without_assoc, variant: variant2))
+
+ package2.add_multiple(build_stubbed_list(:inventory_unit, 6, :without_assoc, variant: variant1))
+ package2.add_multiple(build_stubbed_list(:inventory_unit, 9, :without_assoc, variant: variant2), :backordered)
+ end
+
+ it 'splits each package by shipping category' do
+ expect(result[0].quantity).to eq 4
+ expect(result[1].quantity).to eq 8
+ expect(result[2].quantity).to eq 6
+ expect(result[3].quantity).to eq 9
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock/splitter/weight_spec.rb b/core/spec/models/spree/stock/splitter/weight_spec.rb
new file mode 100644
index 00000000000..579b995c3e8
--- /dev/null
+++ b/core/spec/models/spree/stock/splitter/weight_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+module Spree
+ module Stock
+ module Splitter
+ describe Weight, type: :model do
+ subject(:result) do
+ described_class.new(packer).split(packages)
+ end
+
+ let(:packer) { build(:stock_packer) }
+ let(:packages) { [package] }
+ let(:package) { Package.new(packer.stock_location) }
+
+ let(:heavy_variant) { build_stubbed(:base_variant, weight: 100) }
+ let(:variant) { build_stubbed(:base_variant, weight: 49) }
+
+ context 'with packages that can be reduced' do
+ before do
+ package.add_multiple(build_stubbed_list(:inventory_unit, 2, :without_assoc, variant: heavy_variant))
+ package.add_multiple(build_stubbed_list(:inventory_unit, 4, :without_assoc, variant: variant))
+ package.add_multiple(build_stubbed_list(:inventory_unit, 2, :without_assoc, variant: heavy_variant))
+ end
+
+ it 'splits and keeps splitting until all packages are underweight' do
+ expect(result.size).to eq 4
+
+ result.each do |pack|
+ expect(pack.weight).to be <= described_class.threshold
+ end
+ end
+ end
+
+ context 'with packages that can not be reduced' do
+ let(:variant) { build_stubbed(:base_variant, weight: 200) }
+
+ before do
+ package.add_multiple(build_stubbed_list(:inventory_unit, 2, :without_assoc, variant: variant))
+ end
+
+ it 'handles packages that can not be reduced' do
+ # formula: (2*200) / 150
+ expect(result.size).to eq 2
+ end
+ end
+
+ context 'with multiple packages' do
+ let(:packages) { [package, package1] }
+ let(:package1) { Package.new(packer.stock_location) }
+
+ before do
+ package.add_multiple(build_stubbed_list(:inventory_unit, 2, :without_assoc, variant: heavy_variant))
+ package.add_multiple(build_stubbed_list(:inventory_unit, 4, :without_assoc, variant: variant))
+
+ package1.add_multiple(build_stubbed_list(:inventory_unit, 2, :without_assoc, variant: variant))
+ package1.add_multiple(build_stubbed_list(:inventory_unit, 4, :without_assoc, variant: heavy_variant))
+ end
+
+ it 'splits and keeps splitting until all packages are underweight' do
+ # formula: [2*100 + 4*49] [2*49, 4*100]
+ # first package was splited to 3, 2nd to 4
+ expect(result.size).to eq 7
+
+ result.each do |pack|
+ expect(pack.weight).to be <= described_class.threshold
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock_item_spec.rb b/core/spec/models/spree/stock_item_spec.rb
new file mode 100644
index 00000000000..37596ff5e39
--- /dev/null
+++ b/core/spec/models/spree/stock_item_spec.rb
@@ -0,0 +1,476 @@
+require 'spec_helper'
+
+describe Spree::StockItem, type: :model do
+ subject { stock_location.stock_items.order(:id).first }
+
+ let(:stock_location) { create(:stock_location_with_items) }
+
+ it 'maintains the count on hand for a variant' do
+ expect(subject.count_on_hand).to eq 10
+ end
+
+ it "can return the stock item's variant's name" do
+ expect(subject.variant_name).to eq(subject.variant.name)
+ end
+
+ context 'available to be included in shipment' do
+ context 'has stock' do
+ it { expect(subject).to be_available }
+ end
+
+ context 'backorderable' do
+ before { subject.backorderable = true }
+
+ it { expect(subject).to be_available }
+ end
+
+ context 'no stock and not backorderable' do
+ before do
+ subject.backorderable = false
+ allow(subject).to receive_messages(count_on_hand: 0)
+ end
+
+ it { expect(subject).not_to be_available }
+ end
+ end
+
+ describe 'reduce_count_on_hand_to_zero' do
+ context 'when count_on_hand > 0' do
+ before do
+ subject.update_column('count_on_hand', 4)
+ subject.reduce_count_on_hand_to_zero
+ end
+
+ it { expect(subject.count_on_hand).to eq(0) }
+ end
+
+ context 'when count_on_hand > 0' do
+ before do
+ subject.update_column('count_on_hand', -4)
+ @count_on_hand = subject.count_on_hand
+ subject.reduce_count_on_hand_to_zero
+ end
+
+ it { expect(subject.count_on_hand).to eq(@count_on_hand) }
+ end
+ end
+
+ context 'adjust count_on_hand' do
+ let!(:current_on_hand) { subject.count_on_hand }
+
+ it 'is updated pessimistically' do
+ copy = Spree::StockItem.find(subject.id)
+
+ subject.adjust_count_on_hand(5)
+ expect(subject.count_on_hand).to eq(current_on_hand + 5)
+
+ expect(copy.count_on_hand).to eq(current_on_hand)
+ copy.adjust_count_on_hand(5)
+ expect(copy.count_on_hand).to eq(current_on_hand + 10)
+ end
+
+ context 'item out of stock (by five items)' do
+ context 'when stock received is insufficient to fullfill backorders' do
+ let(:inventory_unit) { double('InventoryUnit') }
+ let(:inventory_unit_2) { double('InventoryUnit2') }
+ let(:split_inventory_unit) { double('SplitInventoryUnit') }
+
+ before do
+ allow(subject).to receive_messages(backordered_inventory_units: [inventory_unit, inventory_unit_2])
+ allow(split_inventory_unit).to receive_messages(quantity: 3)
+ allow(inventory_unit).to receive_messages(quantity: 4, split_inventory!: split_inventory_unit)
+ allow(inventory_unit_2).to receive_messages(quantity: 1)
+ subject.update_column(:count_on_hand, -5)
+ end
+
+ it 'splits inventory to fullfill partial backorder' do
+ expect(inventory_unit_2).not_to receive(:split_inventory!)
+
+ expect(split_inventory_unit).to receive(:fill_backorder)
+ expect(inventory_unit).not_to receive(:fill_backorder)
+ expect(inventory_unit_2).not_to receive(:fill_backorder)
+
+ subject.adjust_count_on_hand(3)
+ expect(subject.count_on_hand).to eq(-2)
+ end
+ end
+ end
+
+ context 'item out of stock (by two items)' do
+ let(:inventory_unit) { double('InventoryUnit') }
+ let(:inventory_unit_2) { double('InventoryUnit2') }
+
+ before do
+ allow(subject).to receive_messages(backordered_inventory_units: [inventory_unit, inventory_unit_2])
+ allow(inventory_unit).to receive_messages(quantity: 1)
+ allow(inventory_unit_2).to receive_messages(quantity: 1)
+ subject.update_column(:count_on_hand, -2)
+ end
+
+ # Regression test for #3755
+ it 'processes existing backorders, even with negative stock' do
+ expect(inventory_unit).to receive(:fill_backorder)
+ expect(inventory_unit_2).not_to receive(:fill_backorder)
+ subject.adjust_count_on_hand(1)
+ expect(subject.count_on_hand).to eq(-1)
+ end
+
+ # Test for #3755
+ it 'does not process backorders when stock is adjusted negatively' do
+ expect(inventory_unit).not_to receive(:fill_backorder)
+ expect(inventory_unit_2).not_to receive(:fill_backorder)
+ subject.adjust_count_on_hand(-1)
+ expect(subject.count_on_hand).to eq(-3)
+ end
+
+ context 'adds new items' do
+ before { allow(subject).to receive_messages(backordered_inventory_units: [inventory_unit, inventory_unit_2]) }
+
+ it 'fills existing backorders' do
+ expect(inventory_unit).to receive(:fill_backorder)
+ expect(inventory_unit_2).to receive(:fill_backorder)
+
+ subject.adjust_count_on_hand(3)
+ expect(subject.count_on_hand).to eq(1)
+ end
+ end
+ end
+ end
+
+ context 'set count_on_hand' do
+ let!(:current_on_hand) { subject.count_on_hand }
+
+ it 'is updated pessimistically' do
+ copy = Spree::StockItem.find(subject.id)
+
+ subject.set_count_on_hand(5)
+ expect(subject.count_on_hand).to eq(5)
+
+ expect(copy.count_on_hand).to eq(current_on_hand)
+ copy.set_count_on_hand(10)
+ expect(copy.count_on_hand).to eq(current_on_hand)
+ end
+
+ context 'item out of stock (by two items)' do
+ let(:inventory_unit) { double('InventoryUnit') }
+ let(:inventory_unit_2) { double('InventoryUnit2') }
+
+ before { subject.set_count_on_hand(-2) }
+
+ it "doesn't process backorders" do
+ expect(subject).not_to receive(:backordered_inventory_units)
+ end
+
+ context 'adds new items' do
+ before do
+ allow(subject).to receive_messages(backordered_inventory_units: [inventory_unit, inventory_unit_2])
+ allow(inventory_unit).to receive_messages(quantity: 1)
+ allow(inventory_unit_2).to receive_messages(quantity: 1)
+ end
+
+ it 'fills existing backorders' do
+ expect(inventory_unit).to receive(:fill_backorder)
+ expect(inventory_unit_2).to receive(:fill_backorder)
+
+ subject.set_count_on_hand(1)
+ expect(subject.count_on_hand).to eq(1)
+ end
+ end
+ end
+ end
+
+ context 'with stock movements' do
+ before { Spree::StockMovement.create(stock_item: subject, quantity: 1) }
+
+ it 'doesnt raise ReadOnlyRecord error' do
+ expect { subject.destroy }.not_to raise_error
+ end
+ end
+
+ context 'destroyed' do
+ before { subject.destroy }
+
+ it 'recreates stock item just fine' do
+ expect do
+ stock_location.stock_items.create!(variant: subject.variant)
+ end.not_to raise_error
+ end
+
+ it 'doesnt allow recreating more than one stock item at once' do
+ stock_location.stock_items.create!(variant: subject.variant)
+
+ expect do
+ stock_location.stock_items.create!(variant: subject.variant)
+ end.to raise_error(StandardError)
+ end
+ end
+
+ describe '#after_save' do
+ before do
+ subject.variant.update_column(:updated_at, 1.day.ago)
+ end
+
+ context 'binary_inventory_cache is set to false (default)' do
+ context 'in_stock? changes' do
+ it 'touches its variant' do
+ expect do
+ subject.adjust_count_on_hand(subject.count_on_hand * -1)
+ end.to change { subject.variant.reload.updated_at }
+ end
+ end
+
+ context 'in_stock? does not change' do
+ it 'touches its variant' do
+ expect do
+ subject.adjust_count_on_hand((subject.count_on_hand * -1) + 1)
+ end.to change { subject.variant.reload.updated_at }
+ end
+ end
+ end
+
+ context 'binary_inventory_cache is set to true' do
+ before { Spree::Config.binary_inventory_cache = true }
+
+ context 'in_stock? changes' do
+ it 'touches its variant' do
+ expect do
+ subject.adjust_count_on_hand(subject.count_on_hand * -1)
+ end.to change { subject.variant.reload.updated_at }
+ end
+ end
+
+ context 'in_stock? does not change' do
+ it 'does not touch its variant' do
+ expect do
+ subject.adjust_count_on_hand((subject.count_on_hand * -1) + 1)
+ end.not_to change { subject.variant.reload.updated_at }
+ end
+ end
+
+ context 'when a new stock location is added' do
+ it 'touches its variant' do
+ expect do
+ create(:stock_location)
+ end.to change { subject.variant.reload.updated_at }
+ end
+ end
+ end
+ end
+
+ describe '#after_touch' do
+ it 'touches its variant' do
+ Timecop.scale(1000) do
+ expect do
+ subject.touch
+ end.to change { subject.variant.updated_at }
+ end
+ end
+ end
+
+ # Regression test for #4651
+ context 'variant' do
+ it 'can be found even if the variant is deleted' do
+ subject.variant.destroy
+ expect(subject.reload.variant).not_to be_nil
+ end
+ end
+
+ describe 'validations' do
+ describe 'count_on_hand' do
+ shared_examples_for 'valid count_on_hand' do
+ before do
+ subject.save
+ end
+
+ it 'has :no errors_on' do
+ expect(subject.errors_on(:count_on_hand).size).to eq(0)
+ end
+ end
+
+ shared_examples_for 'not valid count_on_hand' do
+ before do
+ subject.save
+ end
+
+ it 'has 1 error_on' do
+ expect(subject.error_on(:count_on_hand).size).to eq(1)
+ end
+ it { expect(subject.errors[:count_on_hand]).to include 'must be greater than or equal to 0' }
+ end
+
+ context 'when count_on_hand not changed' do
+ context 'when not backorderable' do
+ before do
+ subject.backorderable = false
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+
+ context 'when backorderable' do
+ before do
+ subject.backorderable = true
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+ end
+
+ context 'when count_on_hand changed' do
+ context 'when backorderable' do
+ before do
+ subject.backorderable = true
+ end
+
+ context 'when both count_on_hand and count_on_hand_was are positive' do
+ context 'when count_on_hand is greater than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, 3)
+ subject.send(:count_on_hand=, subject.count_on_hand + 3)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+
+ context 'when count_on_hand is smaller than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, 3)
+ subject.send(:count_on_hand=, subject.count_on_hand - 2)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+ end
+
+ context 'when both count_on_hand and count_on_hand_was are negative' do
+ context 'when count_on_hand is greater than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, -3)
+ subject.send(:count_on_hand=, subject.count_on_hand + 2)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+
+ context 'when count_on_hand is smaller than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, 3)
+ subject.send(:count_on_hand=, subject.count_on_hand - 3)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+ end
+
+ context 'when both count_on_hand is positive and count_on_hand_was is negative' do
+ context 'when count_on_hand is greater than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, -3)
+ subject.send(:count_on_hand=, subject.count_on_hand + 6)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+ end
+
+ context 'when both count_on_hand is negative and count_on_hand_was is positive' do
+ context 'when count_on_hand is greater than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, 3)
+ subject.send(:count_on_hand=, subject.count_on_hand - 6)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+ end
+ end
+
+ context 'when not backorderable' do
+ before do
+ subject.backorderable = false
+ end
+
+ context 'when both count_on_hand and count_on_hand_was are positive' do
+ context 'when count_on_hand is greater than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, 3)
+ subject.send(:count_on_hand=, subject.count_on_hand + 3)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+
+ context 'when count_on_hand is smaller than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, 3)
+ subject.send(:count_on_hand=, subject.count_on_hand - 2)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+ end
+
+ context 'when both count_on_hand and count_on_hand_was are negative' do
+ context 'when count_on_hand is greater than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, -3)
+ subject.send(:count_on_hand=, subject.count_on_hand + 2)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+
+ context 'when count_on_hand is smaller than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, -3)
+ subject.send(:count_on_hand=, subject.count_on_hand - 3)
+ end
+
+ it_behaves_like 'not valid count_on_hand'
+ end
+ end
+
+ context 'when both count_on_hand is positive and count_on_hand_was is negative' do
+ context 'when count_on_hand is greater than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, -3)
+ subject.send(:count_on_hand=, subject.count_on_hand + 6)
+ end
+
+ it_behaves_like 'valid count_on_hand'
+ end
+ end
+
+ context 'when both count_on_hand is negative and count_on_hand_was is positive' do
+ context 'when count_on_hand is greater than count_on_hand_was' do
+ before do
+ subject.update_column(:count_on_hand, 3)
+ subject.send(:count_on_hand=, subject.count_on_hand - 6)
+ end
+
+ it_behaves_like 'not valid count_on_hand'
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'scopes' do
+ context '.with_active_stock_location' do
+ let(:stock_items_with_active_location) { Spree::StockItem.with_active_stock_location }
+
+ context 'when stock location is active' do
+ before { stock_location.update_column(:active, true) }
+
+ it { expect(stock_items_with_active_location).to include(subject) }
+ end
+
+ context 'when stock location is inactive' do
+ before { stock_location.update_column(:active, false) }
+
+ it { expect(stock_items_with_active_location).not_to include(subject) }
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock_location_spec.rb b/core/spec/models/spree/stock_location_spec.rb
new file mode 100644
index 00000000000..5320af48ada
--- /dev/null
+++ b/core/spec/models/spree/stock_location_spec.rb
@@ -0,0 +1,256 @@
+require 'spec_helper'
+
+module Spree
+ describe StockLocation, type: :model do
+ subject { create(:stock_location_with_items, backorderable_default: true) }
+
+ let(:stock_item) { subject.stock_items.order(:id).first }
+ let(:variant) { stock_item.variant }
+
+ it 'creates stock_items for all variants' do
+ expect(subject.stock_items.count).to eq Variant.count
+ end
+
+ it 'validates uniqueness' do
+ StockLocation.create(name: 'Test')
+ expect(StockLocation.new(name: 'Test')).not_to be_valid
+ end
+
+ context 'handling stock items' do
+ let!(:variant) { create(:variant) }
+
+ context 'given a variant' do
+ subject { StockLocation.create(name: 'testing', propagate_all_variants: false) }
+
+ context 'set up' do
+ it 'creates stock item' do
+ expect(subject).to receive(:propagate_variant)
+ subject.set_up_stock_item(variant)
+ end
+
+ context 'stock item exists' do
+ let!(:stock_item) { subject.propagate_variant(variant) }
+
+ it 'returns existing stock item' do
+ expect(subject.set_up_stock_item(variant)).to eq(stock_item)
+ end
+ end
+ end
+
+ context 'propagate variants' do
+ let(:stock_item) { subject.propagate_variant(variant) }
+
+ it 'creates a new stock item' do
+ expect do
+ subject.propagate_variant(variant)
+ end.to change(StockItem, :count).by(1)
+ end
+
+ context 'passes backorderable default config' do
+ context 'true' do
+ before { subject.backorderable_default = true }
+
+ it { expect(stock_item.backorderable).to be true }
+ end
+
+ context 'false' do
+ before { subject.backorderable_default = false }
+
+ it { expect(stock_item.backorderable).to be false }
+ end
+ end
+ end
+
+ context 'propagate all variants' do
+ subject { StockLocation.new(name: 'testing') }
+
+ context 'true' do
+ before { subject.propagate_all_variants = true }
+
+ specify do
+ expect(subject).to receive(:propagate_variant).at_least(:once)
+ subject.save!
+ end
+ end
+
+ context 'false' do
+ before { subject.propagate_all_variants = false }
+
+ specify do
+ expect(subject).not_to receive(:propagate_variant)
+ subject.save!
+ end
+ end
+ end
+ end
+ end
+
+ it 'finds a stock_item for a variant' do
+ stock_item = subject.stock_item(variant)
+ expect(stock_item.count_on_hand).to eq 10
+ end
+
+ it 'finds a stock_item for a variant by id' do
+ stock_item = subject.stock_item(variant.id)
+ expect(stock_item.variant).to eq variant
+ end
+
+ it 'returns nil when stock_item is not found for variant' do
+ stock_item = subject.stock_item(100)
+ expect(stock_item).to be_nil
+ end
+
+ describe '#stock_item_or_create' do
+ before do
+ variant = create(:variant)
+ variant.stock_items.destroy_all
+ variant.save
+ end
+
+ it 'creates a stock_item if not found for a variant' do
+ stock_item = subject.stock_item_or_create(variant)
+ expect(stock_item.variant).to eq variant
+ end
+ end
+
+ it 'finds a count_on_hand for a variant' do
+ expect(subject.count_on_hand(variant)).to eq 10
+ end
+
+ it 'finds determines if you a variant is backorderable' do
+ expect(subject.backorderable?(variant)).to be true
+ end
+
+ it 'restocks a variant with a positive stock movement' do
+ originator = double
+ expect(subject).to receive(:move).with(variant, 5, originator)
+ subject.restock(variant, 5, originator)
+ end
+
+ it 'unstocks a variant with a negative stock movement' do
+ originator = double
+ expect(subject).to receive(:move).with(variant, -5, originator)
+ subject.unstock(variant, 5, originator)
+ end
+
+ it 'creates a stock_movement' do
+ expect do
+ subject.move variant, 5
+ end.to change { subject.stock_movements.where(stock_item_id: stock_item).count }.by(1)
+ end
+
+ it 'can be deactivated' do
+ create(:stock_location, active: true)
+ create(:stock_location, active: false)
+ expect(Spree::StockLocation.active.count).to eq 1
+ end
+
+ it 'ensures only one stock location is default at a time' do
+ first = create(:stock_location, active: true, default: true)
+ second = create(:stock_location, active: true, default: true)
+
+ expect(first.reload.default).to eq false
+ expect(second.reload.default).to eq true
+
+ first.default = true
+ first.save!
+
+ expect(first.reload.default).to eq true
+ expect(second.reload.default).to eq false
+ end
+
+ context 'fill_status' do
+ it 'all on_hand with no backordered' do
+ on_hand, backordered = subject.fill_status(variant, 5)
+ expect(on_hand).to eq 5
+ expect(backordered).to eq 0
+ end
+
+ it 'some on_hand with some backordered' do
+ on_hand, backordered = subject.fill_status(variant, 20)
+ expect(on_hand).to eq 10
+ expect(backordered).to eq 10
+ end
+
+ it 'zero on_hand with all backordered' do
+ zero_stock_item = mock_model(StockItem,
+ count_on_hand: 0,
+ backorderable?: true)
+ expect(subject).to receive(:stock_item).with(variant).and_return(zero_stock_item)
+
+ on_hand, backordered = subject.fill_status(variant, 20)
+ expect(on_hand).to eq 0
+ expect(backordered).to eq 20
+ end
+
+ context 'when backordering is not allowed' do
+ before do
+ @stock_item = mock_model(StockItem, backorderable?: false)
+ expect(subject).to receive(:stock_item).with(variant).and_return(@stock_item)
+ end
+
+ it 'all on_hand' do
+ allow(@stock_item).to receive_messages(count_on_hand: 10)
+
+ on_hand, backordered = subject.fill_status(variant, 5)
+ expect(on_hand).to eq 5
+ expect(backordered).to eq 0
+ end
+
+ it 'some on_hand' do
+ allow(@stock_item).to receive_messages(count_on_hand: 10)
+
+ on_hand, backordered = subject.fill_status(variant, 20)
+ expect(on_hand).to eq 10
+ expect(backordered).to eq 0
+ end
+
+ it 'zero on_hand' do
+ allow(@stock_item).to receive_messages(count_on_hand: 0)
+
+ on_hand, backordered = subject.fill_status(variant, 20)
+ expect(on_hand).to eq 0
+ expect(backordered).to eq 0
+ end
+ end
+
+ context 'without stock_items' do
+ subject { create(:stock_location) }
+
+ let(:variant) { create(:base_variant) }
+
+ it 'zero on_hand and backordered' do
+ subject
+ variant.stock_items.destroy_all
+ on_hand, backordered = subject.fill_status(variant, 1)
+ expect(on_hand).to eq 0
+ expect(backordered).to eq 0
+ end
+ end
+ end
+
+ context '#state_text' do
+ context 'state is blank' do
+ subject { StockLocation.create(name: 'testing', state: nil, state_name: 'virginia') }
+
+ specify { expect(subject.state_text).to eq('virginia') }
+ end
+
+ context 'both name and abbr is present' do
+ subject { StockLocation.create(name: 'testing', state: state, state_name: nil) }
+
+ let(:state) { stub_model(Spree::State, name: 'virginia', abbr: 'va') }
+
+ specify { expect(subject.state_text).to eq('va') }
+ end
+
+ context 'only name is present' do
+ subject { StockLocation.create(name: 'testing', state: state, state_name: nil) }
+
+ let(:state) { stub_model(Spree::State, name: 'virginia', abbr: nil) }
+
+ specify { expect(subject.state_text).to eq('virginia') }
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock_movement_spec.rb b/core/spec/models/spree/stock_movement_spec.rb
new file mode 100644
index 00000000000..0345c160969
--- /dev/null
+++ b/core/spec/models/spree/stock_movement_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Spree::StockMovement, type: :model do
+ describe 'Constants' do
+ describe 'QUANTITY_LIMITS[:max]' do
+ it 'return 2**31 - 1' do
+ expect(Spree::StockMovement::QUANTITY_LIMITS[:max]).to eq(2**31 - 1)
+ end
+ end
+
+ describe 'QUANTITY_LIMITS[:min]' do
+ it 'return -2**31' do
+ expect(Spree::StockMovement::QUANTITY_LIMITS[:min]).to eq(-2**31)
+ end
+ end
+ end
+
+ describe 'Scope' do
+ describe '.recent' do
+ it 'orders chronologically by created at' do
+ expect(Spree::StockMovement.recent.to_sql).
+ to eq Spree::StockMovement.unscoped.order(created_at: :desc).to_sql
+ end
+ end
+ end
+
+ describe 'whitelisted ransackable attributes' do
+ it 'returns amount attribute' do
+ expect(Spree::StockMovement.whitelisted_ransackable_attributes).to eq(['quantity'])
+ end
+ end
+
+ describe 'Insatance Methods' do
+ let(:stock_location) { create(:stock_location_with_items) }
+ let(:stock_item) { stock_location.stock_items.order(:id).first }
+
+ describe '#readonly?' do
+ let(:stock_movement) { create(:stock_movement, stock_item: stock_item) }
+
+ it 'does not update a persisted record' do
+ expect { stock_movement.save }.to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
+ end
+
+ describe '#update_stock_item_quantity' do
+ let(:stock_movement) { build(:stock_movement, stock_item: stock_item) }
+
+ context 'when track inventory levels is false' do
+ before do
+ Spree::Config[:track_inventory_levels] = false
+ stock_movement.quantity = 1
+ stock_movement.save
+ stock_item.reload
+ end
+
+ it 'does not update count on hand' do
+ expect(stock_item.count_on_hand).to eq(10)
+ end
+ end
+
+ context 'when track inventory tracking is off' do
+ before do
+ stock_item.variant.track_inventory = false
+ stock_movement.quantity = 1
+ stock_movement.save
+ stock_item.reload
+ end
+
+ it 'does not update count on hand' do
+ expect(stock_item.count_on_hand).to eq(10)
+ end
+ end
+
+ context 'when quantity is negative' do
+ before do
+ stock_movement.quantity = -1
+ stock_movement.save
+ stock_item.reload
+ end
+
+ it 'decrements the stock item count on hand' do
+ expect(stock_item.count_on_hand).to eq(9)
+ end
+ end
+
+ context 'when quantity is positive' do
+ before do
+ stock_movement.quantity = 1
+ stock_movement.save
+ stock_item.reload
+ end
+
+ it 'increments the stock item count on hand' do
+ expect(stock_item.count_on_hand).to eq(11)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/stock_transfer_spec.rb b/core/spec/models/spree/stock_transfer_spec.rb
new file mode 100644
index 00000000000..46b5d136bb3
--- /dev/null
+++ b/core/spec/models/spree/stock_transfer_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+module Spree
+ describe StockTransfer, type: :model do
+ subject { StockTransfer.create(reference: 'PO123') }
+
+ let(:destination_location) { create(:stock_location_with_items) }
+ let(:source_location) { create(:stock_location_with_items) }
+ let(:stock_item) { source_location.stock_items.order(:id).first }
+ let(:variant) { stock_item.variant }
+
+ describe '#reference' do
+ subject { super().reference }
+
+ it { is_expected.to eq 'PO123' }
+ end
+
+ describe '#to_param' do
+ subject { super().to_param }
+
+ it { is_expected.to match(/T\d+/) }
+ end
+
+ it 'transfers variants between 2 locations' do
+ variants = { variant => 5 }
+
+ subject.transfer(source_location,
+ destination_location,
+ variants)
+
+ expect(source_location.count_on_hand(variant)).to eq 5
+ expect(destination_location.count_on_hand(variant)).to eq 5
+
+ expect(subject.source_location).to eq source_location
+ expect(subject.destination_location).to eq destination_location
+
+ expect(subject.source_movements.first.quantity).to eq(-5)
+ expect(subject.destination_movements.first.quantity).to eq 5
+ end
+
+ it 'receive new inventory (from a vendor)' do
+ variants = { variant => 5 }
+
+ subject.receive(destination_location, variants)
+
+ expect(destination_location.count_on_hand(variant)).to eq 5
+
+ expect(subject.source_location).to be_nil
+ expect(subject.destination_location).to eq destination_location
+ end
+ end
+end
diff --git a/core/spec/models/spree/store_credit_category_spec.rb b/core/spec/models/spree/store_credit_category_spec.rb
new file mode 100644
index 00000000000..d16af640521
--- /dev/null
+++ b/core/spec/models/spree/store_credit_category_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+describe 'StoreCreditCategory' do
+ describe 'callbacks' do
+ context 'store credit category is not used in store credit' do
+ let!(:store_credit_category) { create(:store_credit_category) }
+
+ it 'can delete store credit category' do
+ expect { store_credit_category.destroy }.to change(Spree::StoreCreditCategory, :count).by(-1)
+ end
+ end
+
+ context 'store credit category is used in store credit' do
+ let!(:store_credit_category) { create(:store_credit_category) }
+ let!(:store_credit) { create(:store_credit, category_id: store_credit_category.id) }
+
+ it 'can not delete store credit category' do
+ store_credit_category.destroy
+ expect(store_credit_category.errors[:base]).to include(
+ I18n.t('activerecord.errors.models.spree/store_credit_category.attributes.base.cannot_destroy_if_used_in_store_credit')
+ )
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/store_credit_event_spec.rb b/core/spec/models/spree/store_credit_event_spec.rb
new file mode 100644
index 00000000000..37178925a3d
--- /dev/null
+++ b/core/spec/models/spree/store_credit_event_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe 'StoreCreditEvent' do
+ describe '#display_amount' do
+ subject { create(:store_credit_auth_event, amount: event_amount) }
+
+ let(:event_amount) { 120.0 }
+
+ it 'returns a Spree::Money instance' do
+ expect(subject.display_amount).to be_instance_of(Spree::Money)
+ end
+
+ it 'uses the events amount attribute' do
+ expect(subject.display_amount).to eq Spree::Money.new(event_amount, currency: subject.currency)
+ end
+ end
+
+ describe '#display_user_total_amount' do
+ subject { create(:store_credit_auth_event, user_total_amount: user_total_amount) }
+
+ let(:user_total_amount) { 300.0 }
+
+ it 'returns a Spree::Money instance' do
+ expect(subject.display_user_total_amount).to be_instance_of(Spree::Money)
+ end
+
+ it 'uses the events user_total_amount attribute' do
+ amount = Spree::Money.new(user_total_amount, currency: subject.currency)
+ expect(subject.display_user_total_amount).to eq amount
+ end
+ end
+
+ describe '#display_action' do
+ subject { create(:store_credit_auth_event, action: action) }
+
+ context 'capture event' do
+ let(:action) { Spree::StoreCredit::CAPTURE_ACTION }
+
+ it 'returns used' do
+ expect(subject.display_action).to eq Spree.t('store_credit.captured')
+ end
+ end
+
+ context 'authorize event' do
+ let(:action) { Spree::StoreCredit::AUTHORIZE_ACTION }
+
+ it 'returns authorized' do
+ expect(subject.display_action).to eq Spree.t('store_credit.authorized')
+ end
+ end
+
+ context 'allocation event' do
+ let(:action) { Spree::StoreCredit::ALLOCATION_ACTION }
+
+ it 'returns added' do
+ expect(subject.display_action).to eq Spree.t('store_credit.allocated')
+ end
+ end
+
+ context 'void event' do
+ let(:action) { Spree::StoreCredit::VOID_ACTION }
+
+ it 'returns credit' do
+ expect(subject.display_action).to eq Spree.t('store_credit.credit')
+ end
+ end
+
+ context 'credit event' do
+ let(:action) { Spree::StoreCredit::CREDIT_ACTION }
+
+ it 'returns credit' do
+ expect(subject.display_action).to eq Spree.t('store_credit.credit')
+ end
+ end
+ end
+
+ describe '#order' do
+ context 'there is no associated payment with the event' do
+ subject { create(:store_credit_auth_event) }
+
+ it 'returns nil' do
+ expect(subject.order).to be_nil
+ end
+ end
+
+ context 'there is an associated payment with the event' do
+ subject do
+ create(:store_credit_auth_event, action: Spree::StoreCredit::CAPTURE_ACTION,
+ authorization_code: authorization_code)
+ end
+
+ let(:authorization_code) { '1-SC-TEST' }
+ let(:order) { create(:order) }
+ let!(:payment) { create(:store_credit_payment, order: order, response_code: authorization_code) }
+
+ it 'returns the order associated with the payment' do
+ expect(subject.order).to eq order
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/store_credit_spec.rb b/core/spec/models/spree/store_credit_spec.rb
new file mode 100644
index 00000000000..37daa0133ec
--- /dev/null
+++ b/core/spec/models/spree/store_credit_spec.rb
@@ -0,0 +1,790 @@
+require 'spec_helper'
+
+describe 'StoreCredit' do
+ let(:currency) { 'TEST' }
+ let(:store_credit) { build(:store_credit, store_credit_attrs) }
+ let(:store_credit_attrs) { {} }
+
+ describe 'callbacks' do
+ subject { store_credit.save }
+
+ context 'amount used is greater than zero' do
+ subject { store_credit.destroy }
+
+ let(:store_credit) { create(:store_credit, amount: 100, amount_used: 1) }
+ let(:validation_message) { I18n.t('activerecord.errors.models.spree/store_credit.attributes.amount_used.greater_than_zero_restrict_delete') }
+
+ it 'can not delete the store credit' do
+ subject
+ expect(store_credit.reload).to eq store_credit
+ expect(store_credit.errors[:amount_used]).to include(validation_message)
+ end
+ end
+
+ context 'category is a non-expiring type' do
+ let!(:secondary_credit_type) { create(:secondary_credit_type) }
+ let(:store_credit) { build(:store_credit, credit_type: nil) }
+
+ before { allow(store_credit.category).to receive(:non_expiring?).and_return(true) }
+
+ it 'sets the credit type to non-expiring' do
+ subject
+ expect(store_credit.credit_type.name).to eq secondary_credit_type.name
+ end
+ end
+
+ context 'category is an expiring type' do
+ before { allow(store_credit.category).to receive(:non_expiring?).and_return(false) }
+
+ it 'sets the credit type to non-expiring' do
+ subject
+ expect(store_credit.credit_type.name).to eq 'Expiring'
+ end
+ end
+
+ context 'the type is set' do
+ let!(:secondary_credit_type) { create(:secondary_credit_type) }
+ let(:store_credit) { build(:store_credit, credit_type: secondary_credit_type) }
+
+ before { allow(store_credit.category).to receive(:non_expiring?).and_return(false) }
+
+ it "doesn't overwrite the type" do
+ expect { subject }.not_to change(store_credit, :credit_type)
+ end
+ end
+ end
+
+ describe 'validations' do
+ describe 'used amount should not be greater than the credited amount' do
+ context 'the used amount is defined' do
+ let(:invalid_store_credit) { build(:store_credit, amount: 100, amount_used: 150) }
+
+ it 'is not valid' do
+ expect(invalid_store_credit).not_to be_valid
+ end
+
+ it 'sets the correct error message' do
+ invalid_store_credit.valid?
+ attribute_name = I18n.t('activerecord.attributes.spree/store_credit.amount_used')
+ validation_message = I18n.t('activerecord.errors.models.spree/store_credit.attributes.amount_used.cannot_be_greater_than_amount')
+ expected_error_message = "#{attribute_name} #{validation_message}"
+ expect(invalid_store_credit.errors.full_messages).to include(expected_error_message)
+ end
+ end
+
+ context 'the used amount is not defined yet' do
+ let(:store_credit) { build(:store_credit, amount: 100) }
+
+ it 'is valid' do
+ expect(store_credit).to be_valid
+ end
+ end
+ end
+
+ describe 'amount used less than or equal to amount' do
+ subject { build(:store_credit, amount_used: 101.0, amount: 100.0) }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ end
+
+ it 'adds an error message about the invalid amount used' do
+ subject.valid?
+ text = I18n.t('activerecord.errors.models.spree/store_credit.attributes.amount_used.cannot_be_greater_than_amount')
+ expect(subject.errors[:amount_used]).to include(text)
+ end
+ end
+
+ describe 'amount authorized less than or equal to amount' do
+ subject { build(:store_credit, amount_authorized: 101.0, amount: 100.0) }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ end
+
+ it 'adds an error message about the invalid authorized amount' do
+ subject.valid?
+ text = I18n.t('activerecord.errors.models.spree/store_credit.attributes.amount_authorized.exceeds_total_credits')
+ expect(subject.errors[:amount_authorized]).to include(text)
+ end
+ end
+ end
+
+ describe '#display_amount' do
+ it 'returns a Spree::Money instance' do
+ expect(store_credit.display_amount).to be_instance_of(Spree::Money)
+ end
+ end
+
+ describe '#display_amount_used' do
+ it 'returns a Spree::Money instance' do
+ expect(store_credit.display_amount_used).to be_instance_of(Spree::Money)
+ end
+ end
+
+ describe '#amount_remaining' do
+ context 'the amount_used is not defined' do
+ context 'the authorized amount is not defined' do
+ it 'returns the credited amount' do
+ expect(store_credit.amount_remaining).to eq store_credit.amount
+ end
+ end
+
+ context 'the authorized amount is defined' do
+ let(:authorized_amount) { 15.00 }
+
+ before { store_credit.update_attributes(amount_authorized: authorized_amount) }
+
+ it 'subtracts the authorized amount from the credited amount' do
+ expect(store_credit.amount_remaining).to eq (store_credit.amount - authorized_amount)
+ end
+ end
+ end
+
+ context 'the amount_used is defined' do
+ let(:amount_used) { 10.0 }
+
+ before { store_credit.update_attributes(amount_used: amount_used) }
+
+ context 'the authorized amount is not defined' do
+ it 'subtracts the amount used from the credited amount' do
+ expect(store_credit.amount_remaining).to eq (store_credit.amount - amount_used)
+ end
+ end
+
+ context 'the authorized amount is defined' do
+ let(:authorized_amount) { 15.00 }
+
+ before { store_credit.update_attributes(amount_authorized: authorized_amount) }
+
+ it 'subtracts the amount used and the authorized amount from the credited amount' do
+ expect(store_credit.amount_remaining).to eq (store_credit.amount - amount_used - authorized_amount)
+ end
+ end
+ end
+ end
+
+ describe '#authorize' do
+ context 'amount is valid' do
+ let(:authorization_amount) { 1.0 }
+ let(:added_authorization_amount) { 3.0 }
+ let(:originator) { nil }
+
+ context 'amount has not been authorized yet' do
+ before { store_credit.update_attributes(amount_authorized: authorization_amount) }
+
+ it 'returns true' do
+ expect(store_credit.authorize(store_credit.amount - authorization_amount, store_credit.currency)).to be_truthy
+ end
+
+ it 'adds the new amount to authorized amount' do
+ store_credit.authorize(added_authorization_amount, store_credit.currency)
+ expect(store_credit.reload.amount_authorized).to eq (authorization_amount + added_authorization_amount)
+ end
+
+ context 'originator is present' do
+ subject do
+ store_credit.authorize(added_authorization_amount, store_credit.currency,
+ action_originator: originator)
+ end
+
+ let(:originator) { create(:refund, amount: 10) }
+
+ it 'records the originator' do
+ expect { subject }.to change { Spree::StoreCreditEvent.count }.by(1)
+ expect(Spree::StoreCreditEvent.last.originator).to eq originator
+ end
+ end
+ end
+
+ context 'authorization has already happened' do
+ let!(:auth_event) { create(:store_credit_auth_event, store_credit: store_credit) }
+
+ before { store_credit.update_attributes(amount_authorized: store_credit.amount) }
+
+ it 'returns true' do
+ expect(store_credit.authorize(store_credit.amount, store_credit.currency,
+ action_authorization_code: auth_event.authorization_code)).to be true
+ end
+ end
+ end
+
+ context 'amount is invalid' do
+ it 'returns false' do
+ expect(store_credit.authorize(store_credit.amount * 2, store_credit.currency)).to be false
+ end
+ end
+ end
+
+ describe '#validate_authorization' do
+ context 'insufficient funds' do
+ subject { store_credit.validate_authorization(store_credit.amount * 2, store_credit.currency) }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'adds an error to the model' do
+ subject
+ text = Spree.t('store_credit_payment_method.insufficient_funds')
+ expect(store_credit.errors.full_messages).to include(text)
+ end
+ end
+
+ context 'currency mismatch' do
+ subject { store_credit.validate_authorization(store_credit.amount, 'EUR') }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'adds an error to the model' do
+ subject
+ text = Spree.t('store_credit_payment_method.currency_mismatch')
+ expect(store_credit.errors.full_messages).to include(text)
+ end
+ end
+
+ context 'valid authorization' do
+ subject { store_credit.validate_authorization(store_credit.amount, store_credit.currency) }
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'troublesome floats' do
+ # 8.21.to_d < 8.21 => true
+ subject { store_credit.validate_authorization(store_credit_attrs[:amount], store_credit.currency) }
+
+ let(:store_credit_attrs) { { amount: 8.21 } }
+
+ it 'returns true' do
+ expect(subject).to be_truthy
+ end
+ end
+ end
+
+ describe '#capture' do
+ let(:authorized_amount) { 10.00 }
+ let(:auth_code) { '23-SC-20140602164814476128' }
+
+ before do
+ store_credit.update_attributes(amount_authorized: authorized_amount, amount_used: 0.0)
+ allow(store_credit).to receive_messages(authorize: true)
+ end
+
+ context 'insufficient funds' do
+ subject { store_credit.capture(authorized_amount * 2, auth_code, store_credit.currency) }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'adds an error to the model' do
+ subject
+ text = Spree.t('store_credit_payment_method.insufficient_authorized_amount')
+ expect(store_credit.errors.full_messages).to include(text)
+ end
+
+ it 'does not update the store credit model' do
+ expect { subject }.not_to change { store_credit }
+ end
+ end
+
+ context 'currency mismatch' do
+ subject { store_credit.capture(authorized_amount, auth_code, 'EUR') }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'adds an error to the model' do
+ subject
+ text = Spree.t('store_credit_payment_method.currency_mismatch')
+ expect(store_credit.errors.full_messages).to include(text)
+ end
+
+ it 'does not update the store credit model' do
+ expect { subject }.not_to change { store_credit }
+ end
+ end
+
+ context 'valid capture' do
+ subject do
+ amount = authorized_amount - remaining_authorized_amount
+ store_credit.capture(amount, auth_code, store_credit.currency,
+ action_originator: originator)
+ end
+
+ let(:remaining_authorized_amount) { 1 }
+ let(:originator) { nil }
+
+ it 'returns true' do
+ expect(subject).to be_truthy
+ end
+
+ it 'updates the authorized amount to the difference between the captured amount and the authorized amount' do
+ subject
+ expect(store_credit.reload.amount_authorized).to eq remaining_authorized_amount
+ end
+
+ it 'updates the used amount to the current used amount plus the captured amount' do
+ subject
+ expect(store_credit.reload.amount_used).to eq authorized_amount - remaining_authorized_amount
+ end
+
+ context 'originator is present' do
+ let(:originator) { create(:refund, amount: 10) }
+
+ it 'records the originator' do
+ expect { subject }.to change { Spree::StoreCreditEvent.count }.by(1)
+ expect(Spree::StoreCreditEvent.last.originator).to eq originator
+ end
+ end
+ end
+ end
+
+ describe '#void' do
+ subject do
+ store_credit.void(auth_code, action_originator: originator)
+ end
+
+ let(:auth_code) { '1-SC-20141111111111' }
+ let(:store_credit) { create(:store_credit, amount_used: 150.0) }
+ let(:originator) { nil }
+
+ context 'no event found for auth_code' do
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'adds an error to the model' do
+ subject
+ text = Spree.t('store_credit_payment_method.unable_to_void', auth_code: auth_code)
+ expect(store_credit.errors.full_messages).to include(text)
+ end
+ end
+
+ context 'capture event found for auth_code' do
+ let(:captured_amount) { 10.0 }
+ let!(:capture_event) do
+ create(:store_credit_auth_event,
+ action: Spree::StoreCredit::CAPTURE_ACTION,
+ authorization_code: auth_code,
+ amount: captured_amount,
+ store_credit: store_credit)
+ end
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'does not change the amount used on the store credit' do
+ expect { subject }.not_to change { store_credit.amount_used.to_f }
+ end
+ end
+
+ context 'auth event found for auth_code' do
+ let(:authorized_amount) { 10.0 }
+ let!(:auth_event) do
+ create(:store_credit_auth_event,
+ authorization_code: auth_code,
+ amount: authorized_amount,
+ store_credit: store_credit)
+ end
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
+
+ it 'returns the capture amount to the store credit' do
+ expect { subject }.to change { store_credit.amount_authorized.to_f }.by(-authorized_amount)
+ end
+
+ context 'originator is present' do
+ let(:originator) { create(:refund, amount: 10) }
+
+ it 'records the originator' do
+ expect { subject }.to change { Spree::StoreCreditEvent.count }.by(1)
+ expect(Spree::StoreCreditEvent.last.originator).to eq originator
+ end
+ end
+ end
+ end
+
+ describe '#credit' do
+ subject do
+ store_credit.credit(credit_amount, auth_code, currency, action_originator: originator)
+ end
+
+ let(:event_auth_code) { '1-SC-20141111111111' }
+ let(:amount_used) { 10.0 }
+ let(:store_credit) { create(:store_credit, amount_used: amount_used) }
+ let!(:capture_event) do
+ create(:store_credit_auth_event,
+ action: Spree::StoreCredit::CAPTURE_ACTION,
+ authorization_code: event_auth_code,
+ amount: captured_amount,
+ store_credit: store_credit)
+ end
+ let(:originator) { nil }
+
+ context 'currency does not match' do
+ let(:currency) { 'AUD' }
+ let(:credit_amount) { 5.0 }
+ let(:captured_amount) { 100.0 }
+ let(:auth_code) { event_auth_code }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'adds an error message about the currency mismatch' do
+ subject
+ text = Spree.t('store_credit_payment_method.currency_mismatch')
+ expect(store_credit.errors.full_messages).to include(text)
+ end
+ end
+
+ context 'unable to find capture event' do
+ let(:currency) { 'USD' }
+ let(:credit_amount) { 5.0 }
+ let(:captured_amount) { 100.0 }
+ let(:auth_code) { 'UNKNOWN_CODE' }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'adds an error message about the currency mismatch' do
+ subject
+ text = Spree.t('store_credit_payment_method.unable_to_credit', auth_code: auth_code)
+ expect(store_credit.errors.full_messages).to include(text)
+ end
+ end
+
+ context 'amount is more than what is captured' do
+ let(:currency) { 'USD' }
+ let(:credit_amount) { 100.0 }
+ let(:captured_amount) { 5.0 }
+ let(:auth_code) { event_auth_code }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+
+ it 'adds an error message about the currency mismatch' do
+ subject
+ text = Spree.t('store_credit_payment_method.unable_to_credit', auth_code: auth_code)
+ expect(store_credit.errors.full_messages).to include(text)
+ end
+ end
+
+ context 'amount is successfully credited' do
+ let(:currency) { 'USD' }
+ let(:credit_amount) { 5.0 }
+ let(:captured_amount) { 100.0 }
+ let(:auth_code) { event_auth_code }
+
+ context 'credit_to_new_allocation is set' do
+ before { Spree::Config[:credit_to_new_allocation] = true }
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
+
+ it 'creates a new store credit record' do
+ expect { subject }.to change { Spree::StoreCredit.count }.by(1)
+ end
+
+ it 'does not create a new store credit event on the parent store credit' do
+ expect { subject }.not_to change { store_credit.store_credit_events.count }
+ end
+
+ context 'credits the passed amount to a new store credit record' do
+ before do
+ subject
+ @new_store_credit = Spree::StoreCredit.last
+ end
+
+ it 'does not set the amount used on hte originating store credit' do
+ expect(store_credit.reload.amount_used).to eq amount_used
+ end
+
+ it 'sets the correct amount on the new store credit' do
+ expect(@new_store_credit.amount).to eq credit_amount
+ end
+
+ [:user_id, :category_id, :created_by_id, :currency, :type_id].each do |attr|
+ it "sets attribute #{attr} inherited from the originating store credit" do
+ expect(@new_store_credit.send(attr)).to eq store_credit.send(attr)
+ end
+ end
+
+ it 'sets a memo' do
+ expect(@new_store_credit.memo).to eq "This is a credit from store credit ID #{store_credit.id}"
+ end
+ end
+
+ context 'originator is present' do
+ let(:originator) { create(:refund, amount: 10) }
+
+ it 'records the originator' do
+ expect { subject }.to change { Spree::StoreCreditEvent.count }.by(1)
+ expect(Spree::StoreCreditEvent.last.originator).to eq originator
+ end
+ end
+ end
+
+ context 'credit_to_new_allocation is not set' do
+ it 'returns true' do
+ expect(subject).to be true
+ end
+
+ it 'credits the passed amount to the store credit amount used' do
+ subject
+ expect(store_credit.reload.amount_used).to eq (amount_used - credit_amount)
+ end
+
+ it 'creates a new store credit event' do
+ expect { subject }.to change { store_credit.store_credit_events.count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe '#amount_used' do
+ context 'amount used is not defined' do
+ subject { Spree::StoreCredit.new }
+
+ it 'returns zero' do
+ expect(subject.amount_used).to be_zero
+ end
+ end
+
+ context 'amount used is defined' do
+ subject { create(:store_credit, amount_used: amount_used) }
+
+ let(:amount_used) { 100.0 }
+
+ it 'returns the attribute value' do
+ expect(subject.amount_used).to eq amount_used
+ end
+ end
+ end
+
+ describe '#amount_authorized' do
+ context 'amount authorized is not defined' do
+ subject { Spree::StoreCredit.new }
+
+ it 'returns zero' do
+ expect(subject.amount_authorized).to be_zero
+ end
+ end
+
+ context 'amount authorized is defined' do
+ subject { create(:store_credit, amount_authorized: amount_authorized) }
+
+ let(:amount_authorized) { 100.0 }
+
+ it 'returns the attribute value' do
+ expect(subject.amount_authorized).to eq amount_authorized
+ end
+ end
+ end
+
+ describe '#can_capture?' do
+ subject { store_credit.can_capture?(payment) }
+
+ let(:store_credit) { create(:store_credit) }
+ let(:payment) { create(:payment, state: payment_state) }
+
+ context 'pending payment' do
+ let(:payment_state) { 'pending' }
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'checkout payment' do
+ let(:payment_state) { 'checkout' }
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'void payment' do
+ let(:payment_state) { Spree::StoreCredit::VOID_ACTION }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'invalid payment' do
+ let(:payment_state) { 'invalid' }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'complete payment' do
+ let(:payment_state) { 'completed' }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+ end
+ end
+
+ describe '#can_void?' do
+ subject { store_credit.can_void?(payment) }
+
+ let(:store_credit) { create(:store_credit) }
+ let(:payment) { create(:payment, state: payment_state) }
+
+ context 'pending payment' do
+ let(:payment_state) { 'pending' }
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'remove store credits' do
+ let(:payment_state) { :checkout }
+
+ context 'when payment is in checkout and order is not completed' do
+ it { is_expected.to be true }
+ end
+
+ context 'when order is completed' do
+ before { payment.order.update_column(:completed_at, Time.current) }
+
+ it { is_expected.to be false }
+ end
+
+ context 'when payment is completed' do
+ before { payment.update_column(:state, :completed) }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ context 'void payment' do
+ let(:payment_state) { Spree::StoreCredit::VOID_ACTION }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'invalid payment' do
+ let(:payment_state) { 'invalid' }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'complete payment' do
+ let(:payment_state) { 'completed' }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+ end
+ end
+
+ describe '#can_credit?' do
+ subject { store_credit.can_credit?(payment) }
+
+ let(:store_credit) { create(:store_credit) }
+ let(:payment) { create(:payment, state: payment_state) }
+
+ context 'payment is not completed' do
+ let(:payment_state) { 'pending' }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'payment is completed' do
+ let(:payment_state) { 'completed' }
+
+ context 'credit is owed on the order' do
+ before { allow(payment.order).to receive_messages(payment_state: 'credit_owed') }
+
+ context "payment doesn't have allowed credit" do
+ before { allow(payment).to receive_messages(credit_allowed: 0.0) }
+
+ it 'returns false' do
+ expect(subject).to be false
+ end
+ end
+
+ context 'payment has allowed credit' do
+ before { allow(payment).to receive_messages(credit_allowed: 5.0) }
+
+ it 'returns true' do
+ expect(subject).to be true
+ end
+ end
+ end
+ end
+
+ describe '#store_events' do
+ context 'create' do
+ context 'user has one store credit' do
+ subject { create(:store_credit, amount: store_credit_amount) }
+
+ let(:store_credit_amount) { 100.0 }
+
+ it 'creates a store credit event' do
+ expect { subject }.to change { Spree::StoreCreditEvent.count }.by(1)
+ end
+
+ it 'makes the store credit event an allocation event' do
+ expect(subject.store_credit_events.first.action).to eq Spree::StoreCredit::ALLOCATION_ACTION
+ end
+
+ it "saves the user's total store credit in the event" do
+ expect(subject.store_credit_events.first.user_total_amount).to eq store_credit_amount
+ end
+ end
+
+ context 'user has multiple store credits' do
+ subject { create(:store_credit, user: user, amount: additional_store_credit_amount) }
+
+ let(:store_credit_amount) { 100.0 }
+ let(:additional_store_credit_amount) { 200.0 }
+
+ let(:user) { create(:user) }
+ let!(:store_credit) { create(:store_credit, user: user, amount: store_credit_amount) }
+
+ it "saves the user's total store credit in the event" do
+ amount = store_credit_amount + additional_store_credit_amount
+ expect(subject.store_credit_events.first.user_total_amount).to eq amount
+ end
+ end
+
+ context 'an action is specified' do
+ it 'creates an event with the set action' do
+ store_credit = build(:store_credit)
+ store_credit.action = Spree::StoreCredit::VOID_ACTION
+ store_credit.action_authorization_code = '1-SC-TEST'
+
+ expect { store_credit.save! }.to change {
+ Spree::StoreCreditEvent.where(action: Spree::StoreCredit::VOID_ACTION).count
+ }.by(1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/store_spec.rb b/core/spec/models/spree/store_spec.rb
new file mode 100644
index 00000000000..bea8fad9d34
--- /dev/null
+++ b/core/spec/models/spree/store_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe Spree::Store, type: :model do
+ describe '.by_url' do
+ let!(:store) { create(:store, url: "website1.com\nwww.subdomain.com") }
+ let!(:store_2) { create(:store, url: 'freethewhales.com') }
+
+ it 'finds stores by url' do
+ by_domain = Spree::Store.by_url('www.subdomain.com')
+
+ expect(by_domain).to include(store)
+ expect(by_domain).not_to include(store_2)
+ end
+ end
+
+ describe '.current' do
+ # there is a default store created with the test_app rake task.
+ let!(:store_1) { Spree::Store.first || create(:store) }
+
+ let!(:store_2) { create(:store, default: false, url: 'www.subdomain.com') }
+
+ it 'returns default when no domain' do
+ expect(subject.class.current).to eql(store_1)
+ end
+
+ it 'returns store for domain' do
+ expect(subject.class.current('spreecommerce.com')).to eql(store_1)
+ expect(subject.class.current('www.subdomain.com')).to eql(store_2)
+ end
+ end
+
+ describe '.default' do
+ context 'when a default store is already present' do
+ let!(:store) { create(:store) }
+ let!(:store_2) { create(:store, default: true) }
+
+ it 'returns the already existing default store' do
+ expect(Spree::Store.default).to eq(store_2)
+ end
+
+ it "ensures there is a default if one doesn't exist yet" do
+ expect(store_2.default).to be true
+ end
+
+ it 'ensures there is only one default' do
+ [store, store_2].each(&:reload)
+
+ expect(Spree::Store.where(default: true).count).to eq(1)
+ expect(store_2.default).to be true
+ expect(store.default).not_to be true
+ end
+
+ context 'when store is not saved' do
+ before do
+ store.default = true
+ store.code = nil
+ store.save
+ end
+
+ it 'ensure old default location still default' do
+ [store, store_2].each(&:reload)
+ expect(store.default).to be false
+ expect(store_2.default).to be true
+ end
+ end
+ end
+
+ context 'when a default store is not present' do
+ it 'builds a new default store' do
+ expect(Spree::Store.default.class).to eq(Spree::Store)
+ expect(Spree::Store.default.persisted?).to eq(false)
+ expect(Spree::Store.default.default).to be(true)
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/tax_category_spec.rb b/core/spec/models/spree/tax_category_spec.rb
new file mode 100644
index 00000000000..5822ff6e6fd
--- /dev/null
+++ b/core/spec/models/spree/tax_category_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Spree::TaxCategory, type: :model do
+ context 'default tax category' do
+ let(:tax_category) { create(:tax_category) }
+ let(:new_tax_category) { create(:tax_category) }
+
+ before do
+ tax_category.update_column(:is_default, true)
+ end
+
+ it 'undefaults the previous default tax category' do
+ new_tax_category.update_attributes(is_default: true)
+ expect(new_tax_category.is_default).to be true
+
+ tax_category.reload
+ expect(tax_category.is_default).to be false
+ end
+
+ it 'undefaults the previous default tax category except when updating the existing default tax category' do
+ tax_category.update_column(:description, 'Updated description')
+
+ tax_category.reload
+ expect(tax_category.is_default).to be true
+ end
+ end
+end
diff --git a/core/spec/models/spree/tax_rate_spec.rb b/core/spec/models/spree/tax_rate_spec.rb
new file mode 100644
index 00000000000..2114832a4b8
--- /dev/null
+++ b/core/spec/models/spree/tax_rate_spec.rb
@@ -0,0 +1,560 @@
+require 'spec_helper'
+
+describe Spree::TaxRate, type: :model do
+ context 'match' do
+ let(:order) { create(:order) }
+ let(:country) { create(:country) }
+ let(:tax_category) { create(:tax_category) }
+ let(:calculator) { Spree::Calculator::FlatRate.new }
+
+ it 'returns an empty array when tax_zone is nil' do
+ allow(order).to receive_messages tax_zone: nil
+ expect(Spree::TaxRate.match(order.tax_zone)).to eq([])
+ end
+
+ context 'when no rate zones match the tax zone' do
+ before do
+ Spree::TaxRate.create(amount: 1, zone: create(:zone))
+ end
+
+ context 'when there is no default tax zone' do
+ before do
+ @zone = create(:zone, name: 'Country Zone', default_tax: false, zone_members: [])
+ @zone.zone_members.create(zoneable: country)
+ end
+
+ it 'returns an empty array' do
+ allow(order).to receive_messages tax_zone: @zone
+ expect(Spree::TaxRate.match(order.tax_zone)).to eq([])
+ end
+
+ it 'returns the rate that matches the rate zone' do
+ rate = Spree::TaxRate.create(
+ amount: 1,
+ zone: @zone,
+ tax_category: tax_category,
+ calculator: calculator
+ )
+
+ allow(order).to receive_messages tax_zone: @zone
+ expect(Spree::TaxRate.match(order.tax_zone)).to eq([rate])
+ end
+
+ it 'returns all rates that match the rate zone' do
+ rate1 = Spree::TaxRate.create(
+ amount: 1,
+ zone: @zone,
+ tax_category: tax_category,
+ calculator: calculator
+ )
+
+ rate2 = Spree::TaxRate.create(
+ amount: 2,
+ zone: @zone,
+ tax_category: tax_category,
+ calculator: Spree::Calculator::FlatRate.new
+ )
+
+ allow(order).to receive_messages tax_zone: @zone
+ expect(Spree::TaxRate.match(order.tax_zone)).to match_array([rate1, rate2])
+ end
+
+ context 'when the tax_zone is contained within a rate zone' do
+ before do
+ sub_zone = create(:zone, name: 'State Zone', zone_members: [])
+ sub_zone.zone_members.create(zoneable: create(:state, country: country))
+ allow(order).to receive_messages tax_zone: sub_zone
+ @rate = Spree::TaxRate.create(
+ amount: 1,
+ zone: @zone,
+ tax_category: tax_category,
+ calculator: calculator
+ )
+ end
+
+ it 'returns the rate zone' do
+ expect(Spree::TaxRate.match(order.tax_zone)).to eq([@rate])
+ end
+ end
+ end
+
+ context 'when there is a default tax zone' do
+ subject { Spree::TaxRate.match(order.tax_zone) }
+
+ before do
+ @zone = create(:zone, name: 'Country Zone', default_tax: true, zone_members: [])
+ @zone.zone_members.create(zoneable: country)
+ end
+
+ let(:included_in_price) { false }
+ let!(:rate) do
+ Spree::TaxRate.create(amount: 1,
+ zone: @zone,
+ tax_category: tax_category,
+ calculator: calculator,
+ included_in_price: included_in_price)
+ end
+
+ context 'when the order has the same tax zone' do
+ before do
+ allow(order).to receive_messages tax_zone: @zone
+ end
+
+ context 'when the tax is not a VAT' do
+ it { is_expected.to eq([rate]) }
+ end
+
+ context 'when the tax is a VAT' do
+ let(:included_in_price) { true }
+
+ it { is_expected.to eq([rate]) }
+ end
+ end
+
+ context 'when the order has a different tax zone' do
+ before do
+ allow(order).to receive_messages tax_zone: create(:zone, name: 'Other Zone')
+ end
+
+ context 'when the tax is a VAT' do
+ let(:included_in_price) { true }
+
+ # The rate should NOT match in this instance because:
+ # The order has a different tax zone, and the price is
+ # henceforth a net price and will not change.
+ it 'return no tax rate' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when the tax is not VAT' do
+ it 'returns no tax rate' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '.adjust' do
+ let(:order) { stub_model(Spree::Order) }
+ let(:tax_category_1) { stub_model(Spree::TaxCategory) }
+ let(:tax_category_2) { stub_model(Spree::TaxCategory) }
+ let(:rate_1) { stub_model(Spree::TaxRate, tax_category: tax_category_1) }
+ let(:rate_2) { stub_model(Spree::TaxRate, tax_category: tax_category_2) }
+
+ context 'with line items' do
+ let(:line_item) do
+ stub_model(Spree::LineItem,
+ price: 10.0,
+ quantity: 1,
+ tax_category: tax_category_1,
+ variant: stub_model(Spree::Variant))
+ end
+
+ let(:line_items) { [line_item] }
+
+ before do
+ allow(Spree::TaxRate).to receive_messages match: [rate_1, rate_2]
+ end
+
+ it 'applies adjustments for two tax rates to the order' do
+ expect(rate_1).to receive(:adjust)
+ expect(rate_2).not_to receive(:adjust)
+ Spree::TaxRate.adjust(order, line_items)
+ end
+ end
+
+ context 'without tax rates' do
+ let(:line_item) do
+ stub_model(Spree::LineItem,
+ price: 10.0,
+ quantity: 2,
+ tax_category: nil,
+ variant: stub_model(Spree::Variant))
+ end
+
+ let(:line_items) { [line_item] }
+
+ it 'updates pre_tax_total to match line item cost if no taxes' do
+ line_item.tax_category = nil
+ Spree::TaxRate.adjust(order, line_items)
+ expect(line_item.pre_tax_amount).to eq line_item.price * line_item.quantity
+ end
+ end
+
+ context 'with shipments' do
+ let(:shipments) { [stub_model(Spree::Shipment, cost: 10.0, tax_category: tax_category_1)] }
+
+ before do
+ allow(Spree::TaxRate).to receive_messages match: [rate_1, rate_2]
+ end
+
+ it 'applies adjustments for two tax rates to the order' do
+ expect(rate_1).to receive(:adjust)
+ expect(rate_2).not_to receive(:adjust)
+ Spree::TaxRate.adjust(order, shipments)
+ end
+ end
+
+ context 'for MOSS taxation in Europe' do
+ let(:germany) { create :country, name: 'Germany' }
+ let(:india) { create :country, name: 'India' }
+ let(:france) { create :country, name: 'France' }
+ let(:france_zone) { create :zone_with_country, name: 'France Zone' }
+ let(:germany_zone) { create :zone_with_country, name: 'Germany Zone', default_tax: true }
+ let(:india_zone) { create :zone_with_country, name: 'India' }
+ let(:moss_category) { Spree::TaxCategory.create(name: 'Digital Goods') }
+ let(:normal_category) { Spree::TaxCategory.create(name: 'Analogue Goods') }
+ let(:eu_zone) { create(:zone, name: 'EU') }
+
+ let!(:german_vat) do
+ Spree::TaxRate.create(
+ name: 'German VAT',
+ amount: 0.19,
+ calculator: Spree::Calculator::DefaultTax.create,
+ tax_category: moss_category,
+ zone: germany_zone,
+ included_in_price: true
+ )
+ end
+ let!(:french_vat) do
+ Spree::TaxRate.create(
+ name: 'French VAT',
+ amount: 0.25,
+ calculator: Spree::Calculator::DefaultTax.create,
+ tax_category: moss_category,
+ zone: france_zone,
+ included_in_price: true
+ )
+ end
+ let!(:eu_vat) do
+ Spree::TaxRate.create(
+ name: 'EU_VAT',
+ amount: 0.19,
+ calculator: Spree::Calculator::DefaultTax.create,
+ tax_category: normal_category,
+ zone: eu_zone,
+ included_in_price: true
+ )
+ end
+
+ let(:download) { create(:product, tax_category: moss_category, price: 100) }
+ let(:tshirt) { create(:product, tax_category: normal_category, price: 100) }
+ let(:order) { Spree::Order.create }
+
+ before do
+ germany_zone.zone_members.create(zoneable: germany)
+ france_zone.zone_members.create(zoneable: france)
+ india_zone.zone_members.create(zoneable: india)
+ eu_zone.zone_members.create(zoneable: germany)
+ eu_zone.zone_members.create(zoneable: france)
+ end
+
+ context 'a download' do
+ before do
+ Spree::Cart::AddItem.call(order: order, variant: download.master)
+ end
+
+ it 'without an adress costs 100 euros including tax' do
+ Spree::TaxRate.adjust(order, order.line_items)
+ order.update_with_updater!
+ expect(order.display_total).to eq(Spree::Money.new(100))
+ expect(order.included_tax_total).to eq(15.97)
+ end
+
+ it 'to germany costs 100 euros including tax' do
+ allow(order).to receive(:tax_zone).and_return(germany_zone)
+ Spree::TaxRate.adjust(order, order.line_items)
+ order.update_with_updater!
+ expect(order.display_total).to eq(Spree::Money.new(100))
+ expect(order.included_tax_total).to eq(15.97)
+ end
+
+ it 'to france costs more including tax' do
+ allow(order).to receive(:tax_zone).and_return(france_zone)
+ order.update_line_item_prices!
+ Spree::TaxRate.adjust(order, order.line_items)
+ order.update_with_updater!
+ expect(order.display_total).to eq(Spree::Money.new(105.04))
+ expect(order.included_tax_total).to eq(21.01)
+ expect(order.additional_tax_total).to eq(0)
+ end
+
+ it 'to somewhere else costs the net amount' do
+ allow(order).to receive(:tax_zone).and_return(india_zone)
+ order.update_line_item_prices!
+ Spree::TaxRate.adjust(order, order.line_items)
+ order.update_with_updater!
+ expect(order.included_tax_total).to eq(0)
+ expect(order.included_tax_total).to eq(0)
+ expect(order.display_total).to eq(Spree::Money.new(84.03))
+ end
+ end
+
+ context 'a t-shirt' do
+ it 'to germany costs 100 euros including tax' do
+ allow(order).to receive(:tax_zone).and_return(germany_zone)
+ Spree::Cart::AddItem.call(order: order, variant: tshirt.master)
+ Spree::TaxRate.adjust(order, order.line_items)
+ order.update_with_updater!
+ expect(order.display_total).to eq(Spree::Money.new(100))
+ expect(order.included_tax_total).to eq(15.97)
+ end
+
+ it 'to france costs 100 euros including tax' do
+ allow(order).to receive(:tax_zone).and_return(france_zone)
+ Spree::Cart::AddItem.call(order: order, variant: tshirt.master)
+ order.update_line_item_prices!
+ Spree::TaxRate.adjust(order, order.line_items)
+ order.update_with_updater!
+ expect(order.display_total).to eq(Spree::Money.new(100.00))
+ expect(order.included_tax_total).to eq(15.97)
+ expect(order.additional_tax_total).to eq(0)
+ end
+
+ it 'to somewhere else costs the net amount' do
+ allow(order).to receive(:tax_zone).and_return(india_zone)
+ Spree::Cart::AddItem.call(order: order, variant: tshirt.master)
+ order.update_line_item_prices!
+ Spree::TaxRate.adjust(order, order.line_items)
+ order.update_with_updater!
+ expect(order.included_tax_total).to eq(0)
+ expect(order.display_total).to eq(Spree::Money.new(84.03))
+ end
+ end
+ end
+ end
+
+ describe '.included_tax_amount_for' do
+ subject(:included_tax_amount) { Spree::TaxRate.included_tax_amount_for(price_options) }
+
+ let!(:order) { create :order_with_line_items }
+ let!(:included_tax_rate) do
+ create :tax_rate,
+ included_in_price: true,
+ tax_category: order.line_items.first.tax_category,
+ zone: order.tax_zone,
+ amount: 0.4
+ end
+
+ let!(:other_included_tax_rate) do
+ create :tax_rate,
+ included_in_price: true,
+ tax_category: order.line_items.first.tax_category,
+ zone: order.tax_zone,
+ amount: 0.05
+ end
+
+ let!(:additional_tax_rate) do
+ create :tax_rate,
+ included_in_price: false,
+ tax_category: order.line_items.first.tax_category,
+ zone: order.tax_zone,
+ amount: 0.2
+ end
+
+ let!(:included_tax_rate_from_somewhere_else) do
+ create :tax_rate,
+ included_in_price: true,
+ tax_category: order.line_items.first.tax_category,
+ zone: create(:zone_with_country),
+ amount: 0.1
+ end
+ let(:price_options) do
+ {
+ tax_zone: order.tax_zone,
+ tax_category: line_item.tax_category
+ }
+ end
+
+ let(:line_item) { order.line_items.first }
+
+ it 'will only get me tax amounts from tax_rates that match' do
+ expect(subject).to eq(included_tax_rate.amount + other_included_tax_rate.amount)
+ end
+ end
+
+ describe '#adjust' do
+ before do
+ @country = create(:country)
+ @zone = create(:zone, name: 'Country Zone', default_tax: true, zone_members: [])
+ @zone.zone_members.create(zoneable: @country)
+ @category = Spree::TaxCategory.create name: 'Taxable Foo'
+ @category2 = Spree::TaxCategory.create(name: 'Non Taxable')
+ @rate1 = Spree::TaxRate.create(
+ amount: 0.10,
+ calculator: Spree::Calculator::DefaultTax.create,
+ tax_category: @category,
+ zone: @zone
+ )
+ @rate2 = Spree::TaxRate.create(
+ amount: 0.05,
+ calculator: Spree::Calculator::DefaultTax.create,
+ tax_category: @category,
+ zone: @zone
+ )
+ @order = Spree::Order.create!
+ @taxable = create(:product, tax_category: @category)
+ @nontaxable = create(:product, tax_category: @category2)
+ end
+
+ context 'not taxable line item ' do
+ let!(:line_item) { Spree::Cart::AddItem.call(order: @order, variant: @nontaxable.master).value }
+
+ it 'does not create a tax adjustment' do
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ expect(line_item.adjustments.tax.charge.count).to eq(0)
+ end
+
+ it 'does not create a refund' do
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ expect(line_item.adjustments.credit.count).to eq(0)
+ end
+ end
+
+ context 'taxable line item' do
+ let!(:line_item) { Spree::Cart::AddItem.call(order: @order, variant: @taxable.master).value }
+
+ context 'when price includes tax' do
+ before do
+ @rate1.update_column(:included_in_price, true)
+ @rate2.update_column(:included_in_price, true)
+ Spree::TaxRate.store_pre_tax_amount(line_item, [@rate1, @rate2])
+ end
+
+ context 'when zone is contained by default tax zone' do
+ it 'creates two adjustments, one for each tax rate' do
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ expect(line_item.adjustments.count).to eq(2)
+ end
+
+ it 'does not create a tax refund' do
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ expect(line_item.adjustments.credit.count).to eq(0)
+ end
+ end
+
+ context "when order's zone is neither the default zone, or included in the default zone, but matches the rate's zone" do
+ before do
+ new_rate = Spree::TaxRate.create(
+ amount: 0.2,
+ included_in_price: true,
+ calculator: Spree::Calculator::DefaultTax.create,
+ tax_category: @category,
+ zone: create(:zone_with_country)
+ )
+ allow(@order).to receive(:tax_zone).and_return(new_rate.zone)
+ end
+
+ it 'creates an adjustment' do
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ expect(line_item.adjustments.charge.count).to eq(1)
+ end
+
+ it 'does not create a tax refund for each tax rate' do
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ expect(line_item.adjustments.credit.count).to eq(0)
+ end
+ end
+
+ context "when order's zone does not match default zone, is not included in the default zone, AND does not match the rate's zone" do
+ before do
+ @new_zone = create(:zone, name: 'New Zone', default_tax: false)
+ @new_country = create(:country, name: 'New Country')
+ @new_zone.zone_members.create(zoneable: @new_country)
+ @order.ship_address = create(:address, country: @new_country)
+ @order.save
+ @order.reload
+ end
+
+ it 'does not create positive adjustments' do
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ expect(line_item.adjustments.charge.count).to eq(0)
+ end
+
+ it 'does not create a tax refund for each tax rate' do
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ expect(line_item.adjustments.credit.count).to eq(0)
+ end
+ end
+
+ context 'when price does not include tax' do
+ before do
+ allow(@order).to receive_messages tax_zone: @zone
+ [@rate1, @rate2].each do |rate|
+ rate.included_in_price = false
+ rate.zone = @zone
+ rate.save
+ end
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ end
+
+ it 'deletes adjustments for open order when taxrate is deleted' do
+ @rate1.destroy!
+ @rate2.destroy!
+ expect(line_item.adjustments.count).to eq(0)
+ end
+
+ it 'does not delete adjustments for complete order when taxrate is deleted' do
+ @order.update_column :completed_at, Time.current
+ @rate1.destroy!
+ @rate2.destroy!
+ expect(line_item.adjustments.count).to eq(2)
+ end
+
+ it 'creates an adjustment' do
+ expect(line_item.adjustments.count).to eq(2)
+ end
+
+ it 'does not create a tax refund' do
+ expect(line_item.adjustments.credit.count).to eq(0)
+ end
+
+ describe 'tax adjustments' do
+ before { Spree::TaxRate.adjust(@order, @order.line_items) }
+
+ it 'applies adjustments when a tax zone is present' do
+ expect(line_item.adjustments.count).to eq(2)
+ line_item.adjustments.each do |adjustment|
+ expect(adjustment.label).to eq("#{adjustment.source.tax_category.name} #{adjustment.source.amount * 100}%")
+ end
+ end
+
+ describe 'when the tax zone is removed' do
+ before { allow(@order).to receive_messages tax_zone: nil }
+
+ it 'does not apply any adjustments' do
+ Spree::TaxRate.adjust(@order, @order.line_items)
+ expect(line_item.adjustments.count).to eq(0)
+ end
+ end
+ end
+ end
+
+ context 'when two rates apply' do
+ before do
+ @price_before_taxes = line_item.price / (1 + @rate1.amount + @rate2.amount)
+ # Use the same rounding method as in DefaultTax calculator
+ @price_before_taxes = BigDecimal(@price_before_taxes).round(2, BigDecimal::ROUND_HALF_UP)
+ line_item.update_column(:pre_tax_amount, @price_before_taxes)
+ # Clear out any previously automatically-applied adjustments
+ @order.all_adjustments.delete_all
+ @rate1.adjust(@order, line_item)
+ @rate2.adjust(@order, line_item)
+ end
+
+ it 'creates two price adjustments' do
+ expect(@order.line_item_adjustments.count).to eq(2)
+ end
+
+ it 'price adjustments should be accurate' do
+ included_tax = @order.line_item_adjustments.sum(:amount)
+ expect(@price_before_taxes + included_tax).to eq(line_item.total)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/taxon_image_spec.rb b/core/spec/models/spree/taxon_image_spec.rb
new file mode 100644
index 00000000000..a5aa58f8fed
--- /dev/null
+++ b/core/spec/models/spree/taxon_image_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Spree::TaxonImage, type: :model do
+ context 'validation' do
+ let(:spree_image) { Spree::TaxonImage.new }
+ let(:image_file) { File.open(Spree::Core::Engine.root + 'spec/fixtures' + 'thinking-cat.jpg') }
+ let(:text_file) { File.open(Spree::Core::Engine.root + 'spec/fixtures' + 'text-file.txt') }
+
+ it 'has allowed attachment content type' do
+ if Rails.application.config.use_paperclip
+ spree_image.attachment = image_file
+ else
+ spree_image.attachment.attach(io: image_file, filename: 'thinking-cat.jpg', content_type: 'image/jpeg')
+ end
+ expect(spree_image).to be_valid
+ end
+
+ it 'has no allowed attachment content type' do
+ if Rails.application.config.use_paperclip
+ spree_image.attachment = text_file
+ else
+ spree_image.attachment.attach(io: text_file, filename: 'text-file.txt', content_type: 'text/plain')
+ end
+ expect(spree_image).not_to be_valid
+ end
+ end
+end
diff --git a/core/spec/models/spree/taxon_spec.rb b/core/spec/models/spree/taxon_spec.rb
new file mode 100644
index 00000000000..f01bc017a39
--- /dev/null
+++ b/core/spec/models/spree/taxon_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe Spree::Taxon, type: :model do
+ let(:taxon) { FactoryBot.build(:taxon, name: 'Ruby on Rails', parent_id: nil) }
+ let(:valid_taxon) { FactoryBot.build(:taxon, name: 'Vaild Rails', parent_id: 1, taxonomy_id: 2) }
+
+ describe '#to_param' do
+ subject { super().to_param }
+
+ it { is_expected.to eql taxon.permalink }
+ end
+
+ context 'check_for_root' do
+ it 'does not validate the taxon' do
+ expect(taxon.valid?).to eq false
+ end
+
+ it 'validates the taxon' do
+ expect(valid_taxon.valid?).to eq true
+ end
+ end
+
+ context 'set_permalink' do
+ it 'sets permalink correctly when no parent present' do
+ taxon.set_permalink
+ expect(taxon.permalink).to eql 'ruby-on-rails'
+ end
+
+ it 'supports Chinese characters' do
+ taxon.name = 'ä½ å¥½'
+ taxon.set_permalink
+ expect(taxon.permalink).to eql 'ni-hao'
+ end
+
+ it 'stores old slugs in FriendlyIds history' do
+ # Stub out the unrelated methods that cannot handle a save without an id
+ allow(subject).to receive(:set_depth!)
+ expect(subject).to receive(:create_slug)
+ subject.permalink = 'custom-slug'
+ subject.run_callbacks :save
+ end
+
+ context 'with parent taxon' do
+ let(:parent) { FactoryBot.build(:taxon, permalink: 'brands') }
+
+ before { allow(taxon).to receive_messages parent: parent }
+
+ it 'sets permalink correctly when taxon has parent' do
+ taxon.set_permalink
+ expect(taxon.permalink).to eql 'brands/ruby-on-rails'
+ end
+
+ it 'sets permalink correctly with existing permalink present' do
+ taxon.permalink = 'b/rubyonrails'
+ taxon.set_permalink
+ expect(taxon.permalink).to eql 'brands/rubyonrails'
+ end
+
+ it 'supports Chinese characters' do
+ taxon.name = '我'
+ taxon.set_permalink
+ expect(taxon.permalink).to eql 'brands/wo'
+ end
+
+ # Regression test for #3390
+ context 'setting a new node sibling position via :child_index=' do
+ let(:idx) { rand(0..100) }
+
+ before { allow(parent).to receive(:move_to_child_with_index) }
+
+ context 'taxon is not new' do
+ before { allow(taxon).to receive(:new_record?).and_return(false) }
+
+ it 'passes the desired index move_to_child_with_index of :parent ' do
+ expect(taxon).to receive(:move_to_child_with_index).with(parent, idx)
+
+ taxon.child_index = idx
+ end
+ end
+ end
+ end
+ end
+
+ # Regression test for #2620
+ context 'creating a child node using first_or_create' do
+ let!(:taxonomy) { create(:taxonomy) }
+
+ it 'does not error out' do
+ expect { taxonomy.root.children.unscoped.where(name: 'Some name', parent_id: taxonomy.taxons.first.id).first_or_create }.not_to raise_error
+ end
+ end
+
+ context 'ransackable_associations' do
+ it { expect(described_class.whitelisted_ransackable_associations).to include('taxonomy') }
+ end
+end
diff --git a/core/spec/models/spree/taxonomy_spec.rb b/core/spec/models/spree/taxonomy_spec.rb
new file mode 100644
index 00000000000..3290da3b798
--- /dev/null
+++ b/core/spec/models/spree/taxonomy_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Spree::Taxonomy, type: :model do
+ context '#destroy' do
+ before do
+ @taxonomy = create(:taxonomy)
+ @root_taxon = @taxonomy.root
+ @child_taxon = create(:taxon, taxonomy_id: @taxonomy.id, parent: @root_taxon)
+ end
+
+ it 'destroys all associated taxons' do
+ @taxonomy.destroy
+ expect { Spree::Taxon.find(@root_taxon.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { Spree::Taxon.find(@child_taxon.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/core/spec/models/spree/user_spec.rb b/core/spec/models/spree/user_spec.rb
new file mode 100644
index 00000000000..634cbc6972d
--- /dev/null
+++ b/core/spec/models/spree/user_spec.rb
@@ -0,0 +1,210 @@
+require 'spec_helper'
+
+describe Spree::LegacyUser, type: :model do # rubocop:disable RSpec/MultipleDescribes
+ # Regression test for #2844 + #3346
+ context '#last_incomplete_order' do
+ let!(:user) { create(:user) }
+ let!(:order) { create(:order, bill_address: create(:address), ship_address: create(:address)) }
+ let(:current_store) { create :store }
+
+ let(:order_1) { create(:order, created_at: 1.day.ago, user: user, created_by: user, store: current_store) }
+ let(:order_2) { create(:order, user: user, created_by: user, store: current_store) }
+ let(:order_3) { create(:order, user: user, created_by: create(:user), store: current_store) }
+
+ it 'returns correct order' do
+ Timecop.scale(3600) do
+ order_1
+ order_2
+ order_3
+
+ expect(user.last_incomplete_spree_order(current_store)).to eq order_3
+ end
+ end
+
+ context 'persists order address' do
+ it 'copies over order addresses' do
+ expect do
+ user.persist_order_address(order)
+ end.to change { Spree::Address.count }.by(2)
+
+ expect(user.bill_address).to eq order.bill_address
+ expect(user.ship_address).to eq order.ship_address
+ end
+
+ it 'doesnt create new addresses if user has already' do
+ user.update_column(:bill_address_id, create(:address).id)
+ user.update_column(:ship_address_id, create(:address).id)
+ user.reload
+
+ expect do
+ user.persist_order_address(order)
+ end.not_to change { Spree::Address.count }
+ end
+
+ it 'set both bill and ship address id on subject' do
+ user.persist_order_address(order)
+
+ expect(user.bill_address_id).not_to be_blank
+ expect(user.ship_address_id).not_to be_blank
+ end
+ end
+
+ context 'payment source' do
+ let(:payment_method) { create(:credit_card_payment_method) }
+ let!(:cc) do
+ create(:credit_card, user_id: user.id, payment_method: payment_method, gateway_customer_profile_id: '2342343')
+ end
+
+ it 'has payment sources' do
+ expect(user.payment_sources.first.gateway_customer_profile_id).not_to be_empty
+ end
+
+ it 'drops payment source' do
+ user.drop_payment_source cc
+ expect(cc.gateway_customer_profile_id).to be_nil
+ end
+ end
+ end
+end
+
+describe Spree.user_class, type: :model do
+ context 'reporting' do
+ let(:order_value) { BigDecimal('80.94') }
+ let(:order_count) { 4 }
+ let(:orders) { Array.new(order_count, double(total: order_value)) }
+
+ before do
+ allow(orders).to receive(:sum).with(:total).and_return(orders.sum(&:total))
+ allow(orders).to receive(:count).and_return(orders.length)
+ end
+
+ def load_orders
+ allow(subject).to receive(:orders).and_return(double(complete: orders))
+ end
+
+ describe '#lifetime_value' do
+ context 'with orders' do
+ before { load_orders }
+
+ it 'returns the total of completed orders for the user' do
+ expect(subject.lifetime_value).to eq (order_count * order_value)
+ end
+ end
+
+ context 'without orders' do
+ it 'returns 0.00' do
+ expect(subject.lifetime_value).to eq BigDecimal('0.00')
+ end
+ end
+ end
+
+ describe '#display_lifetime_value' do
+ it 'returns a Spree::Money version of lifetime_value' do
+ value = BigDecimal('500.05')
+ allow(subject).to receive(:lifetime_value).and_return(value)
+ expect(subject.display_lifetime_value).to eq Spree::Money.new(value)
+ end
+ end
+
+ describe '#order_count' do
+ before { load_orders }
+
+ it 'returns the count of completed orders for the user' do
+ expect(subject.order_count).to eq order_count
+ end
+ end
+
+ describe '#average_order_value' do
+ context 'with orders' do
+ before { load_orders }
+
+ it 'returns the average completed order price for the user' do
+ expect(subject.average_order_value).to eq order_value
+ end
+ end
+
+ context 'without orders' do
+ it 'returns 0.00' do
+ expect(subject.average_order_value).to eq BigDecimal('0.00')
+ end
+ end
+ end
+
+ describe '#display_average_order_value' do
+ before { load_orders }
+
+ it 'returns a Spree::Money version of average_order_value' do
+ value = BigDecimal('500.05')
+ allow(subject).to receive(:average_order_value).and_return(value)
+ expect(subject.display_average_order_value).to eq Spree::Money.new(value)
+ end
+ end
+ end
+
+ describe '#total_available_store_credit' do
+ context 'user does not have any associated store credits' do
+ subject { create(:user) }
+
+ it 'returns 0' do
+ expect(subject.total_available_store_credit).to be_zero
+ end
+ end
+
+ context 'user has several associated store credits' do
+ subject { store_credit.user }
+
+ let(:user) { create(:user) }
+ let(:amount) { 120.25 }
+ let(:additional_amount) { 55.75 }
+ let(:store_credit) { create(:store_credit, user: user, amount: amount, amount_used: 0.0) }
+ let!(:additional_store_credit) { create(:store_credit, user: user, amount: additional_amount, amount_used: 0.0) }
+
+ context 'part of the store credit has been used' do
+ let(:amount_used) { 35.00 }
+
+ before { store_credit.update_attributes(amount_used: amount_used) }
+
+ context 'part of the store credit has been authorized' do
+ let(:authorized_amount) { 10 }
+
+ before { additional_store_credit.update_attributes(amount_authorized: authorized_amount) }
+
+ it 'returns sum of amounts minus used amount and authorized amount' do
+ available_store_credit = amount + additional_amount - amount_used - authorized_amount
+ expect(subject.total_available_store_credit.to_f).to eq available_store_credit
+ end
+ end
+
+ context 'there are no authorized amounts on any of the store credits' do
+ it 'returns sum of amounts minus used amount' do
+ expect(subject.total_available_store_credit.to_f).to eq (amount + additional_amount - amount_used)
+ end
+ end
+ end
+
+ context 'store credits have never been used' do
+ context 'part of the store credit has been authorized' do
+ let(:authorized_amount) { 10 }
+
+ before { additional_store_credit.update_attributes(amount_authorized: authorized_amount) }
+
+ it 'returns sum of amounts minus authorized amount' do
+ expect(subject.total_available_store_credit.to_f).to eq (amount + additional_amount - authorized_amount)
+ end
+ end
+
+ context 'there are no authorized amounts on any of the store credits' do
+ it 'returns sum of amounts' do
+ expect(subject.total_available_store_credit.to_f).to eq (amount + additional_amount)
+ end
+ end
+ end
+
+ context 'all store credits have never been used or authorized' do
+ it 'returns sum of amounts' do
+ expect(subject.total_available_store_credit.to_f).to eq (amount + additional_amount)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/variant_spec.rb b/core/spec/models/spree/variant_spec.rb
new file mode 100644
index 00000000000..1998144b518
--- /dev/null
+++ b/core/spec/models/spree/variant_spec.rb
@@ -0,0 +1,920 @@
+require 'spec_helper'
+
+describe Spree::Variant, type: :model do
+ let!(:variant) { create(:variant) }
+ let(:master_variant) { create(:master_variant) }
+
+ it_behaves_like 'default_price'
+
+ context 'sorting' do
+ it 'responds to set_list_position' do
+ expect(variant.respond_to?(:set_list_position)).to eq(true)
+ end
+ end
+
+ context 'validations' do
+ it 'validates price is greater than 0' do
+ variant.price = -1
+ expect(variant).to be_invalid
+ end
+
+ it 'validates price is 0' do
+ variant.price = 0
+ expect(variant).to be_valid
+ end
+ end
+
+ context 'after create' do
+ let!(:product) { create(:product) }
+
+ it 'propagate to stock items' do
+ expect_any_instance_of(Spree::StockLocation).to receive(:propagate_variant)
+ create(:variant, product: product)
+ end
+
+ context 'stock location has disable propagate all variants' do
+ before { Spree::StockLocation.update_all propagate_all_variants: false }
+
+ it 'propagate to stock items' do
+ expect_any_instance_of(Spree::StockLocation).not_to receive(:propagate_variant)
+ product.variants.create
+ end
+ end
+
+ describe 'mark_master_out_of_stock' do
+ before do
+ product.master.stock_items.first.set_count_on_hand(5)
+ end
+
+ context 'when product is created without variants but with stock' do
+ it { expect(product.master).to be_in_stock }
+ end
+
+ context 'when a variant is created' do
+ let!(:new_variant) { create(:variant, product: product) }
+
+ it { expect(product.master).not_to be_in_stock }
+ end
+ end
+ end
+
+ describe 'scope' do
+ describe '.eligible' do
+ context 'when only master variants' do
+ let!(:product_1) { create(:product) }
+ let!(:product_2) { create(:product) }
+
+ it 'returns all of them' do
+ expect(Spree::Variant.eligible).to include(product_1.master)
+ expect(Spree::Variant.eligible).to include(product_2.master)
+ end
+ end
+
+ context 'when product has more than 1 variant' do
+ let!(:product) { create(:product) }
+ let!(:variant) { create(:variant, product: product) }
+
+ it 'filters master variant out' do
+ expect(Spree::Variant.eligible).to include(variant)
+ expect(Spree::Variant.eligible).not_to include(product.master)
+ end
+ end
+ end
+
+ describe '.not_discontinued' do
+ context 'when discontinued' do
+ let!(:discontinued_variant) { create(:variant, discontinue_on: Time.current - 1.day) }
+
+ it { expect(Spree::Variant.not_discontinued).not_to include(discontinued_variant) }
+ end
+
+ context 'when not discontinued' do
+ let!(:variant_2) { create(:variant, discontinue_on: Time.current + 1.day) }
+
+ it { expect(Spree::Variant.not_discontinued).to include(variant_2) }
+ end
+
+ context 'when discontinue_on not present' do
+ let!(:variant_2) { create(:variant, discontinue_on: nil) }
+
+ it { expect(Spree::Variant.not_discontinued).to include(variant_2) }
+ end
+ end
+
+ describe '.not_deleted' do
+ context 'when deleted' do
+ let!(:deleted_variant) { create(:variant, deleted_at: Time.current) }
+
+ it { expect(Spree::Variant.not_deleted).not_to include(deleted_variant) }
+ end
+
+ context 'when not deleted' do
+ let!(:variant_2) { create(:variant, deleted_at: nil) }
+
+ it { expect(Spree::Variant.not_deleted).to include(variant_2) }
+ end
+ end
+
+ describe '.for_currency_and_available_price_amount' do
+ let(:currency) { 'EUR' }
+
+ context 'when price with currency present' do
+ context 'when price has amount' do
+ let!(:price_1) { create(:price, currency: currency, variant: variant, amount: 10) }
+
+ it { expect(Spree::Variant.for_currency_and_available_price_amount(currency)).to include(variant) }
+ end
+
+ context 'when price do not have amount' do
+ let!(:price_1) { create(:price, currency: currency, variant: variant, amount: nil) }
+
+ it { expect(Spree::Variant.for_currency_and_available_price_amount(currency)).not_to include(variant) }
+ end
+ end
+
+ context 'when price with currency not present' do
+ let!(:unavailable_currency) { 'INR' }
+
+ context 'when price has amount' do
+ let!(:price_1) { create(:price, currency: unavailable_currency, variant: variant, amount: 10) }
+
+ it { expect(Spree::Variant.for_currency_and_available_price_amount(currency)).not_to include(variant) }
+ end
+
+ context 'when price do not have amount' do
+ let!(:price_1) { create(:price, currency: unavailable_currency, variant: variant, amount: nil) }
+
+ it { expect(Spree::Variant.for_currency_and_available_price_amount(currency)).not_to include(variant) }
+ end
+ end
+
+ context 'when multiple prices for same currency present' do
+ let!(:price_1) { create(:price, currency: currency, variant: variant) }
+ let!(:price_2) { create(:price, currency: currency, variant: variant) }
+
+ it 'does not duplicate variant' do
+ expect(Spree::Variant.for_currency_and_available_price_amount(currency)).to eq([variant])
+ end
+ end
+
+ context 'when currency parameter is nil' do
+ let!(:price_1) { create(:price, currency: currency, variant: variant, amount: 10) }
+
+ before { Spree::Config[:currency] = currency }
+
+ it { expect(Spree::Variant.for_currency_and_available_price_amount).to include(variant) }
+ end
+ end
+
+ describe '.active' do
+ let!(:variants) { [variant] }
+ let!(:currency) { 'EUR' }
+
+ before do
+ allow(Spree::Variant).to receive(:not_discontinued).and_return(variants)
+ allow(variants).to receive(:not_deleted).and_return(variants)
+ allow(variants).to receive(:for_currency_and_available_price_amount).with(currency).and_return(variants)
+ end
+
+ it 'finds not_discontinued variants' do
+ expect(Spree::Variant).to receive(:not_discontinued).and_return(variants)
+ Spree::Variant.active(currency)
+ end
+
+ it 'finds not_deleted variants' do
+ expect(variants).to receive(:not_deleted).and_return(variants)
+ Spree::Variant.active(currency)
+ end
+
+ it 'finds variants for_currency_and_available_price_amount' do
+ expect(variants).to receive(:for_currency_and_available_price_amount).with(currency).and_return(variants)
+ Spree::Variant.active(currency)
+ end
+
+ it { expect(Spree::Variant.active(currency)).to eq(variants) }
+ end
+ end
+
+ context 'product has other variants' do
+ describe 'option value accessors' do
+ before do
+ @multi_variant = FactoryBot.create :variant, product: variant.product
+ variant.product.reload
+ end
+
+ let(:multi_variant) { @multi_variant }
+
+ it 'sets option value' do
+ expect(multi_variant.option_value('media_type')).to be_nil
+
+ multi_variant.set_option_value('media_type', 'DVD')
+ expect(multi_variant.option_value('media_type')).to eql 'DVD'
+
+ multi_variant.set_option_value('media_type', 'CD')
+ expect(multi_variant.option_value('media_type')).to eql 'CD'
+ end
+
+ it 'does not duplicate associated option values when set multiple times' do
+ multi_variant.set_option_value('media_type', 'CD')
+
+ expect do
+ multi_variant.set_option_value('media_type', 'DVD')
+ end.not_to change(multi_variant.option_values, :count)
+
+ expect do
+ multi_variant.set_option_value('coolness_type', 'awesome')
+ end.to change(multi_variant.option_values, :count).by(1)
+ end
+ end
+
+ context 'product has other variants' do
+ describe 'option value accessors' do
+ before do
+ @multi_variant = create(:variant, product: variant.product)
+ variant.product.reload
+ end
+
+ let(:multi_variant) { @multi_variant }
+
+ it 'sets option value' do
+ expect(multi_variant.option_value('media_type')).to be_nil
+
+ multi_variant.set_option_value('media_type', 'DVD')
+ expect(multi_variant.option_value('media_type')).to eql 'DVD'
+
+ multi_variant.set_option_value('media_type', 'CD')
+ expect(multi_variant.option_value('media_type')).to eql 'CD'
+ end
+
+ it 'does not duplicate associated option values when set multiple times' do
+ multi_variant.set_option_value('media_type', 'CD')
+
+ expect do
+ multi_variant.set_option_value('media_type', 'DVD')
+ end.not_to change(multi_variant.option_values, :count)
+
+ expect do
+ multi_variant.set_option_value('coolness_type', 'awesome')
+ end.to change(multi_variant.option_values, :count).by(1)
+ end
+ end
+ end
+ end
+
+ context '#cost_price=' do
+ it 'uses LocalizedNumber.parse' do
+ expect(Spree::LocalizedNumber).to receive(:parse).with('1,599.99')
+ subject.cost_price = '1,599.99'
+ end
+ end
+
+ context '#price=' do
+ it 'uses LocalizedNumber.parse' do
+ expect(Spree::LocalizedNumber).to receive(:parse).with('1,599.99')
+ subject.price = '1,599.99'
+ end
+ end
+
+ context '#weight=' do
+ it 'uses LocalizedNumber.parse' do
+ expect(Spree::LocalizedNumber).to receive(:parse).with('1,599.99')
+ subject.weight = '1,599.99'
+ end
+ end
+
+ context '#currency' do
+ it 'returns the globally configured currency' do
+ expect(variant.currency).to eql 'USD'
+ end
+ end
+
+ context '#display_amount' do
+ it 'returns a Spree::Money' do
+ variant.price = 21.22
+ expect(variant.display_amount.to_s).to eql '$21.22'
+ end
+ end
+
+ context '#cost_currency' do
+ context 'when cost currency is nil' do
+ before { variant.cost_currency = nil }
+
+ it 'populates cost currency with the default value on save' do
+ variant.save!
+ expect(variant.cost_currency).to eql 'USD'
+ end
+ end
+ end
+
+ describe '.price_in' do
+ subject { variant.price_in(currency).display_amount }
+
+ before do
+ variant.prices << create(:price, variant: variant, currency: 'EUR', amount: 33.33)
+ end
+
+ context 'when currency is not specified' do
+ let(:currency) { nil }
+
+ it 'returns 0' do
+ expect(subject.to_s).to eql '$0.00'
+ end
+ end
+
+ context 'when currency is EUR' do
+ let(:currency) { 'EUR' }
+
+ it 'returns the value in the EUR' do
+ expect(subject.to_s).to eql '€33.33'
+ end
+ end
+
+ context 'when currency is USD' do
+ let(:currency) { 'USD' }
+
+ it 'returns the value in the USD' do
+ expect(subject.to_s).to eql '$19.99'
+ end
+ end
+ end
+
+ describe '.amount_in' do
+ subject { variant.amount_in(currency) }
+
+ before do
+ variant.prices << create(:price, variant: variant, currency: 'EUR', amount: 33.33)
+ end
+
+ context 'when currency is not specified' do
+ let(:currency) { nil }
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when currency is EUR' do
+ let(:currency) { 'EUR' }
+
+ it 'returns the value in the EUR' do
+ expect(subject).to eq(33.33)
+ end
+ end
+
+ context 'when currency is USD' do
+ let(:currency) { 'USD' }
+
+ it 'returns the value in the USD' do
+ expect(subject).to eq(19.99)
+ end
+ end
+ end
+
+ # Regression test for #2432
+ describe 'options_text' do
+ let!(:variant) { build(:variant, option_values: []) }
+ let!(:master) { create(:master_variant) }
+
+ before do
+ # Order bar than foo
+ variant.option_values << create(:option_value, name: 'Foo', presentation: 'Foo', option_type: create(:option_type, position: 2, name: 'Foo Type', presentation: 'Foo Type'))
+ variant.option_values << create(:option_value, name: 'Bar', presentation: 'Bar', option_type: create(:option_type, position: 1, name: 'Bar Type', presentation: 'Bar Type'))
+ variant.save
+ end
+
+ it 'orders by bar than foo' do
+ expect(variant.options_text).to eql 'Bar Type: Bar, Foo Type: Foo'
+ end
+ end
+
+ describe 'exchange_name' do
+ let!(:variant) { build(:variant, option_values: []) }
+ let!(:master) { create(:master_variant) }
+
+ before do
+ variant.option_values << create(:option_value, name: 'Foo',
+ presentation: 'Foo',
+ option_type: create(:option_type, position: 2, name: 'Foo Type', presentation: 'Foo Type'))
+ variant.save
+ end
+
+ context 'master variant' do
+ it 'returns name' do
+ expect(master.exchange_name).to eql master.name
+ end
+ end
+
+ context 'variant' do
+ it 'returns options text' do
+ expect(variant.exchange_name).to eql 'Foo Type: Foo'
+ end
+ end
+ end
+
+ describe 'exchange_name' do
+ let!(:variant) { build(:variant, option_values: []) }
+ let!(:master) { create(:master_variant) }
+
+ before do
+ variant.option_values << create(:option_value, name: 'Foo',
+ presentation: 'Foo',
+ option_type: create(:option_type, position: 2, name: 'Foo Type', presentation: 'Foo Type'))
+ variant.save
+ end
+
+ context 'master variant' do
+ it 'returns name' do
+ expect(master.exchange_name).to eql master.name
+ end
+ end
+
+ context 'variant' do
+ it 'returns options text' do
+ expect(variant.exchange_name).to eql 'Foo Type: Foo'
+ end
+ end
+ end
+
+ describe 'descriptive_name' do
+ let!(:variant) { build(:variant, option_values: []) }
+ let!(:master) { create(:master_variant) }
+
+ before do
+ variant.option_values << create(:option_value, name: 'Foo',
+ presentation: 'Foo',
+ option_type: create(:option_type, position: 2, name: 'Foo Type', presentation: 'Foo Type'))
+ variant.save
+ end
+
+ context 'master variant' do
+ it 'returns name with Master identifier' do
+ expect(master.descriptive_name).to eql master.name + ' - Master'
+ end
+ end
+
+ context 'variant' do
+ it 'returns options text with name' do
+ expect(variant.descriptive_name).to eql variant.name + ' - Foo Type: Foo'
+ end
+ end
+ end
+
+ # Regression test for #2744
+ describe 'set_position' do
+ it 'sets variant position after creation' do
+ variant = create(:variant)
+ expect(variant.position).not_to be_nil
+ end
+ end
+
+ describe '#in_stock?' do
+ before do
+ Spree::Config.track_inventory_levels = true
+ end
+
+ context 'when stock_items are not backorderable' do
+ before do
+ allow_any_instance_of(Spree::StockItem).to receive_messages(backorderable: false)
+ end
+
+ context 'when stock_items in stock' do
+ before do
+ variant.stock_items.first.update_column(:count_on_hand, 10)
+ end
+
+ it 'returns true if stock_items in stock' do
+ expect(variant.in_stock?).to be true
+ end
+ end
+
+ context 'when stock_items out of stock' do
+ before do
+ allow_any_instance_of(Spree::StockItem).to receive_messages(backorderable: false)
+ allow_any_instance_of(Spree::StockItem).to receive_messages(count_on_hand: 0)
+ end
+
+ it 'return false if stock_items out of stock' do
+ expect(variant.in_stock?).to be false
+ end
+ end
+ end
+
+ describe '#can_supply?' do
+ it 'calls out to quantifier' do
+ expect(Spree::Stock::Quantifier).to receive(:new).and_return(quantifier = double)
+ expect(quantifier).to receive(:can_supply?).with(10)
+ variant.can_supply?(10)
+ end
+ end
+
+ context 'when stock_items are backorderable' do
+ before do
+ allow_any_instance_of(Spree::StockItem).to receive_messages(backorderable: true)
+ end
+
+ context 'when stock_items out of stock' do
+ before do
+ allow_any_instance_of(Spree::StockItem).to receive_messages(count_on_hand: 0)
+ end
+
+ it 'in_stock? returns false' do
+ expect(variant.in_stock?).to be false
+ end
+
+ it 'can_supply? return true' do
+ expect(variant.can_supply?).to be true
+ end
+ end
+ end
+ end
+
+ describe '#is_backorderable' do
+ subject { variant.is_backorderable? }
+
+ let(:variant) { build(:variant) }
+
+ it 'invokes Spree::Stock::Quantifier' do
+ expect_any_instance_of(Spree::Stock::Quantifier).to receive(:backorderable?).and_return(true)
+ subject
+ end
+ end
+
+ describe '#purchasable?' do
+ context 'when stock_items are not backorderable' do
+ before do
+ allow_any_instance_of(Spree::StockItem).to receive_messages(backorderable: false)
+ end
+
+ context 'when stock_items in stock' do
+ before do
+ variant.stock_items.first.update_column(:count_on_hand, 10)
+ end
+
+ it 'returns true if stock_items in stock' do
+ expect(variant.purchasable?).to be true
+ end
+ end
+
+ context 'when stock_items out of stock' do
+ before do
+ allow_any_instance_of(Spree::StockItem).to receive_messages(count_on_hand: 0)
+ end
+
+ it 'return false if stock_items out of stock' do
+ expect(variant.purchasable?).to be false
+ end
+ end
+ end
+
+ context 'when stock_items are out of stock' do
+ before do
+ allow_any_instance_of(Spree::StockItem).to receive_messages(count_on_hand: 0)
+ end
+
+ context 'when stock item are backorderable' do
+ before do
+ allow_any_instance_of(Spree::StockItem).to receive_messages(backorderable: true)
+ end
+
+ it 'returns true if stock_items are backorderable' do
+ expect(variant.purchasable?).to be true
+ end
+ end
+
+ context 'when stock_items are not backorderable' do
+ before do
+ allow_any_instance_of(Spree::StockItem).to receive_messages(backorderable: false)
+ end
+
+ it 'return false if stock_items are not backorderable' do
+ expect(variant.purchasable?).to be false
+ end
+ end
+ end
+ end
+
+ describe '#total_on_hand' do
+ it 'is infinite if track_inventory_levels is false' do
+ Spree::Config[:track_inventory_levels] = false
+ expect(build(:variant).total_on_hand).to eql(Float::INFINITY)
+ end
+
+ it 'matches quantifier total_on_hand' do
+ variant = build(:variant)
+ expect(variant.total_on_hand).to eq(Spree::Stock::Quantifier.new(variant).total_on_hand)
+ end
+ end
+
+ describe '#tax_category' do
+ context 'when tax_category is nil' do
+ let(:product) { build(:product) }
+ let(:variant) { build(:variant, product: product, tax_category_id: nil) }
+
+ it 'returns the parent products tax_category' do
+ expect(variant.tax_category).to eq(product.tax_category)
+ end
+ end
+
+ context 'when tax_category is set' do
+ let(:tax_category) { create(:tax_category) }
+ let(:variant) { build(:variant, tax_category: tax_category) }
+
+ it 'returns the tax_category set on itself' do
+ expect(variant.tax_category).to eq(tax_category)
+ end
+ end
+ end
+
+ describe 'touching' do
+ it 'updates a product' do
+ variant.product.update_column(:updated_at, 1.day.ago)
+ variant.touch
+ expect(variant.product.reload.updated_at).to be_within(3.seconds).of(Time.current)
+ end
+
+ it 'clears the in_stock cache key' do
+ expect(Rails.cache).to receive(:delete).with(variant.send(:in_stock_cache_key))
+ variant.touch
+ end
+ end
+
+ describe '#should_track_inventory?' do
+ it 'does not track inventory when global setting is off' do
+ Spree::Config[:track_inventory_levels] = false
+
+ expect(build(:variant).should_track_inventory?).to eq(false)
+ end
+
+ it 'does not track inventory when variant is turned off' do
+ Spree::Config[:track_inventory_levels] = true
+
+ expect(build(:on_demand_variant).should_track_inventory?).to eq(false)
+ end
+
+ it 'tracks inventory when global and variant are on' do
+ Spree::Config[:track_inventory_levels] = true
+
+ expect(build(:variant).should_track_inventory?).to eq(true)
+ end
+ end
+
+ describe 'deleted_at scope' do
+ before { variant.destroy && variant.reload }
+
+ it 'has a price if deleted' do
+ variant.price = 10
+ expect(variant.price).to eq(10)
+ end
+ end
+
+ describe 'stock movements' do
+ let!(:movement) { create(:stock_movement, stock_item: variant.stock_items.first) }
+
+ it 'builds out collection just fine through stock items' do
+ expect(variant.stock_movements.to_a).not_to be_empty
+ end
+ end
+
+ describe 'in_stock scope' do
+ it 'returns all in stock variants' do
+ in_stock_variant = create(:variant)
+ create(:variant) # out_of_stock_variant
+
+ in_stock_variant.stock_items.first.update_column(:count_on_hand, 10)
+
+ expect(Spree::Variant.in_stock).to eq [in_stock_variant]
+ end
+ end
+
+ context '#volume' do
+ let(:variant_zero_width) { create(:variant, width: 0) }
+ let(:variant) { create(:variant) }
+
+ it 'is zero if any dimension parameter is zero' do
+ expect(variant_zero_width.volume).to eq 0
+ end
+
+ it 'return the volume if the dimension parameters are different of zero' do
+ volume_expected = variant.width * variant.depth * variant.height
+ expect(variant.volume).to eq volume_expected
+ end
+ end
+
+ context '#dimension' do
+ let(:variant) { create(:variant) }
+
+ it 'return the dimension if the dimension parameters are different of zero' do
+ dimension_expected = variant.width + variant.depth + variant.height
+ expect(variant.dimension).to eq dimension_expected
+ end
+ end
+
+ context '#discontinue!' do
+ let(:variant) { create(:variant) }
+
+ it 'sets the discontinued' do
+ variant.discontinue!
+ variant.reload
+ expect(variant.discontinued?).to be(true)
+ end
+
+ it 'changes updated_at' do
+ Timecop.scale(1000) do
+ expect { variant.discontinue! }.to change(variant.reload, :updated_at)
+ end
+ end
+ end
+
+ context '#discontinued?' do
+ let(:variant_live) { build(:variant) }
+ let(:variant_discontinued) { build(:variant, discontinue_on: Time.now - 1.day) }
+
+ it 'is false' do
+ expect(variant_live.discontinued?).to be(false)
+ end
+
+ it 'is true' do
+ expect(variant_discontinued.discontinued?).to be(true)
+ end
+ end
+
+ describe '#available?' do
+ let(:variant) { create(:variant) }
+
+ context 'when discontinued' do
+ before do
+ variant.discontinue_on = Time.current - 1.day
+ end
+
+ context 'when product is available' do
+ before do
+ allow(variant.product).to receive(:available?).and_return(true)
+ end
+
+ it { expect(variant.available?).to be(false) }
+ end
+
+ context 'when product is not available' do
+ before do
+ allow(variant.product).to receive(:available?).and_return(false)
+ end
+
+ it { expect(variant.available?).to be(false) }
+ end
+ end
+
+ context 'when not discontinued' do
+ before do
+ variant.discontinue_on = Time.current + 1.day
+ end
+
+ context 'when product is available' do
+ before do
+ allow(variant.product).to receive(:available?).and_return(true)
+ end
+
+ it { expect(variant.available?).to be(true) }
+ end
+
+ context 'when product is not available' do
+ before do
+ allow(variant.product).to receive(:available?).and_return(false)
+ end
+
+ it { expect(variant.available?).to be(false) }
+ end
+ end
+ end
+
+ describe '#check_price' do
+ let(:variant) { create(:variant) }
+ let(:variant2) { create(:variant) }
+
+ context 'require_master_price set false' do
+ before { Spree::Config.set(require_master_price: false) }
+
+ context 'price present and currency present' do
+ it { expect(variant.send(:check_price)).to be(nil) }
+ end
+
+ context 'price present and currency nil' do
+ before { variant.currency = nil }
+
+ it { expect(variant.send(:check_price)).to be(Spree::Config[:currency]) }
+ end
+
+ context 'price nil and currency present' do
+ before { variant.price = nil }
+
+ it { expect(variant.send(:check_price)).to be(nil) }
+ end
+
+ context 'price nil and currency nil' do
+ before { variant.price = nil }
+
+ it { expect(variant.send(:check_price)).to be(nil) }
+ end
+ end
+
+ context 'require_master_price set true' do
+ before { Spree::Config.set(require_master_price: true) }
+
+ context 'price present and currency present' do
+ it { expect(variant.send(:check_price)).to be(nil) }
+ end
+
+ context 'price present and currency nil' do
+ before { variant.currency = nil }
+
+ it { expect(variant.send(:check_price)).to be(Spree::Config[:currency]) }
+ end
+
+ context 'product and master_variant present and equal' do
+ context 'price nil and currency present' do
+ before { variant.price = nil }
+
+ it { expect(variant.send(:check_price)).to be(nil) }
+
+ context 'check variant price' do
+ before { variant.send(:check_price) }
+
+ it { expect(variant.price).to eq(variant.product.master.price) }
+ end
+ end
+
+ context 'price nil and currency nil' do
+ before do
+ variant.price = nil
+ variant.send(:check_price)
+ end
+
+ it { expect(variant.price).to eq(variant.product.master.price) }
+ it { expect(variant.currency).to eq(Spree::Config[:currency]) }
+ end
+ end
+
+ context 'product not present' do
+ context 'product not present' do
+ before { variant.product = nil }
+
+ context 'price nil and currency present' do
+ before { variant.price = nil }
+
+ it 'adds absence of master error' do
+ variant.send(:check_price)
+ expect(variant.errors[:base]).to include I18n.t('activerecord.errors.models.spree/variant.attributes.base.no_master_variant_found_to_infer_price')
+ end
+ end
+
+ context 'price nil and currency nil' do
+ before { variant.price = nil }
+
+ it 'adds absence of master error' do
+ variant.send(:check_price)
+ expect(variant.errors[:base]).to include I18n.t('activerecord.errors.models.spree/variant.attributes.base.no_master_variant_found_to_infer_price')
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '#created_at' do
+ it 'creates variant with created_at timestamp' do
+ expect(variant.created_at).not_to be_nil
+ end
+ end
+
+ describe '#updated_at' do
+ it 'creates variant with updated_at timestamp' do
+ expect(variant.updated_at).not_to be_nil
+ end
+ end
+
+ context '#backordered?' do
+ let!(:variant) { create(:variant) }
+
+ it 'returns true when out of stock and backorderable' do
+ expect(variant.backordered?).to eq(true)
+ end
+
+ it 'returns false when out of stock and not backorderable' do
+ variant.stock_items.first.update(backorderable: false)
+ expect(variant.backordered?).to eq(false)
+ end
+
+ it 'returns false when there is available item in stock' do
+ variant.stock_items.first.update(count_on_hand: 10)
+ expect(variant.backordered?).to eq(false)
+ end
+ end
+
+ describe '#ensure_no_line_items' do
+ let!(:line_item) { create(:line_item, variant: variant) }
+
+ it 'adds error on product destroy' do
+ expect(variant.destroy).to eq false
+ expect(variant.errors[:base]).to include I18n.t('activerecord.errors.models.spree/variant.attributes.base.cannot_destroy_if_attached_to_line_items')
+ end
+ end
+end
diff --git a/core/spec/models/spree/zone_member_spec.rb b/core/spec/models/spree/zone_member_spec.rb
new file mode 100644
index 00000000000..9821e234240
--- /dev/null
+++ b/core/spec/models/spree/zone_member_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Spree::ZoneMember, type: :model do
+ let(:country) { create(:country) }
+ let(:state) { create(:state) }
+ let(:zone) { create(:zone, kind: 'country') }
+ let(:zone_member) { create(:zone_member, zone: zone, zoneable: country) }
+
+ describe 'scopes' do
+ describe '.defunct_without_kind' do
+ let(:defunct_without_kind) { Spree::ZoneMember.defunct_without_kind('country') }
+
+ context 'zoneable is present and is of defunct kind' do
+ it { expect(defunct_without_kind).not_to include(zone_member) }
+ end
+
+ context 'zoneable is not of defunct kind' do
+ before { zone_member.update(zoneable: state) }
+
+ it { expect(defunct_without_kind).to include(zone_member) }
+ end
+
+ context 'zoneable is absent' do
+ before { zone_member.update_column(:zoneable_id, nil) }
+
+ it { expect(defunct_without_kind).to include(zone_member) }
+ end
+ end
+ end
+end
diff --git a/core/spec/models/spree/zone_spec.rb b/core/spec/models/spree/zone_spec.rb
new file mode 100644
index 00000000000..41907d5331b
--- /dev/null
+++ b/core/spec/models/spree/zone_spec.rb
@@ -0,0 +1,468 @@
+require 'spec_helper'
+
+describe Spree::Zone, type: :model do
+ context '#match' do
+ let(:country_zone) { create(:zone, kind: 'country') }
+
+ let(:country) do
+ create(:country)
+ end
+
+ let(:state) do
+ create(:state, country: country)
+ end
+
+ before { country_zone.members.create(zoneable: country) }
+
+ describe 'scopes' do
+ describe '.remove_previous_default' do
+ subject { Spree::Zone.with_default_tax }
+
+ let(:zone_with_default_tax) { create(:zone, kind: 'country', default_tax: true) }
+ let(:zone_not_with_default_tax) { create(:zone, kind: 'country', default_tax: false) }
+
+ it 'is expected to include zone with default tax' do
+ expect(subject).to include(zone_with_default_tax)
+ end
+
+ it 'is expected to not include zone with default tax' do
+ expect(subject).not_to include(zone_not_with_default_tax)
+ end
+ end
+ end
+
+ describe 'callbacks' do
+ describe '#remove_previous_default' do
+ let!(:zone_with_default_tax) { create(:zone, kind: 'country', default_tax: true) }
+ let!(:zone_not_with_default_tax) { create(:zone, kind: 'country', default_tax: false) }
+
+ it 'is expected to make previous default tax zones to non default tax zones' do
+ expect(zone_with_default_tax).to be_default_tax
+ zone_not_with_default_tax.update(default_tax: true)
+ expect(zone_with_default_tax.reload).not_to be_default_tax
+ end
+ end
+ end
+
+ context 'when there is only one qualifying zone' do
+ let(:address) { create(:address, country: country, state: state) }
+
+ it 'returns the qualifying zone' do
+ expect(Spree::Zone.match(address)).to eq(country_zone)
+ end
+ end
+
+ context 'when there are two qualified zones with same member type' do
+ let(:address) { create(:address, country: country, state: state) }
+ let(:second_zone) { create(:zone, name: 'SecondZone') }
+
+ before { second_zone.members.create(zoneable: country) }
+
+ context 'when both zones have the same number of members' do
+ it 'returns the zone that was created first' do
+ Timecop.scale(100) do
+ expect(Spree::Zone.match(address)).to eq(country_zone)
+ end
+ end
+ end
+
+ context 'when one of the zones has fewer members' do
+ let(:country2) { create(:country) }
+
+ before { country_zone.members.create(zoneable: country2) }
+
+ it 'returns the zone with fewer members' do
+ expect(Spree::Zone.match(address)).to eq(second_zone)
+ end
+ end
+ end
+
+ context 'when there are two qualified zones with different member types' do
+ let(:state_zone) { create(:zone, kind: 'state') }
+ let(:address) { create(:address, country: country, state: state) }
+
+ before { state_zone.members.create!(zoneable: state) }
+
+ it 'returns the zone with the more specific member type' do
+ expect(Spree::Zone.match(address)).to eq(state_zone)
+ end
+ end
+
+ context 'when there are no qualifying zones' do
+ it 'returns nil' do
+ expect(Spree::Zone.match(Spree::Address.new)).to be_nil
+ end
+ end
+ end
+
+ context '#country_list' do
+ let(:state) { create(:state) }
+ let(:country) { state.country }
+
+ context 'when zone consists of countries' do
+ let(:country_zone) { create(:zone, kind: 'country') }
+
+ before { country_zone.members.create(zoneable: country) }
+
+ it 'returns a list of countries' do
+ expect(country_zone.country_list).to eq([country])
+ end
+ end
+
+ context 'when zone consists of states' do
+ let(:state_zone) { create(:zone, kind: 'state') }
+
+ before { state_zone.members.create(zoneable: state) }
+
+ it 'returns a list of countries' do
+ expect(state_zone.country_list).to eq([state.country])
+ end
+ end
+ end
+
+ context '#include?' do
+ let(:state) { create(:state) }
+ let(:country) { state.country }
+ let(:address) { create(:address, state: state) }
+
+ context 'when zone is country type' do
+ let(:country_zone) { create(:zone, kind: 'country') }
+
+ before { country_zone.members.create(zoneable: country) }
+
+ it 'is true' do
+ expect(country_zone.include?(address)).to be true
+ end
+ end
+
+ context 'when zone is state type' do
+ let(:state_zone) { create(:zone, kind: 'state') }
+
+ before { state_zone.members.create(zoneable: state) }
+
+ it 'is true' do
+ expect(state_zone.include?(address)).to be true
+ end
+ end
+ end
+
+ context '.default_tax' do
+ context 'when there is a default tax zone specified' do
+ before { @foo_zone = create(:zone, name: 'whatever', default_tax: true) }
+
+ it 'is the correct zone' do
+ create(:zone, name: 'foo')
+ expect(Spree::Zone.default_tax).to eq(@foo_zone)
+ end
+ end
+
+ context 'when there is no default tax zone specified' do
+ it 'is nil' do
+ expect(Spree::Zone.default_tax).to be_nil
+ end
+ end
+ end
+
+ context '#contains?' do
+ let(:country1) { create(:country) }
+ let(:country2) { create(:country) }
+ let(:country3) { create(:country) }
+
+ let(:state1) { create(:state) }
+ let(:state2) { create(:state) }
+ let(:state3) { create(:state) }
+
+ before do
+ @source = create(:zone, name: 'source', zone_members: [])
+ @target = create(:zone, name: 'target', zone_members: [])
+ end
+
+ context 'when the target has no members' do
+ before { @source.members.create(zoneable: country1) }
+
+ it 'is false' do
+ expect(@source.contains?(@target)).to be false
+ end
+ end
+
+ context 'when the source has no members' do
+ before { @target.members.create(zoneable: country1) }
+
+ it 'is false' do
+ expect(@source.contains?(@target)).to be false
+ end
+ end
+
+ context 'when both zones are the same zone' do
+ before do
+ @source.members.create(zoneable: country1)
+ @target = @source
+ end
+
+ it 'is true' do
+ expect(@source.contains?(@target)).to be true
+ end
+ end
+
+ context 'when checking countries against countries' do
+ before do
+ @source.members.create(zoneable: country1)
+ @source.members.create(zoneable: country2)
+ end
+
+ context 'when all members are included in the zone we check against' do
+ before do
+ @target.members.create(zoneable: country1)
+ @target.members.create(zoneable: country2)
+ end
+
+ it 'is true' do
+ expect(@source.contains?(@target)).to be true
+ end
+ end
+
+ context 'when some members are included in the zone we check against' do
+ before do
+ @target.members.create(zoneable: country1)
+ @target.members.create(zoneable: country2)
+ @target.members.create(zoneable: create(:country))
+ end
+
+ it 'is false' do
+ expect(@source.contains?(@target)).to be false
+ end
+ end
+
+ context 'when none of the members are included in the zone we check against' do
+ before do
+ @target.members.create(zoneable: create(:country))
+ @target.members.create(zoneable: create(:country))
+ end
+
+ it 'is false' do
+ expect(@source.contains?(@target)).to be false
+ end
+ end
+ end
+
+ context 'when checking states against states' do
+ before do
+ @source.members.create(zoneable: state1)
+ @source.members.create(zoneable: state2)
+ end
+
+ context 'when all members are included in the zone we check against' do
+ before do
+ @target.members.create(zoneable: state1)
+ @target.members.create(zoneable: state2)
+ end
+
+ it 'is true' do
+ expect(@source.contains?(@target)).to be true
+ end
+ end
+
+ context 'when some members are included in the zone we check against' do
+ before do
+ @target.members.create(zoneable: state1)
+ @target.members.create(zoneable: state2)
+ @target.members.create(zoneable: create(:state))
+ end
+
+ it 'is false' do
+ expect(@source.contains?(@target)).to be false
+ end
+ end
+
+ context 'when none of the members are included in the zone we check against' do
+ before do
+ @target.members.create(zoneable: create(:state))
+ @target.members.create(zoneable: create(:state))
+ end
+
+ it 'is false' do
+ expect(@source.contains?(@target)).to be false
+ end
+ end
+ end
+
+ context 'when checking country against state' do
+ before do
+ @source.members.create(zoneable: create(:state))
+ @target.members.create(zoneable: country1)
+ end
+
+ it 'is false' do
+ expect(@source.contains?(@target)).to be false
+ end
+ end
+
+ context 'when checking state against country' do
+ before { @source.members.create(zoneable: country1) }
+
+ context 'when all states contained in one of the countries we check against' do
+ before do
+ state1 = create(:state, country: country1)
+ @target.members.create(zoneable: state1)
+ end
+
+ it 'is true' do
+ expect(@source.contains?(@target)).to be true
+ end
+ end
+
+ context 'when some states contained in one of the countries we check against' do
+ before do
+ state1 = create(:state, country: country1)
+ @target.members.create(zoneable: state1)
+ @target.members.create(zoneable: create(:state, country: country2))
+ end
+
+ it 'is false' do
+ expect(@source.contains?(@target)).to be false
+ end
+ end
+
+ context 'when none of the states contained in any of the countries we check against' do
+ before do
+ @target.members.create(zoneable: create(:state, country: country2))
+ @target.members.create(zoneable: create(:state, country: country2))
+ end
+
+ it 'is false' do
+ expect(@source.contains?(@target)).to be false
+ end
+ end
+ end
+ end
+
+ context '#save' do
+ context 'when default_tax is true' do
+ it 'clears previous default tax zone' do
+ zone1 = create(:zone, name: 'foo', default_tax: true)
+ create(:zone, name: 'bar', default_tax: true)
+ expect(zone1.reload.default_tax).to be false
+ end
+ end
+
+ context 'when a zone member country is added to an existing zone consisting of state members' do
+ it 'removes existing state members' do
+ zone = create(:zone, name: 'foo', zone_members: [])
+ state = create(:state)
+ country = create(:country)
+ zone.members.create(zoneable: state)
+ country_member = zone.members.create(zoneable: country)
+ zone.save
+ expect(zone.reload.members).to eq([country_member])
+ end
+ end
+ end
+
+ context '#kind' do
+ it 'returns whatever value you set' do
+ zone = Spree::Zone.new kind: 'city'
+ expect(zone.kind).to eq 'city'
+ end
+
+ context 'when the zone consists of country zone members' do
+ before do
+ @zone = create(:zone, name: 'country', zone_members: [])
+ @zone.members.create(zoneable: create(:country))
+ end
+
+ it 'returns the kind of zone member' do
+ expect(@zone.kind).to eq('country')
+ end
+ end
+ end
+
+ context '#potential_matching_zones' do
+ let!(:country) { create(:country) }
+ let!(:country2) { create(:country, name: 'OtherCountry') }
+ let!(:country3) { create(:country, name: 'TaxCountry') }
+ let!(:default_tax_zone) do
+ create(:zone, default_tax: true).tap { |z| z.members.create(zoneable: country3) }
+ end
+
+ context 'finding potential matches for a country zone' do
+ let!(:zone) do
+ create(:zone).tap do |z|
+ z.members.create(zoneable: country)
+ z.members.create(zoneable: country2)
+ z.save!
+ end
+ end
+ let!(:zone2) do
+ create(:zone).tap { |z| z.members.create(zoneable: country) && z.save! }
+ end
+
+ before { @result = Spree::Zone.potential_matching_zones(zone) }
+
+ it 'will find all zones with countries covered by the passed in zone' do
+ expect(@result).to include(zone, zone2)
+ end
+
+ it 'only returns each zone once' do
+ expect(@result.select { |z| z == zone }.size).to be 1
+ end
+ end
+
+ context 'finding potential matches for a state zone' do
+ let!(:state) { create(:state, country: country) }
+ let!(:state2) { create(:state, country: country2, name: 'OtherState') }
+ let!(:state3) { create(:state, country: country2, name: 'State') }
+ let!(:zone) do
+ create(:zone).tap do |z|
+ z.members.create(zoneable: state)
+ z.members.create(zoneable: state2)
+ z.save!
+ end
+ end
+ let!(:zone2) do
+ create(:zone).tap { |z| z.members.create(zoneable: state) && z.save! }
+ end
+ let!(:zone3) do
+ create(:zone).tap { |z| z.members.create(zoneable: state2) && z.save! }
+ end
+
+ before { @result = Spree::Zone.potential_matching_zones(zone) }
+
+ it 'will find all zones which share states covered by passed in zone' do
+ expect(@result).to include(zone, zone2)
+ end
+
+ it 'will find zones that share countries with any states of the passed in zone' do
+ expect(@result).to include(zone3)
+ end
+
+ it 'only returns each zone once' do
+ expect(@result.select { |z| z == zone }.size).to be 1
+ end
+ end
+ end
+
+ context 'state and country associations' do
+ let!(:country) { create(:country) }
+
+ context 'has countries associated' do
+ let!(:zone) do
+ create(:zone, countries: [country])
+ end
+
+ it 'can access associated countries' do
+ expect(zone.countries).to include(country)
+ end
+ end
+
+ context 'has states associated' do
+ let!(:state) { create(:state, country: country) }
+ let!(:zone) do
+ create(:zone, states: [state])
+ end
+
+ it 'can access associated states' do
+ expect(zone.states).to include(state)
+ end
+ end
+ end
+end
diff --git a/core/spec/services/spree/cart/add_item_spec.rb b/core/spec/services/spree/cart/add_item_spec.rb
new file mode 100644
index 00000000000..12e9d8a8ef1
--- /dev/null
+++ b/core/spec/services/spree/cart/add_item_spec.rb
@@ -0,0 +1,225 @@
+require 'spec_helper'
+
+module Spree
+ describe Cart::AddItem do
+ subject { described_class }
+
+ let(:order) { create :order }
+ let(:variant) { create :variant, price: 20 }
+ let(:qty) { 1 }
+ let(:execute) { subject.call(order: order, variant: variant, quantity: qty) }
+ let(:value) { execute.value }
+ let(:expected_line_item) { order.reload.line_items.first }
+
+ context 'add line item to order' do
+ it 'change by one and recalculate amount' do
+ expect { execute }.to change { order.line_items.count }.by(1)
+ expect(execute).to be_success
+ expect(value).to eq expected_line_item
+ expect(order.amount).to eq 20
+ end
+ end
+
+ context 'with same line item' do
+ let(:line_item) { create :line_item, variant: variant }
+ let(:order) { create :order, line_items: [line_item] }
+
+ it 'not to add' do
+ expect(execute).to be_success
+ expect(value).to eq expected_line_item
+ expect(order.line_items.count).to eq 1
+ end
+ end
+
+ context 'with given shipment' do
+ let(:shipment) { create :shipment }
+ let(:options) { { shipment: shipment } }
+ let(:execute) { subject.call(order: order, variant: variant, quantity: qty, options: options) }
+
+ it 'ensure shipment calls update_amounts instead of order calling ensure_updated_shipments' do
+ expect(order).to receive(:refresh_shipment_rates).with(Spree::ShippingMethod::DISPLAY_ON_BACK_END)
+ expect(order).not_to receive(:ensure_updated_shipments)
+ expect(shipment).to receive(:update_amounts)
+ expect(execute).to be_success
+ end
+ end
+
+ context 'not given a shipment' do
+ let(:execute) { subject.call(order: order, variant: variant, quantity: qty) }
+
+ it 'ensures updated shipments' do
+ expect(order).to receive(:ensure_updated_shipments)
+ expect(execute).to be_success
+ end
+ end
+
+ context 'with store_credits payment' do
+ let!(:payment) { create(:store_credit_payment, order: order) }
+ let(:execute) { subject.call(order: order, variant: variant, quantity: 1) }
+
+ it do
+ expect { execute }.to change { order.payments.store_credits.count }.by(-1)
+ end
+ end
+
+ context 'running promotions' do
+ let(:promotion) { create(:promotion) }
+ let(:calculator) { Spree::Calculator::FlatRate.new(preferred_amount: 10) }
+
+ context 'one active order promotion' do
+ let!(:action) { Spree::Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) }
+
+ before do
+ subject.call(order: order, variant: variant, quantity: 1)
+ order.reload
+ end
+
+ it 'creates valid discount on order' do
+ subject.call(order: order, variant: variant, quantity: 1)
+ expect(order.adjustments.to_a.sum(&:amount)).not_to eq 0
+ expect(order.total).to eq 30
+ end
+ end
+
+ context 'one active line item promotion' do
+ let!(:action) { Spree::Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) }
+
+ before do
+ subject.call(order: order, variant: variant, quantity: 1)
+ order.reload
+ end
+
+ it 'creates valid discount on order' do
+ subject.call(order: order, variant: variant, quantity: 1)
+ expect(order.line_item_adjustments.to_a.sum(&:amount)).not_to eq 0
+ expect(order.total).to eq 30
+ end
+ end
+
+ context 'VAT for variant with percent promotion' do
+ let!(:category) { Spree::TaxCategory.create name: 'Taxable Foo' }
+ let!(:rate) do
+ Spree::TaxRate.create(
+ amount: 0.25,
+ included_in_price: true,
+ calculator: Spree::Calculator::DefaultTax.create,
+ tax_category: category,
+ zone: create(:zone_with_country, default_tax: true)
+ )
+ end
+ let(:variant) { create(:variant, price: 1000) }
+ let(:calculator) { Spree::Calculator::PercentOnLineItem.new(preferred_percent: 50) }
+ let!(:action) { Spree::Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) }
+
+ it 'updates included_tax_total' do
+ expect(order.included_tax_total.to_f).to eq(0.00)
+ subject.call(order: order, variant: variant, quantity: 1)
+ expect(order.included_tax_total.to_f).to eq(100)
+ end
+
+ it 'updates included_tax_total after adding two line items' do
+ subject.call(order: order, variant: variant, quantity: 1)
+ expect(order.included_tax_total.to_f).to eq(100)
+ subject.call(order: order, variant: variant, quantity: 1)
+ expect(order.included_tax_total.to_f).to eq(200)
+ end
+ end
+ end
+
+ context 'pass valid params hash in options' do
+ let(:options) { { quantity: 2, variant_id: variant.id } }
+ let(:execute) { subject.call(order: order, variant: variant, quantity: nil, options: options) }
+
+ it do
+ expect(execute).to be_success
+ expect(order.line_items.count).to eq 1
+ line_item = order.line_items.first
+ expect(line_item.quantity).to eq 2
+ end
+ end
+
+ context 'pass invalid arguments' do
+ context 'different quantity in argument and in options' do
+ let(:options) { { quantity: 2 } }
+ let(:execute) { subject.call(order: order, variant: variant, quantity: 3, options: options) }
+
+ it 'take value from options' do
+ expect(execute).to be_success
+ line_item = order.line_items.first
+ expect(line_item.quantity).to eq 2
+ end
+ end
+
+ context 'different quantity no quantity in argument and in params' do
+ let(:options) { {} }
+ let(:execute) { subject.call(order: order, variant: variant, quantity: nil, options: options) }
+
+ it 'set default' do
+ expect(execute).to be_success
+ line_item = order.line_items.first
+ expect(line_item.quantity).to eq 1
+ end
+ end
+
+ context 'not permitted' do
+ let(:options) { { dummy_param: true } }
+ let(:execute) { subject.call(order: order, variant: variant, quantity: 1, options: options) }
+
+ it do
+ expect(execute).to be_success
+ line_item = order.line_items.first
+ expect(line_item.quantity).to eq 1
+ end
+ end
+
+ context 'pass non-existing variant' do
+ let(:variant_2) { create :variant }
+ let(:execute) { subject.call(order: order, variant: variant_2, quantity: 1) }
+
+ before { Spree::Variant.find(variant_2.id).destroy }
+
+ it do
+ expect(execute).to be_failure
+ order.reload
+ expect(order.line_items.count).to eq 0
+ end
+ end
+
+ context 'pass non-existing variant' do
+ let(:variant_2) { create :variant }
+ let(:execute) { subject.call(order: order, variant: variant_2, quantity: 1) }
+
+ before { Spree::Variant.find(variant_2.id).destroy }
+
+ it do
+ expect(execute).to be_failure
+ order.reload
+ expect(order.line_items.count).to eq 0
+ end
+ end
+
+ context 'variant have not desired quantity' do
+ let(:execute) { subject.call(order: order, variant: variant, quantity: 10) }
+
+ before { variant.stock_items.first.update backorderable: false }
+
+ it do
+ expect(execute).to be_failure
+ order.reload
+ expect(order.line_items.count).to eq 0
+ end
+ end
+
+ context 'variant has been descontinued' do
+ let(:variant) { create :variant, discontinue_on: 1.day.ago }
+ let(:execute) { subject.call(order: order, variant: variant, quantity: 10) }
+
+ it do
+ expect(execute).to be_failure
+ order.reload
+ expect(order.line_items.count).to eq 0
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/services/spree/cart/create_spec.rb b/core/spec/services/spree/cart/create_spec.rb
new file mode 100644
index 00000000000..b3b85e3d07f
--- /dev/null
+++ b/core/spec/services/spree/cart/create_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+module Spree
+ describe Cart::Create do
+ subject { described_class }
+
+ let(:user) { create :user }
+ let(:store) { create :store }
+ let(:currency) { 'USD' }
+ let(:expected) { Order.first }
+
+ context 'create an order' do
+ let(:execute) { subject.call user: user, store: store, currency: currency }
+ let(:value) { execute.value }
+
+ it do
+ expect { execute }.to change(Order, :count)
+ expect(execute).to be_success
+ expect(value).to eq expected
+ expect(expected.number).to be_present
+ end
+ end
+
+ context 'create an order with store in params' do
+ let(:store_2) { create :store }
+ let(:order_params) { { store: store_2, currency: 'XVII' } }
+ let(:execute) { subject.call user: user, store: store, currency: currency, order_params: order_params }
+ let(:value) { execute.value }
+
+ it do
+ expect { execute }.to change(Order, :count)
+ expect(execute).to be_success
+ expect(value).to eq expected
+ expect(expected.user).to eq user
+ expect(expected.store).to eq store_2
+ expect(expected.currency).to eq 'XVII'
+ expect(expected.number).to be_present
+ end
+ end
+
+ context 'create an order when no store and currency pass in params' do
+ let!(:default_store) { create :store, default: true }
+ let(:execute) { subject.call user: user, store: nil, currency: nil }
+ let(:value) { execute.value }
+
+ it do
+ expect { execute }.to change(Order, :count)
+ expect(execute).to be_success
+ expect(value).to eq expected
+ expect(expected.currency).to eq Spree::Config[:currency]
+ expect(expected.store).to eq Spree::Store.default
+ expect(expected.number).to be_present
+ end
+ end
+ end
+end
diff --git a/core/spec/services/spree/cart/remove_item_spec.rb b/core/spec/services/spree/cart/remove_item_spec.rb
new file mode 100644
index 00000000000..c7c4394b2a0
--- /dev/null
+++ b/core/spec/services/spree/cart/remove_item_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+module Spree
+ describe Cart::RemoveItem do
+ subject { described_class }
+
+ let(:order) { create :order, line_items: [line_item] }
+ let(:line_item) { create :line_item, variant: variant, price: nil }
+ let(:variant) { create :variant, price: 20 }
+ let(:execute) { subject.call order: order, variant: variant }
+ let(:value) { execute.value }
+
+ context 'single line item' do
+ it 'remove item from order' do
+ expect(order.amount).to eq 20
+ expect { execute }.to change { order.line_items.count }.by(-1)
+ expect(execute).to be_success
+ expect(value).to eq line_item
+ expect(order.amount).to eq 0
+ end
+ end
+
+ context 'line items with more than one quantity' do
+ let(:line_item) { create :line_item, variant: variant, quantity: 2, price: nil }
+ let(:execute) { subject.call order: order, variant: variant }
+
+ it 'remove quantity from line item' do
+ expect { execute }.to change(order, :amount).by(-20)
+ expect(execute).to be_success
+ expect(value).to eq line_item
+ line_item.reload
+ expect(order.line_items.count).to eq 1
+ expect(line_item.quantity).to eq 1
+ end
+ end
+
+ context 'raise error' do
+ let(:variant_2) { create :variant }
+ let(:execute) { subject.call order: order, variant: variant_2 }
+
+ it 'when try remove non existing item' do
+ expect { execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'given a shipment' do
+ let(:shipment) { create :shipment }
+ let(:options) { { shipment: shipment } }
+ let(:execute) { subject.call order: order, variant: variant, options: options }
+
+ it 'ensure shipment calls update_amounts instead of order calling ensure_updated_shipments' do
+ expect(order).not_to receive(:ensure_updated_shipments)
+ expect(shipment).to receive(:update_amounts)
+ expect(execute).to be_success
+ end
+ end
+
+ context 'not given a shipment' do
+ let(:execute) { subject.call order: order, variant: variant }
+
+ it 'ensures updated shipments' do
+ expect(order).to receive(:ensure_updated_shipments)
+ expect(execute).to be_success
+ end
+ end
+
+ context 'when store_credits payment' do
+ let!(:payment) { create(:store_credit_payment, order: order) }
+ let(:execute) { subject.call order: order, variant: variant }
+
+ it do
+ expect { execute }.to change { order.payments.store_credits.count }.by(-1)
+ end
+ end
+ end
+end
diff --git a/core/spec/services/spree/cart/remove_line_item_spec.rb b/core/spec/services/spree/cart/remove_line_item_spec.rb
new file mode 100644
index 00000000000..a7bdb8517a6
--- /dev/null
+++ b/core/spec/services/spree/cart/remove_line_item_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+module Spree
+ describe Cart::RemoveLineItem do
+ subject { described_class }
+
+ let(:order) { create :order, line_items: [line_item] }
+ let(:line_item) { create :line_item, variant: variant, price: nil, quantity: 10 }
+ let(:variant) { create :variant, price: 20 }
+ let(:execute) { subject.call order: order, line_item: line_item }
+ let(:value) { execute.value }
+
+ context 'remove line item' do
+ it 'with any quantity' do
+ expect(order.amount).to eq 200
+ expect { execute }.to change { order.line_items.count }.by(-1)
+ expect(execute).to be_success
+ expect(value).to eq line_item
+ order.reload
+ expect(order.amount).to eq 0
+ end
+ end
+
+ context 'given a shipment' do
+ let(:shipment) { create :shipment }
+ let(:options) { { shipment: shipment } }
+ let(:execute) { subject.call order: order, line_item: line_item, options: options }
+
+ it 'ensure shipment calls update_amounts instead of order calling ensure_updated_shipments' do
+ expect(order).not_to receive(:ensure_updated_shipments)
+ expect(shipment).to receive(:update_amounts)
+ expect(execute).to be_success
+ end
+ end
+
+ context 'not given a shipment' do
+ let(:execute) { subject.call order: order, line_item: line_item }
+
+ it 'ensures updated shipments' do
+ expect(order).to receive(:ensure_updated_shipments)
+ expect(execute).to be_success
+ end
+ end
+ end
+end
diff --git a/core/spec/services/spree/cart/set_quantity_spec.rb b/core/spec/services/spree/cart/set_quantity_spec.rb
new file mode 100644
index 00000000000..1abbf03ea5c
--- /dev/null
+++ b/core/spec/services/spree/cart/set_quantity_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+module Spree
+ describe Cart::SetQuantity do
+ subject { described_class }
+
+ let!(:order) { Spree::Order.create }
+ let!(:line_item) { create(:line_item, order: order) }
+
+ context 'with non-backorderable item' do
+ before do
+ line_item.variant.stock_items.first.update(backorderable: false)
+ line_item.variant.stock_items.first.update(count_on_hand: 5)
+ end
+
+ context 'with sufficient stock quantity' do
+ it 'returns successful result', :aggregate_failures do
+ result = subject.call(order: order, line_item: line_item, quantity: 5)
+
+ expect(result.success).to eq(true)
+ expect(result.value).to be_a LineItem
+ expect(result.value.quantity).to eq(5)
+ end
+ end
+
+ context 'with insufficient stock quantity' do
+ it 'return result with success equal false', :aggregate_failures do
+ result = subject.call(order: order, line_item: line_item, quantity: 10)
+
+ expect(result.success).to eq(false)
+ expect(result.value).to be_a LineItem
+ expect(result.error.to_s).to eq("Quantity selected of \"#{line_item.name}\" is not available.")
+ end
+ end
+ end
+
+ context 'with backorderable item' do
+ it 'returns successfull result', :aggregate_failures do
+ result = subject.call(order: order, line_item: line_item, quantity: 5)
+
+ expect(result.success).to eq(true)
+ expect(result.value).to be_a LineItem
+ expect(result.value.quantity).to eq(5)
+ end
+ end
+ end
+end
diff --git a/core/spec/services/spree/checkout/add_store_credit_spec.rb b/core/spec/services/spree/checkout/add_store_credit_spec.rb
new file mode 100644
index 00000000000..aa8feaad19a
--- /dev/null
+++ b/core/spec/services/spree/checkout/add_store_credit_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe Spree::Checkout::AddStoreCredit, type: :service do
+
+ describe '#call' do
+ subject { described_class.call(order: order) }
+
+ let(:order_total) { 500.00 }
+
+ before { create(:store_credit_payment_method) }
+
+ context 'there is no store credit' do
+ let(:order) { create(:store_credits_order_without_user, total: order_total) }
+
+ before do
+ # callbacks recalculate total based on line items
+ # this ensures the total is what we expect
+ order.update_column(:total, order_total)
+ subject
+ order.reload
+ end
+
+ it 'does not create a store credit payment' do
+ expect(order.payments.count).to eq 0
+ end
+ end
+
+ context 'there is enough store credit to pay for the entire order' do
+ let(:store_credit) { create(:store_credit, amount: order_total) }
+ let(:order) { create(:order, user: store_credit.user, total: order_total) }
+
+ context 'with no amount specified' do
+ before do
+ subject
+ order.reload
+ end
+
+ it 'creates a store credit payment for the full amount' do
+ expect(order.payments.count).to eq 1
+ expect(order.payments.first).to be_store_credit
+ expect(order.payments.first.amount).to eq order_total
+ end
+ end
+
+ context 'with store credit amount specified' do
+ let(:requested_amount) { 300.0 }
+
+ before do
+ described_class.call(order: order, amount: requested_amount)
+ end
+
+ it 'creates a store credit payment for the specified amount' do
+ expect(order.payments.count).to eq 1
+ expect(order.payments.first).to be_store_credit
+ expect(order.payments.first.amount).to eq requested_amount
+ end
+ end
+ end
+
+ context 'the available store credit is not enough to pay for the entire order' do
+ let(:expected_cc_total) { 100.0 }
+ let(:store_credit_total) { order_total - expected_cc_total }
+ let(:store_credit) { create(:store_credit, amount: store_credit_total) }
+ let(:order) { create(:order, user: store_credit.user, total: order_total) }
+
+ before do
+ # callbacks recalculate total based on line items
+ # this ensures the total is what we expect
+ order.update_column(:total, order_total)
+ subject
+ order.reload
+ end
+
+ it 'creates a store credit payment for the available amount' do
+ expect(order.payments.count).to eq 1
+ expect(order.payments.first).to be_store_credit
+ expect(order.payments.first.amount).to eq store_credit_total
+ end
+ end
+
+ context 'there are multiple store credits' do
+ context 'they have different credit type priorities' do
+ let(:amount_difference) { 100 }
+ let!(:primary_store_credit) { create(:store_credit, amount: (order_total - amount_difference)) }
+ let!(:secondary_store_credit) do
+ create(:store_credit, amount: order_total, user: primary_store_credit.user,
+ credit_type: create(:secondary_credit_type))
+ end
+ let(:order) { create(:order, user: primary_store_credit.user, total: order_total) }
+
+ before do
+ Timecop.scale(3600)
+ subject
+ order.reload
+ end
+
+ after { Timecop.return }
+
+ it 'uses the primary store credit type over the secondary' do
+ primary_payment = order.payments.first
+ secondary_payment = order.payments.last
+
+ expect(order.payments.size).to eq 2
+ expect(primary_payment.source).to eq primary_store_credit
+ expect(secondary_payment.source).to eq secondary_store_credit
+ expect(primary_payment.amount).to eq(order_total - amount_difference)
+ expect(secondary_payment.amount).to eq(amount_difference)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/services/spree/checkout/get_shipping_rates_spec.rb b/core/spec/services/spree/checkout/get_shipping_rates_spec.rb
new file mode 100644
index 00000000000..d9bdfa1abdb
--- /dev/null
+++ b/core/spec/services/spree/checkout/get_shipping_rates_spec.rb
@@ -0,0 +1,127 @@
+require 'spec_helper'
+
+module Spree
+ describe Checkout::GetShippingRates do
+ subject { described_class }
+
+ let(:order) { create(:order) }
+ let(:line_item) { create(:line_item, order: order) }
+ let(:shipment) { order.reload.shipments.first }
+
+ let(:execute) { subject.call(order: order) }
+ let(:value) { execute.value }
+ let(:error) { execute.error.to_s }
+
+ let!(:country) { create(:country) }
+ let!(:shipping_method) do
+ create(:shipping_method).tap do |shipping_method|
+ shipping_method.calculator.preferred_amount = 10
+ shipping_method.calculator.save
+ shipping_method.zones = [zone]
+ end
+ end
+ let!(:zone) { create(:zone) }
+ let!(:zone_member) { create(:zone_member, zone: zone, zoneable: country) }
+ let(:address) { create(:address, country: country) }
+
+ let(:free_shipping_promotion) { create(:free_shipping_promotion) }
+
+ shared_examples 'generates shipping rates' do
+ it 'returns shipping rates' do
+ expect(execute.success?).to eq(true)
+ expect(value).not_to be_empty
+ expect(value).to eq(order.reload.shipments)
+ expect(shipment.shipping_method).to eq(shipping_method)
+ end
+
+ it "doesn't update checkout state" do
+ expect { execute }.not_to change {
+ order.state
+ order.completed_at
+ }
+ end
+ end
+
+ shared_examples 'applies standard shipping costs' do
+ before { execute }
+
+ it 'for shipment' do
+ expect(shipment.final_price).to eq(10.0)
+ end
+
+ it 'updates shipment total' do
+ expect(order.reload.shipment_total).to eq(10.0)
+ end
+ end
+
+ shared_examples 'failure' do
+ it 'returns error' do
+ expect(execute.success?).to eq(false)
+ expect(value).to be_empty
+ expect(error).to eq(error_message)
+ end
+
+ it "doesn't generate shipping rates" do
+ expect { execute }.not_to change {
+ Spree::ShippingRate.count
+ order.shipments
+ order.state
+ order.completed_at
+ }
+ end
+ end
+
+ context 'without shipping address' do
+ let(:error_message) { 'To generate Shipping Rates Order needs to have a Shipping Address' }
+
+ it_behaves_like 'failure'
+ end
+
+ context 'without line items' do
+ let(:error_message) { 'To generate Shipping Rates you need to add some Line Items to Order' }
+
+ before do
+ order.ship_address = address
+ order.save!
+ end
+
+ it_behaves_like 'failure'
+ end
+
+ context 'with line items and shipping address' do
+ before do
+ line_item
+ order.ship_address = address
+ order.save!
+ end
+
+ context 'without shipments' do
+ before { order.shipments.destroy_all }
+
+ it_behaves_like 'generates shipping rates'
+ it_behaves_like 'applies standard shipping costs'
+ end
+
+ context 'with already present shipments' do
+ it_behaves_like 'generates shipping rates'
+ it_behaves_like 'applies standard shipping costs'
+
+ it 'replaces current shipments with new ones' do
+ expect { execute }.to change { order.shipments.pluck(:number) }
+ end
+ end
+
+ context 'with free shipping promotion' do
+ before do
+ free_shipping_promotion
+ execute
+ end
+
+ it 'applies promotion' do
+ expect(order.promotions).to include(free_shipping_promotion)
+ expect(shipment.final_price).to eq(0.0)
+ end
+ end
+ end
+ end
+end
diff --git a/core/spec/services/spree/checkout/remove_store_credit_spec.rb b/core/spec/services/spree/checkout/remove_store_credit_spec.rb
new file mode 100644
index 00000000000..df5939b30c7
--- /dev/null
+++ b/core/spec/services/spree/checkout/remove_store_credit_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Spree::Checkout::RemoveStoreCredit, type: :service do
+ describe '#call' do
+ subject { described_class.call(order: order) }
+
+ let(:order_total) { 500.00 }
+ let(:order) { create(:order, user: store_credit.user, total: order_total) }
+
+ context 'when order is not complete' do
+ let(:store_credit) { create(:store_credit, amount: order_total - 1) }
+
+ before do
+ create(:store_credit_payment_method)
+ Spree::Checkout::AddStoreCredit.call(order: order)
+ end
+
+ it { expect { subject }.to change { order.payments.checkout.store_credits.count }.from(1).to(0) }
+ it { expect { subject }.to change { order.payments.with_state(:invalid).store_credits.count }.from(0).to(1) }
+ end
+
+ context 'when order is complete' do
+ let(:order) { create(:completed_order_with_store_credit_payment) }
+ let(:store_credit_payments) { order.payments.checkout.store_credits }
+
+ before do
+ subject
+ order.reload
+ end
+
+ it { expect(order.payments.checkout.store_credits).to eq store_credit_payments }
+ end
+ end
+end
diff --git a/core/spec/services/spree/checkout/update_spec.rb b/core/spec/services/spree/checkout/update_spec.rb
new file mode 100644
index 00000000000..ff52d43cad9
--- /dev/null
+++ b/core/spec/services/spree/checkout/update_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Spree::Checkout::Update, type: :service do
+ describe '#transform_address_params' do
+ let!(:state) { create(:state) }
+ let!(:country) { state.country }
+ let!(:replace_country) { described_class.new }
+ let!(:address) do
+ {
+ firstname: 'John',
+ lastname: 'Doe',
+ address1: '7735 Old Georgetown Road',
+ city: 'Bethesda',
+ phone: '3014445002',
+ zipcode: '20814',
+ state_id: state.id,
+ country_iso: country.iso
+ }
+ end
+
+ context 'with ship_address order params' do
+ let(:order_params) do
+ ActionController::Parameters.new(
+ order: {
+ ship_address_attributes: address
+ }
+ )
+ end
+ let(:result) { replace_country.send(:replace_country_iso_with_id, order_params, 'ship') }
+
+ before { result }
+
+ it 'will return hash contain country_id' do
+ expect(result[:order][:ship_address_attributes][:country_id]).to eq country.id
+ end
+
+ it 'will return hash without country_iso' do
+ expect(result[:order][:ship_address_attributes]).not_to include(:country_iso)
+ end
+ end
+
+ context 'with bill_address order params' do
+ let(:order_params) do
+ ActionController::Parameters.new(
+ order: {
+ bill_address_attributes: address
+ }
+ )
+ end
+ let(:result) { replace_country.send(:replace_country_iso_with_id, order_params, 'bill') }
+
+ before { result }
+
+ it 'will return hash contain country_id' do
+ expect(result[:order][:bill_address_attributes][:country_id]).to eq country.id
+ end
+
+ it 'will return hash without country_iso' do
+ expect(result[:order][:bill_address_attributes]).not_to include(:country_iso)
+ end
+ end
+ end
+end
diff --git a/core/spec/spec_helper.rb b/core/spec/spec_helper.rb
new file mode 100644
index 00000000000..25a5c06808b
--- /dev/null
+++ b/core/spec/spec_helper.rb
@@ -0,0 +1,80 @@
+if ENV['COVERAGE']
+ # Run Coverage report
+ require 'simplecov'
+ SimpleCov.start 'rails' do
+ add_group 'Finders', 'app/finders'
+ add_group 'Mailers', 'app/mailers'
+ add_group 'Paginators', 'app/paginators'
+ add_group 'Services', 'app/services'
+ add_group 'Sorters', 'app/sorters'
+ add_group 'Validators', 'app/validators'
+ add_group 'Libraries', 'lib/spree'
+
+ add_filter '/bin/'
+ add_filter '/db/'
+ add_filter '/script/'
+ add_filter '/spec/'
+ add_filter '/lib/spree/testing_support/'
+ add_filter '/lib/generators/'
+
+ coverage_dir "#{ENV['COVERAGE_DIR']}/core" if ENV['COVERAGE_DIR']
+ end
+end
+
+# This file is copied to ~/spec when you run 'ruby script/generate rspec'
+# from the project root directory.
+ENV['RAILS_ENV'] ||= 'test'
+
+begin
+ require File.expand_path('../dummy/config/environment', __FILE__)
+rescue LoadError
+ puts 'Could not load dummy application. Please ensure you have run `bundle exec rake test_app`'
+end
+
+require 'rspec/rails'
+require 'database_cleaner'
+require 'ffaker'
+
+Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
+
+require 'spree/testing_support/i18n' if ENV['CHECK_TRANSLATIONS']
+
+require 'spree/testing_support/factories'
+require 'spree/testing_support/preferences'
+require 'spree/testing_support/url_helpers'
+require 'spree/testing_support/kernel'
+
+RSpec.configure do |config|
+ config.color = true
+ config.default_formatter = 'doc'
+ config.fail_fast = ENV['FAIL_FAST'] || false
+ config.fixture_path = File.join(__dir__, 'fixtures')
+ config.infer_spec_type_from_file_location!
+ config.mock_with :rspec
+ config.raise_errors_for_deprecations!
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, comment the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = true
+
+ config.before do
+ begin
+ Rails.cache.clear
+ reset_spree_preferences
+ rescue Errno::ENOTEMPTY
+ end
+ end
+
+ config.include FactoryBot::Syntax::Methods
+ config.include Spree::TestingSupport::Preferences
+ config.include Spree::TestingSupport::Kernel
+
+ # Clean out the database state before the tests run
+ config.before(:suite) do
+ DatabaseCleaner.clean_with(:truncation)
+ end
+
+ config.order = :random
+ Kernel.srand config.seed
+end
diff --git a/core/spec/support/big_decimal.rb b/core/spec/support/big_decimal.rb
new file mode 100644
index 00000000000..826ea6932a0
--- /dev/null
+++ b/core/spec/support/big_decimal.rb
@@ -0,0 +1,5 @@
+class BigDecimal
+ def inspect
+ "#"
+ end
+end
diff --git a/core/spec/support/concerns/adjustment_source.rb b/core/spec/support/concerns/adjustment_source.rb
new file mode 100644
index 00000000000..6dfb6bd31d0
--- /dev/null
+++ b/core/spec/support/concerns/adjustment_source.rb
@@ -0,0 +1,25 @@
+shared_examples_for 'an adjustment source' do
+ subject(:source) { described_class.create }
+
+ before do
+ allow(Spree::Adjustable::AdjustmentsUpdater).to receive(:update)
+ order.adjustments.create(order: order, amount: 10, label: 'Adjustment', source: source)
+ end
+
+ describe '#destroy' do
+ before { source.destroy }
+
+ context 'when order incomplete' do
+ let(:order) { create(:order_with_line_items) }
+
+ it { expect(order.adjustments.count).to eq(0) }
+ end
+
+ context 'when order is complete' do
+ let(:order) { create(:completed_order_with_totals) }
+
+ it { expect(order.adjustments.count).to eq(1) }
+ it { expect(order.adjustments.reload.first.source).to be_nil }
+ end
+ end
+end
diff --git a/core/spec/support/concerns/default_price.rb b/core/spec/support/concerns/default_price.rb
new file mode 100644
index 00000000000..7b9f479dcd4
--- /dev/null
+++ b/core/spec/support/concerns/default_price.rb
@@ -0,0 +1,47 @@
+shared_examples_for 'default_price' do
+ subject(:instance) { FactoryBot.build(model.name.demodulize.downcase.to_sym) }
+
+ let(:model) { described_class }
+
+ describe '.has_one :default_price' do
+ let(:default_price_association) { model.reflect_on_association(:default_price) }
+
+ it 'is a has one association' do
+ expect(default_price_association.macro).to eq :has_one
+ end
+
+ it 'has a dependent destroy' do
+ expect(default_price_association.options[:dependent]).to eq :destroy
+ end
+
+ it 'has the class name of Spree::Price' do
+ expect(default_price_association.options[:class_name]).to eq 'Spree::Price'
+ end
+ end
+
+ describe '#default_price' do
+ subject { instance.default_price }
+
+ it 'returns a valid class' do
+ expect(subject.class).to eql(Spree::Price)
+ end
+
+ it 'delegates price' do
+ expect(instance.default_price).to receive(:price)
+ instance.price
+ end
+
+ it 'delegates price_including_vat_for' do
+ expect(instance.default_price).to receive(:price_including_vat_for)
+ instance.price_including_vat_for
+ end
+ end
+
+ describe '#has_default_price?' do
+ subject { instance.has_default_price? }
+
+ it 'should bo truthy' do
+ expect(subject).to be_truthy
+ end
+ end
+end
diff --git a/core/spec/support/rake.rb b/core/spec/support/rake.rb
new file mode 100644
index 00000000000..c57e122ca1d
--- /dev/null
+++ b/core/spec/support/rake.rb
@@ -0,0 +1,14 @@
+require 'rake'
+
+shared_context 'rake' do
+ subject { Rake::Task[task_name] }
+
+ let(:task_name) { self.class.top_level_description }
+ let(:task_path) { "lib/tasks/#{task_name.split(':').first}" }
+
+ before do
+ Rake::Task.define_task(:environment)
+ load File.expand_path(Rails.root + "../../#{task_path}.rake")
+ subject.reenable
+ end
+end
diff --git a/core/spec/support/test_gateway.rb b/core/spec/support/test_gateway.rb
new file mode 100644
index 00000000000..f6cf9ea0a00
--- /dev/null
+++ b/core/spec/support/test_gateway.rb
@@ -0,0 +1,2 @@
+class Spree::Gateway::Test < Spree::Gateway
+end
diff --git a/core/spec/validators/email_validator_spec.rb b/core/spec/validators/email_validator_spec.rb
new file mode 100644
index 00000000000..a9dbcb48463
--- /dev/null
+++ b/core/spec/validators/email_validator_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe EmailValidator do
+ class Tester
+ include ActiveModel::Validations
+ attr_accessor :email_address
+ validates :email_address, email: true
+ end
+
+ let(:valid_emails) do
+ [
+ 'valid@email.com',
+ 'valid@email.com.uk',
+ 'e@email.com',
+ 'valid+email@email.com',
+ 'valid-email@email.com',
+ 'valid_email@email.com',
+ 'validemail_@email.com',
+ 'valid.email@email.com',
+ 'valid.email@email.photography'
+ ]
+ end
+ let(:invalid_emails) do
+ [
+ '',
+ ' ',
+ 'invalid email@email.com',
+ 'invalidemail @email.com',
+ '@email.com',
+ 'invalidemailemail.com',
+ '@invalid.email@email.com',
+ 'invalid@email@email.com',
+ 'invalid.email@@email.com'
+ ]
+ end
+
+ let(:tester) { Tester.new }
+
+ it 'validates valid email addresses' do
+ valid_emails.each do |email|
+ tester.email_address = email
+ expect(tester).to be_valid
+ end
+ end
+
+ it 'validates invalid email addresses' do
+ invalid_emails.each do |email|
+ tester.email_address = email
+ expect(tester).to be_invalid
+ end
+ end
+end
diff --git a/core/spree_core.gemspec b/core/spree_core.gemspec
new file mode 100644
index 00000000000..dd183b4a52b
--- /dev/null
+++ b/core/spree_core.gemspec
@@ -0,0 +1,49 @@
+# encoding: UTF-8
+
+require_relative 'lib/spree/core/version.rb'
+
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'spree_core'
+ s.version = Spree.version
+ s.summary = 'The bare bones necessary for Spree.'
+ s.description = 'The bare bones necessary for Spree.'
+
+ s.required_ruby_version = '>= 2.3.3'
+ s.required_rubygems_version = '>= 1.8.23'
+
+ s.author = 'Sean Schofield'
+ s.email = 'sean@spreecommerce.com'
+ s.homepage = 'http://spreecommerce.org'
+ s.license = 'BSD-3-Clause'
+
+ s.files = `git ls-files`.split("\n").reject { |f| f.match(/^spec/) && !f.match(/^spec\/fixtures/) }
+ s.require_path = 'lib'
+
+ s.add_dependency 'activemerchant', '~> 1.67'
+ s.add_dependency 'acts_as_list', '~> 0.8'
+ s.add_dependency 'awesome_nested_set', '~> 3.1.4'
+ s.add_dependency 'carmen', '~> 1.0.0'
+ s.add_dependency 'cancancan', '~> 2.0'
+ s.add_dependency 'deface', '~> 1.0'
+ s.add_dependency 'ffaker', '~> 2.2'
+ s.add_dependency 'friendly_id', '~> 5.2.1'
+ s.add_dependency 'highline', '~> 2.0.0' # Necessary for the install generator
+ s.add_dependency 'kaminari', '~> 1.0.1'
+ s.add_dependency 'money', '~> 6.13'
+ s.add_dependency 'monetize', '~> 1.9'
+ s.add_dependency 'paperclip', '~> 6.1.0'
+ s.add_dependency 'paranoia', '~> 2.4.1'
+ s.add_dependency 'premailer-rails'
+ s.add_dependency 'acts-as-taggable-on', '~> 6.0'
+ s.add_dependency 'rails', '~> 5.2.1', '>= 5.2.1.1'
+ s.add_dependency 'ransack', '~> 2.1.1'
+ s.add_dependency 'responders'
+ s.add_dependency 'state_machines-activerecord', '~> 0.5'
+ s.add_dependency 'stringex'
+ s.add_dependency 'twitter_cldr', '~> 4.3'
+ s.add_dependency 'sprockets-rails'
+ s.add_dependency 'mini_magick', '~> 4.8.0'
+
+ s.add_development_dependency 'email_spec', '~> 1.6'
+end
diff --git a/core/vendor/assets/javascripts/fetch.umd.js b/core/vendor/assets/javascripts/fetch.umd.js
new file mode 100644
index 00000000000..f9b44fd7c5f
--- /dev/null
+++ b/core/vendor/assets/javascripts/fetch.umd.js
@@ -0,0 +1,531 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ (factory((global.WHATWGFetch = {})));
+}(this, (function (exports) { 'use strict';
+
+ var support = {
+ searchParams: 'URLSearchParams' in self,
+ iterable: 'Symbol' in self && 'iterator' in Symbol,
+ blob:
+ 'FileReader' in self &&
+ 'Blob' in self &&
+ (function() {
+ try {
+ new Blob();
+ return true
+ } catch (e) {
+ return false
+ }
+ })(),
+ formData: 'FormData' in self,
+ arrayBuffer: 'ArrayBuffer' in self
+ };
+
+ function isDataView(obj) {
+ return obj && DataView.prototype.isPrototypeOf(obj)
+ }
+
+ if (support.arrayBuffer) {
+ var viewClasses = [
+ '[object Int8Array]',
+ '[object Uint8Array]',
+ '[object Uint8ClampedArray]',
+ '[object Int16Array]',
+ '[object Uint16Array]',
+ '[object Int32Array]',
+ '[object Uint32Array]',
+ '[object Float32Array]',
+ '[object Float64Array]'
+ ];
+
+ var isArrayBufferView =
+ ArrayBuffer.isView ||
+ function(obj) {
+ return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
+ };
+ }
+
+ function normalizeName(name) {
+ if (typeof name !== 'string') {
+ name = String(name);
+ }
+ if (/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(name)) {
+ throw new TypeError('Invalid character in header field name')
+ }
+ return name.toLowerCase()
+ }
+
+ function normalizeValue(value) {
+ if (typeof value !== 'string') {
+ value = String(value);
+ }
+ return value
+ }
+
+ // Build a destructive iterator for the value list
+ function iteratorFor(items) {
+ var iterator = {
+ next: function() {
+ var value = items.shift();
+ return {done: value === undefined, value: value}
+ }
+ };
+
+ if (support.iterable) {
+ iterator[Symbol.iterator] = function() {
+ return iterator
+ };
+ }
+
+ return iterator
+ }
+
+ function Headers(headers) {
+ this.map = {};
+
+ if (headers instanceof Headers) {
+ headers.forEach(function(value, name) {
+ this.append(name, value);
+ }, this);
+ } else if (Array.isArray(headers)) {
+ headers.forEach(function(header) {
+ this.append(header[0], header[1]);
+ }, this);
+ } else if (headers) {
+ Object.getOwnPropertyNames(headers).forEach(function(name) {
+ this.append(name, headers[name]);
+ }, this);
+ }
+ }
+
+ Headers.prototype.append = function(name, value) {
+ name = normalizeName(name);
+ value = normalizeValue(value);
+ var oldValue = this.map[name];
+ this.map[name] = oldValue ? oldValue + ', ' + value : value;
+ };
+
+ Headers.prototype['delete'] = function(name) {
+ delete this.map[normalizeName(name)];
+ };
+
+ Headers.prototype.get = function(name) {
+ name = normalizeName(name);
+ return this.has(name) ? this.map[name] : null
+ };
+
+ Headers.prototype.has = function(name) {
+ return this.map.hasOwnProperty(normalizeName(name))
+ };
+
+ Headers.prototype.set = function(name, value) {
+ this.map[normalizeName(name)] = normalizeValue(value);
+ };
+
+ Headers.prototype.forEach = function(callback, thisArg) {
+ for (var name in this.map) {
+ if (this.map.hasOwnProperty(name)) {
+ callback.call(thisArg, this.map[name], name, this);
+ }
+ }
+ };
+
+ Headers.prototype.keys = function() {
+ var items = [];
+ this.forEach(function(value, name) {
+ items.push(name);
+ });
+ return iteratorFor(items)
+ };
+
+ Headers.prototype.values = function() {
+ var items = [];
+ this.forEach(function(value) {
+ items.push(value);
+ });
+ return iteratorFor(items)
+ };
+
+ Headers.prototype.entries = function() {
+ var items = [];
+ this.forEach(function(value, name) {
+ items.push([name, value]);
+ });
+ return iteratorFor(items)
+ };
+
+ if (support.iterable) {
+ Headers.prototype[Symbol.iterator] = Headers.prototype.entries;
+ }
+
+ function consumed(body) {
+ if (body.bodyUsed) {
+ return Promise.reject(new TypeError('Already read'))
+ }
+ body.bodyUsed = true;
+ }
+
+ function fileReaderReady(reader) {
+ return new Promise(function(resolve, reject) {
+ reader.onload = function() {
+ resolve(reader.result);
+ };
+ reader.onerror = function() {
+ reject(reader.error);
+ };
+ })
+ }
+
+ function readBlobAsArrayBuffer(blob) {
+ var reader = new FileReader();
+ var promise = fileReaderReady(reader);
+ reader.readAsArrayBuffer(blob);
+ return promise
+ }
+
+ function readBlobAsText(blob) {
+ var reader = new FileReader();
+ var promise = fileReaderReady(reader);
+ reader.readAsText(blob);
+ return promise
+ }
+
+ function readArrayBufferAsText(buf) {
+ var view = new Uint8Array(buf);
+ var chars = new Array(view.length);
+
+ for (var i = 0; i < view.length; i++) {
+ chars[i] = String.fromCharCode(view[i]);
+ }
+ return chars.join('')
+ }
+
+ function bufferClone(buf) {
+ if (buf.slice) {
+ return buf.slice(0)
+ } else {
+ var view = new Uint8Array(buf.byteLength);
+ view.set(new Uint8Array(buf));
+ return view.buffer
+ }
+ }
+
+ function Body() {
+ this.bodyUsed = false;
+
+ this._initBody = function(body) {
+ this._bodyInit = body;
+ if (!body) {
+ this._bodyText = '';
+ } else if (typeof body === 'string') {
+ this._bodyText = body;
+ } else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
+ this._bodyBlob = body;
+ } else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
+ this._bodyFormData = body;
+ } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
+ this._bodyText = body.toString();
+ } else if (support.arrayBuffer && support.blob && isDataView(body)) {
+ this._bodyArrayBuffer = bufferClone(body.buffer);
+ // IE 10-11 can't handle a DataView body.
+ this._bodyInit = new Blob([this._bodyArrayBuffer]);
+ } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
+ this._bodyArrayBuffer = bufferClone(body);
+ } else {
+ this._bodyText = body = Object.prototype.toString.call(body);
+ }
+
+ if (!this.headers.get('content-type')) {
+ if (typeof body === 'string') {
+ this.headers.set('content-type', 'text/plain;charset=UTF-8');
+ } else if (this._bodyBlob && this._bodyBlob.type) {
+ this.headers.set('content-type', this._bodyBlob.type);
+ } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
+ this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
+ }
+ }
+ };
+
+ if (support.blob) {
+ this.blob = function() {
+ var rejected = consumed(this);
+ if (rejected) {
+ return rejected
+ }
+
+ if (this._bodyBlob) {
+ return Promise.resolve(this._bodyBlob)
+ } else if (this._bodyArrayBuffer) {
+ return Promise.resolve(new Blob([this._bodyArrayBuffer]))
+ } else if (this._bodyFormData) {
+ throw new Error('could not read FormData body as blob')
+ } else {
+ return Promise.resolve(new Blob([this._bodyText]))
+ }
+ };
+
+ this.arrayBuffer = function() {
+ if (this._bodyArrayBuffer) {
+ return consumed(this) || Promise.resolve(this._bodyArrayBuffer)
+ } else {
+ return this.blob().then(readBlobAsArrayBuffer)
+ }
+ };
+ }
+
+ this.text = function() {
+ var rejected = consumed(this);
+ if (rejected) {
+ return rejected
+ }
+
+ if (this._bodyBlob) {
+ return readBlobAsText(this._bodyBlob)
+ } else if (this._bodyArrayBuffer) {
+ return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
+ } else if (this._bodyFormData) {
+ throw new Error('could not read FormData body as text')
+ } else {
+ return Promise.resolve(this._bodyText)
+ }
+ };
+
+ if (support.formData) {
+ this.formData = function() {
+ return this.text().then(decode)
+ };
+ }
+
+ this.json = function() {
+ return this.text().then(JSON.parse)
+ };
+
+ return this
+ }
+
+ // HTTP methods whose capitalization should be normalized
+ var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'];
+
+ function normalizeMethod(method) {
+ var upcased = method.toUpperCase();
+ return methods.indexOf(upcased) > -1 ? upcased : method
+ }
+
+ function Request(input, options) {
+ options = options || {};
+ var body = options.body;
+
+ if (input instanceof Request) {
+ if (input.bodyUsed) {
+ throw new TypeError('Already read')
+ }
+ this.url = input.url;
+ this.credentials = input.credentials;
+ if (!options.headers) {
+ this.headers = new Headers(input.headers);
+ }
+ this.method = input.method;
+ this.mode = input.mode;
+ this.signal = input.signal;
+ if (!body && input._bodyInit != null) {
+ body = input._bodyInit;
+ input.bodyUsed = true;
+ }
+ } else {
+ this.url = String(input);
+ }
+
+ this.credentials = options.credentials || this.credentials || 'same-origin';
+ if (options.headers || !this.headers) {
+ this.headers = new Headers(options.headers);
+ }
+ this.method = normalizeMethod(options.method || this.method || 'GET');
+ this.mode = options.mode || this.mode || null;
+ this.signal = options.signal || this.signal;
+ this.referrer = null;
+
+ if ((this.method === 'GET' || this.method === 'HEAD') && body) {
+ throw new TypeError('Body not allowed for GET or HEAD requests')
+ }
+ this._initBody(body);
+ }
+
+ Request.prototype.clone = function() {
+ return new Request(this, {body: this._bodyInit})
+ };
+
+ function decode(body) {
+ var form = new FormData();
+ body
+ .trim()
+ .split('&')
+ .forEach(function(bytes) {
+ if (bytes) {
+ var split = bytes.split('=');
+ var name = split.shift().replace(/\+/g, ' ');
+ var value = split.join('=').replace(/\+/g, ' ');
+ form.append(decodeURIComponent(name), decodeURIComponent(value));
+ }
+ });
+ return form
+ }
+
+ function parseHeaders(rawHeaders) {
+ var headers = new Headers();
+ // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
+ // https://tools.ietf.org/html/rfc7230#section-3.2
+ var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' ');
+ preProcessedHeaders.split(/\r?\n/).forEach(function(line) {
+ var parts = line.split(':');
+ var key = parts.shift().trim();
+ if (key) {
+ var value = parts.join(':').trim();
+ headers.append(key, value);
+ }
+ });
+ return headers
+ }
+
+ Body.call(Request.prototype);
+
+ function Response(bodyInit, options) {
+ if (!options) {
+ options = {};
+ }
+
+ this.type = 'default';
+ this.status = options.status === undefined ? 200 : options.status;
+ this.ok = this.status >= 200 && this.status < 300;
+ this.statusText = 'statusText' in options ? options.statusText : 'OK';
+ this.headers = new Headers(options.headers);
+ this.url = options.url || '';
+ this._initBody(bodyInit);
+ }
+
+ Body.call(Response.prototype);
+
+ Response.prototype.clone = function() {
+ return new Response(this._bodyInit, {
+ status: this.status,
+ statusText: this.statusText,
+ headers: new Headers(this.headers),
+ url: this.url
+ })
+ };
+
+ Response.error = function() {
+ var response = new Response(null, {status: 0, statusText: ''});
+ response.type = 'error';
+ return response
+ };
+
+ var redirectStatuses = [301, 302, 303, 307, 308];
+
+ Response.redirect = function(url, status) {
+ if (redirectStatuses.indexOf(status) === -1) {
+ throw new RangeError('Invalid status code')
+ }
+
+ return new Response(null, {status: status, headers: {location: url}})
+ };
+
+ exports.DOMException = self.DOMException;
+ try {
+ new exports.DOMException();
+ } catch (err) {
+ exports.DOMException = function(message, name) {
+ this.message = message;
+ this.name = name;
+ var error = Error(message);
+ this.stack = error.stack;
+ };
+ exports.DOMException.prototype = Object.create(Error.prototype);
+ exports.DOMException.prototype.constructor = exports.DOMException;
+ }
+
+ function fetch(input, init) {
+ return new Promise(function(resolve, reject) {
+ var request = new Request(input, init);
+
+ if (request.signal && request.signal.aborted) {
+ return reject(new exports.DOMException('Aborted', 'AbortError'))
+ }
+
+ var xhr = new XMLHttpRequest();
+
+ function abortXhr() {
+ xhr.abort();
+ }
+
+ xhr.onload = function() {
+ var options = {
+ status: xhr.status,
+ statusText: xhr.statusText,
+ headers: parseHeaders(xhr.getAllResponseHeaders() || '')
+ };
+ options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
+ var body = 'response' in xhr ? xhr.response : xhr.responseText;
+ resolve(new Response(body, options));
+ };
+
+ xhr.onerror = function() {
+ reject(new TypeError('Network request failed'));
+ };
+
+ xhr.ontimeout = function() {
+ reject(new TypeError('Network request failed'));
+ };
+
+ xhr.onabort = function() {
+ reject(new exports.DOMException('Aborted', 'AbortError'));
+ };
+
+ xhr.open(request.method, request.url, true);
+
+ if (request.credentials === 'include') {
+ xhr.withCredentials = true;
+ } else if (request.credentials === 'omit') {
+ xhr.withCredentials = false;
+ }
+
+ if ('responseType' in xhr && support.blob) {
+ xhr.responseType = 'blob';
+ }
+
+ request.headers.forEach(function(value, name) {
+ xhr.setRequestHeader(name, value);
+ });
+
+ if (request.signal) {
+ request.signal.addEventListener('abort', abortXhr);
+
+ xhr.onreadystatechange = function() {
+ // DONE (success or failure)
+ if (xhr.readyState === 4) {
+ request.signal.removeEventListener('abort', abortXhr);
+ }
+ };
+ }
+
+ xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit);
+ })
+ }
+
+ fetch.polyfill = true;
+
+ if (!self.fetch) {
+ self.fetch = fetch;
+ self.Headers = Headers;
+ self.Request = Request;
+ self.Response = Response;
+ }
+
+ exports.Headers = Headers;
+ exports.Request = Request;
+ exports.Response = Response;
+ exports.fetch = fetch;
+
+ Object.defineProperty(exports, '__esModule', { value: true });
+
+})));
diff --git a/core/vendor/assets/javascripts/jquery.payment.js b/core/vendor/assets/javascripts/jquery.payment.js
new file mode 100644
index 00000000000..dcf829fbc8c
--- /dev/null
+++ b/core/vendor/assets/javascripts/jquery.payment.js
@@ -0,0 +1,652 @@
+// Generated by CoffeeScript 1.7.1
+(function() {
+ var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlashAndSpace, hasTextSelected, luhnCheck, reFormatCVC, reFormatCardNumber, reFormatExpiry, reFormatNumeric, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, safeVal, setCardType,
+ __slice = [].slice,
+ __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+
+ $ = window.jQuery || window.Zepto || window.$;
+
+ $.payment = {};
+
+ $.payment.fn = {};
+
+ $.fn.payment = function() {
+ var args, method;
+ method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+ return $.payment.fn[method].apply(this, args);
+ };
+
+ defaultFormat = /(\d{1,4})/g;
+
+ $.payment.cards = cards = [
+ {
+ type: 'maestro',
+ patterns: [5018, 502, 503, 506, 56, 58, 639, 6220, 67],
+ format: defaultFormat,
+ length: [12, 13, 14, 15, 16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'forbrugsforeningen',
+ patterns: [600],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'dankort',
+ patterns: [5019],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'visa',
+ patterns: [4],
+ format: defaultFormat,
+ length: [13, 16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'mastercard',
+ patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'amex',
+ patterns: [34, 37],
+ format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
+ length: [15],
+ cvcLength: [3, 4],
+ luhn: true
+ }, {
+ type: 'dinersclub',
+ patterns: [30, 36, 38, 39],
+ format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/,
+ length: [14],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'discover',
+ patterns: [60, 64, 65, 622],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'unionpay',
+ patterns: [62, 88],
+ format: defaultFormat,
+ length: [16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: false
+ }, {
+ type: 'jcb',
+ patterns: [35],
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }
+ ];
+
+ cardFromNumber = function(num) {
+ var card, p, pattern, _i, _j, _len, _len1, _ref;
+ num = (num + '').replace(/\D/g, '');
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ _ref = card.patterns;
+ for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) {
+ pattern = _ref[_j];
+ p = pattern + '';
+ if (num.substr(0, p.length) === p) {
+ return card;
+ }
+ }
+ }
+ };
+
+ cardFromType = function(type) {
+ var card, _i, _len;
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ if (card.type === type) {
+ return card;
+ }
+ }
+ };
+
+ luhnCheck = function(num) {
+ var digit, digits, odd, sum, _i, _len;
+ odd = true;
+ sum = 0;
+ digits = (num + '').split('').reverse();
+ for (_i = 0, _len = digits.length; _i < _len; _i++) {
+ digit = digits[_i];
+ digit = parseInt(digit, 10);
+ if ((odd = !odd)) {
+ digit *= 2;
+ }
+ if (digit > 9) {
+ digit -= 9;
+ }
+ sum += digit;
+ }
+ return sum % 10 === 0;
+ };
+
+ hasTextSelected = function($target) {
+ var _ref;
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) {
+ return true;
+ }
+ if ((typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? _ref.createRange : void 0 : void 0) != null) {
+ if (document.selection.createRange().text) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ safeVal = function(value, $target) {
+ var currPair, cursor, digit, error, last, prevPair;
+ try {
+ cursor = $target.prop('selectionStart');
+ } catch (_error) {
+ error = _error;
+ cursor = null;
+ }
+ last = $target.val();
+ $target.val(value);
+ if (cursor !== null && $target.is(":focus")) {
+ if (cursor === last.length) {
+ cursor = value.length;
+ }
+ if (last !== value) {
+ prevPair = last.slice(cursor - 1, +cursor + 1 || 9e9);
+ currPair = value.slice(cursor - 1, +cursor + 1 || 9e9);
+ digit = value[cursor];
+ if (/\d/.test(digit) && prevPair === ("" + digit + " ") && currPair === (" " + digit)) {
+ cursor = cursor + 1;
+ }
+ }
+ $target.prop('selectionStart', cursor);
+ return $target.prop('selectionEnd', cursor);
+ }
+ };
+
+ replaceFullWidthChars = function(str) {
+ var chars, chr, fullWidth, halfWidth, idx, value, _i, _len;
+ if (str == null) {
+ str = '';
+ }
+ fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19';
+ halfWidth = '0123456789';
+ value = '';
+ chars = str.split('');
+ for (_i = 0, _len = chars.length; _i < _len; _i++) {
+ chr = chars[_i];
+ idx = fullWidth.indexOf(chr);
+ if (idx > -1) {
+ chr = halfWidth[idx];
+ }
+ value += chr;
+ }
+ return value;
+ };
+
+ reFormatNumeric = function(e) {
+ var $target;
+ $target = $(e.currentTarget);
+ return setTimeout(function() {
+ var value;
+ value = $target.val();
+ value = replaceFullWidthChars(value);
+ value = value.replace(/\D/g, '');
+ return safeVal(value, $target);
+ });
+ };
+
+ reFormatCardNumber = function(e) {
+ var $target;
+ $target = $(e.currentTarget);
+ return setTimeout(function() {
+ var value;
+ value = $target.val();
+ value = replaceFullWidthChars(value);
+ value = $.payment.formatCardNumber(value);
+ return safeVal(value, $target);
+ });
+ };
+
+ formatCardNumber = function(e) {
+ var $target, card, digit, length, re, upperLength, value;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ value = $target.val();
+ card = cardFromNumber(value + digit);
+ length = (value.replace(/\D/g, '') + digit).length;
+ upperLength = 16;
+ if (card) {
+ upperLength = card.length[card.length.length - 1];
+ }
+ if (length >= upperLength) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (card && card.type === 'amex') {
+ re = /^(\d{4}|\d{4}\s\d{6})$/;
+ } else {
+ re = /(?:^|\s)(\d{4})$/;
+ }
+ if (re.test(value)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value + ' ' + digit);
+ });
+ } else if (re.test(value + digit)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value + digit + ' ');
+ });
+ }
+ };
+
+ formatBackCardNumber = function(e) {
+ var $target, value;
+ $target = $(e.currentTarget);
+ value = $target.val();
+ if (e.which !== 8) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (/\d\s$/.test(value)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value.replace(/\d\s$/, ''));
+ });
+ } else if (/\s\d?$/.test(value)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value.replace(/\d$/, ''));
+ });
+ }
+ };
+
+ reFormatExpiry = function(e) {
+ var $target;
+ $target = $(e.currentTarget);
+ return setTimeout(function() {
+ var value;
+ value = $target.val();
+ value = replaceFullWidthChars(value);
+ value = $.payment.formatExpiry(value);
+ return safeVal(value, $target);
+ });
+ };
+
+ formatExpiry = function(e) {
+ var $target, digit, val;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val() + digit;
+ if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val("0" + val + " / ");
+ });
+ } else if (/^\d\d$/.test(val)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ var m1, m2;
+ m1 = parseInt(val[0], 10);
+ m2 = parseInt(val[1], 10);
+ if (m2 > 2 && m1 !== 0) {
+ return $target.val("0" + m1 + " / " + m2);
+ } else {
+ return $target.val("" + val + " / ");
+ }
+ });
+ }
+ };
+
+ formatForwardExpiry = function(e) {
+ var $target, digit, val;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val();
+ if (/^\d\d$/.test(val)) {
+ return $target.val("" + val + " / ");
+ }
+ };
+
+ formatForwardSlashAndSpace = function(e) {
+ var $target, val, which;
+ which = String.fromCharCode(e.which);
+ if (!(which === '/' || which === ' ')) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val();
+ if (/^\d$/.test(val) && val !== '0') {
+ return $target.val("0" + val + " / ");
+ }
+ };
+
+ formatBackExpiry = function(e) {
+ var $target, value;
+ $target = $(e.currentTarget);
+ value = $target.val();
+ if (e.which !== 8) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (/\d\s\/\s$/.test(value)) {
+ e.preventDefault();
+ return setTimeout(function() {
+ return $target.val(value.replace(/\d\s\/\s$/, ''));
+ });
+ }
+ };
+
+ reFormatCVC = function(e) {
+ var $target;
+ $target = $(e.currentTarget);
+ return setTimeout(function() {
+ var value;
+ value = $target.val();
+ value = replaceFullWidthChars(value);
+ value = value.replace(/\D/g, '').slice(0, 4);
+ return safeVal(value, $target);
+ });
+ };
+
+ restrictNumeric = function(e) {
+ var input;
+ if (e.metaKey || e.ctrlKey) {
+ return true;
+ }
+ if (e.which === 32) {
+ return false;
+ }
+ if (e.which === 0) {
+ return true;
+ }
+ if (e.which < 33) {
+ return true;
+ }
+ input = String.fromCharCode(e.which);
+ return !!/[\d\s]/.test(input);
+ };
+
+ restrictCardNumber = function(e) {
+ var $target, card, digit, value;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ value = ($target.val() + digit).replace(/\D/g, '');
+ card = cardFromNumber(value);
+ if (card) {
+ return value.length <= card.length[card.length.length - 1];
+ } else {
+ return value.length <= 16;
+ }
+ };
+
+ restrictExpiry = function(e) {
+ var $target, digit, value;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ value = $target.val() + digit;
+ value = value.replace(/\D/g, '');
+ if (value.length > 6) {
+ return false;
+ }
+ };
+
+ restrictCVC = function(e) {
+ var $target, digit, val;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ val = $target.val() + digit;
+ return val.length <= 4;
+ };
+
+ setCardType = function(e) {
+ var $target, allTypes, card, cardType, val;
+ $target = $(e.currentTarget);
+ val = $target.val();
+ cardType = $.payment.cardType(val) || 'unknown';
+ if (!$target.hasClass(cardType)) {
+ allTypes = (function() {
+ var _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ _results.push(card.type);
+ }
+ return _results;
+ })();
+ $target.removeClass('unknown');
+ $target.removeClass(allTypes.join(' '));
+ $target.addClass(cardType);
+ $target.toggleClass('identified', cardType !== 'unknown');
+ return $target.trigger('payment.cardType', cardType);
+ }
+ };
+
+ $.payment.fn.formatCardCVC = function() {
+ this.on('keypress', restrictNumeric);
+ this.on('keypress', restrictCVC);
+ this.on('paste', reFormatCVC);
+ this.on('change', reFormatCVC);
+ this.on('input', reFormatCVC);
+ return this;
+ };
+
+ $.payment.fn.formatCardExpiry = function() {
+ this.on('keypress', restrictNumeric);
+ this.on('keypress', restrictExpiry);
+ this.on('keypress', formatExpiry);
+ this.on('keypress', formatForwardSlashAndSpace);
+ this.on('keypress', formatForwardExpiry);
+ this.on('keydown', formatBackExpiry);
+ this.on('change', reFormatExpiry);
+ this.on('input', reFormatExpiry);
+ return this;
+ };
+
+ $.payment.fn.formatCardNumber = function() {
+ this.on('keypress', restrictNumeric);
+ this.on('keypress', restrictCardNumber);
+ this.on('keypress', formatCardNumber);
+ this.on('keydown', formatBackCardNumber);
+ this.on('keyup', setCardType);
+ this.on('paste', reFormatCardNumber);
+ this.on('change', reFormatCardNumber);
+ this.on('input', reFormatCardNumber);
+ this.on('input', setCardType);
+ return this;
+ };
+
+ $.payment.fn.restrictNumeric = function() {
+ this.on('keypress', restrictNumeric);
+ this.on('paste', reFormatNumeric);
+ this.on('change', reFormatNumeric);
+ this.on('input', reFormatNumeric);
+ return this;
+ };
+
+ $.payment.fn.cardExpiryVal = function() {
+ return $.payment.cardExpiryVal($(this).val());
+ };
+
+ $.payment.cardExpiryVal = function(value) {
+ var month, prefix, year, _ref;
+ _ref = value.split(/[\s\/]+/, 2), month = _ref[0], year = _ref[1];
+ if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) {
+ prefix = (new Date).getFullYear();
+ prefix = prefix.toString().slice(0, 2);
+ year = prefix + year;
+ }
+ month = parseInt(month, 10);
+ year = parseInt(year, 10);
+ return {
+ month: month,
+ year: year
+ };
+ };
+
+ $.payment.validateCardNumber = function(num) {
+ var card, _ref;
+ num = (num + '').replace(/\s+|-/g, '');
+ if (!/^\d+$/.test(num)) {
+ return false;
+ }
+ card = cardFromNumber(num);
+ if (!card) {
+ return false;
+ }
+ return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num));
+ };
+
+ $.payment.validateCardExpiry = function(month, year) {
+ var currentTime, expiry, _ref;
+ if (typeof month === 'object' && 'month' in month) {
+ _ref = month, month = _ref.month, year = _ref.year;
+ }
+ if (!(month && year)) {
+ return false;
+ }
+ month = $.trim(month);
+ year = $.trim(year);
+ if (!/^\d+$/.test(month)) {
+ return false;
+ }
+ if (!/^\d+$/.test(year)) {
+ return false;
+ }
+ if (!((1 <= month && month <= 12))) {
+ return false;
+ }
+ if (year.length === 2) {
+ if (year < 70) {
+ year = "20" + year;
+ } else {
+ year = "19" + year;
+ }
+ }
+ if (year.length !== 4) {
+ return false;
+ }
+ expiry = new Date(year, month);
+ currentTime = new Date;
+ expiry.setMonth(expiry.getMonth() - 1);
+ expiry.setMonth(expiry.getMonth() + 1, 1);
+ return expiry > currentTime;
+ };
+
+ $.payment.validateCardCVC = function(cvc, type) {
+ var card, _ref;
+ cvc = $.trim(cvc);
+ if (!/^\d+$/.test(cvc)) {
+ return false;
+ }
+ card = cardFromType(type);
+ if (card != null) {
+ return _ref = cvc.length, __indexOf.call(card.cvcLength, _ref) >= 0;
+ } else {
+ return cvc.length >= 3 && cvc.length <= 4;
+ }
+ };
+
+ $.payment.cardType = function(num) {
+ var _ref;
+ if (!num) {
+ return null;
+ }
+ return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null;
+ };
+
+ $.payment.formatCardNumber = function(num) {
+ var card, groups, upperLength, _ref;
+ num = num.replace(/\D/g, '');
+ card = cardFromNumber(num);
+ if (!card) {
+ return num;
+ }
+ upperLength = card.length[card.length.length - 1];
+ num = num.slice(0, upperLength);
+ if (card.format.global) {
+ return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0;
+ } else {
+ groups = card.format.exec(num);
+ if (groups == null) {
+ return;
+ }
+ groups.shift();
+ groups = $.grep(groups, function(n) {
+ return n;
+ });
+ return groups.join(' ');
+ }
+ };
+
+ $.payment.formatExpiry = function(expiry) {
+ var mon, parts, sep, year;
+ parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/);
+ if (!parts) {
+ return '';
+ }
+ mon = parts[1] || '';
+ sep = parts[2] || '';
+ year = parts[3] || '';
+ if (year.length > 0) {
+ sep = ' / ';
+ } else if (sep === ' /') {
+ mon = mon.substring(0, 1);
+ sep = '';
+ } else if (mon.length === 2 || sep.length > 0) {
+ sep = ' / ';
+ } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) {
+ mon = "0" + mon;
+ sep = ' / ';
+ }
+ return mon + sep + year;
+ };
+
+}).call(this);
diff --git a/core/vendor/assets/javascripts/jsuri.js b/core/vendor/assets/javascripts/jsuri.js
new file mode 100755
index 00000000000..45031c9b99c
--- /dev/null
+++ b/core/vendor/assets/javascripts/jsuri.js
@@ -0,0 +1,458 @@
+/*!
+ * jsUri
+ * https://github.com/derek-watson/jsUri
+ *
+ * Copyright 2013, Derek Watson
+ * Released under the MIT license.
+ *
+ * Includes parseUri regular expressions
+ * http://blog.stevenlevithan.com/archives/parseuri
+ * Copyright 2007, Steven Levithan
+ * Released under the MIT license.
+ */
+
+ /*globals define, module */
+
+(function(global) {
+
+ var re = {
+ starts_with_slashes: /^\/+/,
+ ends_with_slashes: /\/+$/,
+ pluses: /\+/g,
+ query_separator: /[&;]/,
+ uri_parser: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*)(?::([^:@]*))?)?@)?(\[[0-9a-fA-F:.]+\]|[^:\/?#]*)(?::(\d+|(?=:)))?(:)?)((((?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
+ };
+
+ /**
+ * Define forEach for older js environments
+ * @see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach#Compatibility
+ */
+ if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function(callback, thisArg) {
+ var T, k;
+
+ if (this == null) {
+ throw new TypeError(' this is null or not defined');
+ }
+
+ var O = Object(this);
+ var len = O.length >>> 0;
+
+ if (typeof callback !== "function") {
+ throw new TypeError(callback + ' is not a function');
+ }
+
+ if (arguments.length > 1) {
+ T = thisArg;
+ }
+
+ k = 0;
+
+ while (k < len) {
+ var kValue;
+ if (k in O) {
+ kValue = O[k];
+ callback.call(T, kValue, k, O);
+ }
+ k++;
+ }
+ };
+ }
+
+ /**
+ * unescape a query param value
+ * @param {string} s encoded value
+ * @return {string} decoded value
+ */
+ function decode(s) {
+ if (s) {
+ s = s.toString().replace(re.pluses, '%20');
+ s = decodeURIComponent(s);
+ }
+ return s;
+ }
+
+ /**
+ * Breaks a uri string down into its individual parts
+ * @param {string} str uri
+ * @return {object} parts
+ */
+ function parseUri(str) {
+ var parser = re.uri_parser;
+ var parserKeys = ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "isColonUri", "relative", "path", "directory", "file", "query", "anchor"];
+ var m = parser.exec(str || '');
+ var parts = {};
+
+ parserKeys.forEach(function(key, i) {
+ parts[key] = m[i] || '';
+ });
+
+ return parts;
+ }
+
+ /**
+ * Breaks a query string down into an array of key/value pairs
+ * @param {string} str query
+ * @return {array} array of arrays (key/value pairs)
+ */
+ function parseQuery(str) {
+ var i, ps, p, n, k, v, l;
+ var pairs = [];
+
+ if (typeof(str) === 'undefined' || str === null || str === '') {
+ return pairs;
+ }
+
+ if (str.indexOf('?') === 0) {
+ str = str.substring(1);
+ }
+
+ ps = str.toString().split(re.query_separator);
+
+ for (i = 0, l = ps.length; i < l; i++) {
+ p = ps[i];
+ n = p.indexOf('=');
+
+ if (n !== 0) {
+ k = decode(p.substring(0, n));
+ v = decode(p.substring(n + 1));
+ pairs.push(n === -1 ? [p, null] : [k, v]);
+ }
+
+ }
+ return pairs;
+ }
+
+ /**
+ * Creates a new Uri object
+ * @constructor
+ * @param {string} str
+ */
+ function Uri(str) {
+ this.uriParts = parseUri(str);
+ this.queryPairs = parseQuery(this.uriParts.query);
+ this.hasAuthorityPrefixUserPref = null;
+ }
+
+ /**
+ * Define getter/setter methods
+ */
+ ['protocol', 'userInfo', 'host', 'port', 'path', 'anchor'].forEach(function(key) {
+ Uri.prototype[key] = function(val) {
+ if (typeof val !== 'undefined') {
+ this.uriParts[key] = val;
+ }
+ return this.uriParts[key];
+ };
+ });
+
+ /**
+ * if there is no protocol, the leading // can be enabled or disabled
+ * @param {Boolean} val
+ * @return {Boolean}
+ */
+ Uri.prototype.hasAuthorityPrefix = function(val) {
+ if (typeof val !== 'undefined') {
+ this.hasAuthorityPrefixUserPref = val;
+ }
+
+ if (this.hasAuthorityPrefixUserPref === null) {
+ return (this.uriParts.source.indexOf('//') !== -1);
+ } else {
+ return this.hasAuthorityPrefixUserPref;
+ }
+ };
+
+ Uri.prototype.isColonUri = function (val) {
+ if (typeof val !== 'undefined') {
+ this.uriParts.isColonUri = !!val;
+ } else {
+ return !!this.uriParts.isColonUri;
+ }
+ };
+
+ /**
+ * Serializes the internal state of the query pairs
+ * @param {string} [val] set a new query string
+ * @return {string} query string
+ */
+ Uri.prototype.query = function(val) {
+ var s = '', i, param, l;
+
+ if (typeof val !== 'undefined') {
+ this.queryPairs = parseQuery(val);
+ }
+
+ for (i = 0, l = this.queryPairs.length; i < l; i++) {
+ param = this.queryPairs[i];
+ if (s.length > 0) {
+ s += '&';
+ }
+ if (param[1] === null) {
+ s += param[0];
+ } else {
+ s += param[0];
+ s += '=';
+ if (typeof param[1] !== 'undefined') {
+ s += encodeURIComponent(param[1]);
+ }
+ }
+ }
+ return s.length > 0 ? '?' + s : s;
+ };
+
+ /**
+ * returns the first query param value found for the key
+ * @param {string} key query key
+ * @return {string} first value found for key
+ */
+ Uri.prototype.getQueryParamValue = function (key) {
+ var param, i, l;
+ for (i = 0, l = this.queryPairs.length; i < l; i++) {
+ param = this.queryPairs[i];
+ if (key === param[0]) {
+ return param[1];
+ }
+ }
+ };
+
+ /**
+ * returns an array of query param values for the key
+ * @param {string} key query key
+ * @return {array} array of values
+ */
+ Uri.prototype.getQueryParamValues = function (key) {
+ var arr = [], i, param, l;
+ for (i = 0, l = this.queryPairs.length; i < l; i++) {
+ param = this.queryPairs[i];
+ if (key === param[0]) {
+ arr.push(param[1]);
+ }
+ }
+ return arr;
+ };
+
+ /**
+ * removes query parameters
+ * @param {string} key remove values for key
+ * @param {val} [val] remove a specific value, otherwise removes all
+ * @return {Uri} returns self for fluent chaining
+ */
+ Uri.prototype.deleteQueryParam = function (key, val) {
+ var arr = [], i, param, keyMatchesFilter, valMatchesFilter, l;
+
+ for (i = 0, l = this.queryPairs.length; i < l; i++) {
+
+ param = this.queryPairs[i];
+ keyMatchesFilter = decode(param[0]) === decode(key);
+ valMatchesFilter = param[1] === val;
+
+ if ((arguments.length === 1 && !keyMatchesFilter) || (arguments.length === 2 && (!keyMatchesFilter || !valMatchesFilter))) {
+ arr.push(param);
+ }
+ }
+
+ this.queryPairs = arr;
+
+ return this;
+ };
+
+ /**
+ * adds a query parameter
+ * @param {string} key add values for key
+ * @param {string} val value to add
+ * @param {integer} [index] specific index to add the value at
+ * @return {Uri} returns self for fluent chaining
+ */
+ Uri.prototype.addQueryParam = function (key, val, index) {
+ if (arguments.length === 3 && index !== -1) {
+ index = Math.min(index, this.queryPairs.length);
+ this.queryPairs.splice(index, 0, [key, val]);
+ } else if (arguments.length > 0) {
+ this.queryPairs.push([key, val]);
+ }
+ return this;
+ };
+
+ /**
+ * test for the existence of a query parameter
+ * @param {string} key check values for key
+ * @return {Boolean} true if key exists, otherwise false
+ */
+ Uri.prototype.hasQueryParam = function (key) {
+ var i, len = this.queryPairs.length;
+ for (i = 0; i < len; i++) {
+ if (this.queryPairs[i][0] == key)
+ return true;
+ }
+ return false;
+ };
+
+ /**
+ * replaces query param values
+ * @param {string} key key to replace value for
+ * @param {string} newVal new value
+ * @param {string} [oldVal] replace only one specific value (otherwise replaces all)
+ * @return {Uri} returns self for fluent chaining
+ */
+ Uri.prototype.replaceQueryParam = function (key, newVal, oldVal) {
+ var index = -1, len = this.queryPairs.length, i, param;
+
+ if (arguments.length === 3) {
+ for (i = 0; i < len; i++) {
+ param = this.queryPairs[i];
+ if (decode(param[0]) === decode(key) && decodeURIComponent(param[1]) === decode(oldVal)) {
+ index = i;
+ break;
+ }
+ }
+ if (index >= 0) {
+ this.deleteQueryParam(key, decode(oldVal)).addQueryParam(key, newVal, index);
+ }
+ } else {
+ for (i = 0; i < len; i++) {
+ param = this.queryPairs[i];
+ if (decode(param[0]) === decode(key)) {
+ index = i;
+ break;
+ }
+ }
+ this.deleteQueryParam(key);
+ this.addQueryParam(key, newVal, index);
+ }
+ return this;
+ };
+
+ /**
+ * Define fluent setter methods (setProtocol, setHasAuthorityPrefix, etc)
+ */
+ ['protocol', 'hasAuthorityPrefix', 'isColonUri', 'userInfo', 'host', 'port', 'path', 'query', 'anchor'].forEach(function(key) {
+ var method = 'set' + key.charAt(0).toUpperCase() + key.slice(1);
+ Uri.prototype[method] = function(val) {
+ this[key](val);
+ return this;
+ };
+ });
+
+ /**
+ * Scheme name, colon and doubleslash, as required
+ * @return {string} http:// or possibly just //
+ */
+ Uri.prototype.scheme = function() {
+ var s = '';
+
+ if (this.protocol()) {
+ s += this.protocol();
+ if (this.protocol().indexOf(':') !== this.protocol().length - 1) {
+ s += ':';
+ }
+ s += '//';
+ } else {
+ if (this.hasAuthorityPrefix() && this.host()) {
+ s += '//';
+ }
+ }
+
+ return s;
+ };
+
+ /**
+ * Same as Mozilla nsIURI.prePath
+ * @return {string} scheme://user:password@host:port
+ * @see https://developer.mozilla.org/en/nsIURI
+ */
+ Uri.prototype.origin = function() {
+ var s = this.scheme();
+
+ if (this.userInfo() && this.host()) {
+ s += this.userInfo();
+ if (this.userInfo().indexOf('@') !== this.userInfo().length - 1) {
+ s += '@';
+ }
+ }
+
+ if (this.host()) {
+ s += this.host();
+ if (this.port() || (this.path() && this.path().substr(0, 1).match(/[0-9]/))) {
+ s += ':' + this.port();
+ }
+ }
+
+ return s;
+ };
+
+ /**
+ * Adds a trailing slash to the path
+ */
+ Uri.prototype.addTrailingSlash = function() {
+ var path = this.path() || '';
+
+ if (path.substr(-1) !== '/') {
+ this.path(path + '/');
+ }
+
+ return this;
+ };
+
+ /**
+ * Serializes the internal state of the Uri object
+ * @return {string}
+ */
+ Uri.prototype.toString = function() {
+ var path, s = this.origin();
+
+ if (this.isColonUri()) {
+ if (this.path()) {
+ s += ':'+this.path();
+ }
+ } else if (this.path()) {
+ path = this.path();
+ if (!(re.ends_with_slashes.test(s) || re.starts_with_slashes.test(path))) {
+ s += '/';
+ } else {
+ if (s) {
+ s.replace(re.ends_with_slashes, '/');
+ }
+ path = path.replace(re.starts_with_slashes, '/');
+ }
+ s += path;
+ } else {
+ if (this.host() && (this.query().toString() || this.anchor())) {
+ s += '/';
+ }
+ }
+ if (this.query().toString()) {
+ s += this.query().toString();
+ }
+
+ if (this.anchor()) {
+ if (this.anchor().indexOf('#') !== 0) {
+ s += '#';
+ }
+ s += this.anchor();
+ }
+
+ return s;
+ };
+
+ /**
+ * Clone a Uri object
+ * @return {Uri} duplicate copy of the Uri
+ */
+ Uri.prototype.clone = function() {
+ return new Uri(this.toString());
+ };
+
+ /**
+ * export via AMD or CommonJS, otherwise leak a global
+ */
+ if (typeof define === 'function' && define.amd) {
+ define(function() {
+ return Uri;
+ });
+ } else if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
+ module.exports = Uri;
+ } else {
+ global.Uri = Uri;
+ }
+}(this));
diff --git a/core/vendor/assets/javascripts/polyfill.min.js b/core/vendor/assets/javascripts/polyfill.min.js
new file mode 100644
index 00000000000..425c164d044
--- /dev/null
+++ b/core/vendor/assets/javascripts/polyfill.min.js
@@ -0,0 +1 @@
+!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n():"function"==typeof define&&define.amd?define(n):n()}(0,function(){"use strict";function e(e){var n=this.constructor;return this.then(function(t){return n.resolve(e()).then(function(){return t})},function(t){return n.resolve(e()).then(function(){return n.reject(t)})})}function n(){}function t(e){if(!(this instanceof t))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],u(e,this)}function o(e,n){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,t._immediateFn(function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null!==t){var o;try{o=t(e._value)}catch(f){return void i(n.promise,f)}r(n.promise,o)}else(1===e._state?r:i)(n.promise,e._value)})):e._deferreds.push(n)}function r(e,n){try{if(n===e)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var o=n.then;if(n instanceof t)return e._state=3,e._value=n,void f(e);if("function"==typeof o)return void u(function(e,n){return function(){e.apply(n,arguments)}}(o,n),e)}e._state=1,e._value=n,f(e)}catch(r){i(e,r)}}function i(e,n){e._state=2,e._value=n,f(e)}function f(e){2===e._state&&0===e._deferreds.length&&t._immediateFn(function(){e._handled||t._unhandledRejectionFn(e._value)});for(var n=0,r=e._deferreds.length;r>n;n++)o(e,e._deferreds[n]);e._deferreds=null}function u(e,n){var t=!1;try{e(function(e){t||(t=!0,r(n,e))},function(e){t||(t=!0,i(n,e))})}catch(o){if(t)return;t=!0,i(n,o)}}var c=setTimeout;t.prototype["catch"]=function(e){return this.then(null,e)},t.prototype.then=function(e,t){var r=new this.constructor(n);return o(this,new function(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}(e,t,r)),r},t.prototype["finally"]=e,t.all=function(e){return new t(function(n,t){function o(e,f){try{if(f&&("object"==typeof f||"function"==typeof f)){var u=f.then;if("function"==typeof u)return void u.call(f,function(n){o(e,n)},t)}r[e]=f,0==--i&&n(r)}catch(c){t(c)}}if(!e||"undefined"==typeof e.length)throw new TypeError("Promise.all accepts an array");var r=Array.prototype.slice.call(e);if(0===r.length)return n([]);for(var i=r.length,f=0;r.length>f;f++)o(f,r[f])})},t.resolve=function(e){return e&&"object"==typeof e&&e.constructor===t?e:new t(function(n){n(e)})},t.reject=function(e){return new t(function(n,t){t(e)})},t.race=function(e){return new t(function(n,t){for(var o=0,r=e.length;r>o;o++)e[o].then(n,t)})},t._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){c(e,0)},t._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var l=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"Promise"in l?l.Promise.prototype["finally"]||(l.Promise.prototype["finally"]=e):l.Promise=t});
diff --git a/db/.gitignore b/db/.gitignore
deleted file mode 100644
index d1b811b7de5..00000000000
--- a/db/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.sql
diff --git a/db/migrate/001_create_addresses.rb b/db/migrate/001_create_addresses.rb
deleted file mode 100644
index 0c33ca43447..00000000000
--- a/db/migrate/001_create_addresses.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class CreateAddresses < ActiveRecord::Migration
- def self.up
- create_table :addresses do |t|
- t.string :firstname
- t.string :lastname
- t.string :address1
- t.string :address2
- t.string :city
- t.integer :state_id
- t.string :zipcode
- t.integer :country_id
- t.string :phone
- t.timestamps
- end
- end
-
- def self.down
- drop_table :addresses
- end
-end
\ No newline at end of file
diff --git a/db/migrate/002_create_cart_items.rb b/db/migrate/002_create_cart_items.rb
deleted file mode 100644
index 25a5127e9db..00000000000
--- a/db/migrate/002_create_cart_items.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class CreateCartItems < ActiveRecord::Migration
- def self.up
- create_table "cart_items", :force => true do |t|
- t.column "cart_id", :integer, :null => false
- t.column "variant_id", :integer, :null => false
- t.column "quantity", :integer, :null => false
- end
- end
-
- def self.down
- drop_table "cart_items"
- end
-end
\ No newline at end of file
diff --git a/db/migrate/003_create_carts.rb b/db/migrate/003_create_carts.rb
deleted file mode 100644
index efcccc637ab..00000000000
--- a/db/migrate/003_create_carts.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class CreateCarts < ActiveRecord::Migration
- def self.up
- create_table "carts", :force => true do |t|
- t.column "created_at", :datetime
- t.column "updated_at", :datetime
- end
- end
-
- def self.down
- drop_table "carts"
- end
-end
\ No newline at end of file
diff --git a/db/migrate/004_create_categories.rb b/db/migrate/004_create_categories.rb
deleted file mode 100644
index c43e65f9f43..00000000000
--- a/db/migrate/004_create_categories.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class CreateCategories < ActiveRecord::Migration
- def self.up
- create_table "categories", :force => true do |t|
- t.column "name", :string, :default => "", :null => false
- t.column "parent_id", :integer
- t.column "position", :integer, :null => false
- t.column :created_at, :datetime
- t.column :updated_at, :datetime
- end
- end
-
- def self.down
- drop_table "categories"
- end
-end
\ No newline at end of file
diff --git a/db/migrate/005_create_categories_products.rb b/db/migrate/005_create_categories_products.rb
deleted file mode 100644
index b2f044cc7b8..00000000000
--- a/db/migrate/005_create_categories_products.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class CreateCategoriesProducts < ActiveRecord::Migration
- def self.up
- #create_table "categories_products", :id => false, :force => true do |t|
- # t.column "category_id", :integer
- # t.column "product_id", :integer
- #end
- end
-
- def self.down
- #drop_table "categories_products"
- end
-end
\ No newline at end of file
diff --git a/db/migrate/006_create_line_items.rb b/db/migrate/006_create_line_items.rb
deleted file mode 100644
index 6c41243d153..00000000000
--- a/db/migrate/006_create_line_items.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class CreateLineItems < ActiveRecord::Migration
- def self.up
- create_table "line_items", :force => true do |t|
- t.integer :order_id
- t.integer :variant_id
- t.integer :quantity, :null => false
- t.decimal :price, :precision => 8, :scale => 2, :null => false
- t.timestamps
- end
- end
-
- def self.down
- drop_table "line_items"
- end
-end
\ No newline at end of file
diff --git a/db/migrate/007_create_option_values.rb b/db/migrate/007_create_option_values.rb
deleted file mode 100644
index 8199d687dcf..00000000000
--- a/db/migrate/007_create_option_values.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class CreateOptionValues < ActiveRecord::Migration
- def self.up
- create_table :option_values do |t|
- t.integer :option_type_id
- t.string :name
- t.integer :position
- t.string :presentation
- t.timestamps
- end
- end
-
- def self.down
- drop_table :option_values
- end
-end
\ No newline at end of file
diff --git a/db/migrate/008_create_orders.rb b/db/migrate/008_create_orders.rb
deleted file mode 100644
index 6f2eb25a820..00000000000
--- a/db/migrate/008_create_orders.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class CreateOrders < ActiveRecord::Migration
- def self.up
- create_table :orders, :force => true do |t|
- t.integer :user_id
- t.string :number, :limit => 15
- t.integer :status
- t.integer :ship_method
- t.decimal :ship_amount, :precision => 8, :scale => 2, :default => 0.0, :null => false
- t.decimal :tax_amount, :precision => 8, :scale => 2, :default => 0.0, :null => false
- t.decimal :item_total, :precision => 8, :scale => 2, :default => 0.0, :null => false
- t.decimal :total, :precision => 8, :scale => 2, :default => 0.0, :null => false
- t.string :ip_address
- t.text :special_instructions
- t.integer :ship_address_id
- t.integer :bill_address_id
- t.timestamps
- end
- end
-
- def self.down
- drop_table :orders
- end
-end
\ No newline at end of file
diff --git a/db/migrate/009_create_products.rb b/db/migrate/009_create_products.rb
deleted file mode 100644
index 83a54e3fc73..00000000000
--- a/db/migrate/009_create_products.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-class CreateProducts < ActiveRecord::Migration
- def self.up
- create_table :products do |t|
- t.string :name, :default => "", :null => false
- t.string :description
- t.decimal :price, :precision => 8, :scale => 2, :null => false
- t.integer :category_id
- t.integer :viewable_id
- t.timestamps
- end
- end
-
- def self.down
- drop_table "products"
- end
-end
\ No newline at end of file
diff --git a/db/migrate/010_create_txns.rb b/db/migrate/010_create_txns.rb
deleted file mode 100644
index 5c22e38e812..00000000000
--- a/db/migrate/010_create_txns.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-class CreateTxns < ActiveRecord::Migration
- def self.up
- create_table "txns", :force => true do |t|
- t.column "credit_card_id", :integer
- t.column "amount", :decimal, :precision => 8, :scale => 2, :default => 0.0, :null => false
- t.column "txn_type", :string
- t.column "response_code", :string
- t.column "avs_response", :text
- t.column "cvv_response", :text
- t.column "created_at", :datetime
- t.column "updated_at", :datetime
- end
- end
-
- def self.down
- drop_table "txns"
- end
-end
\ No newline at end of file
diff --git a/db/migrate/011_create_variants.rb b/db/migrate/011_create_variants.rb
deleted file mode 100644
index 8b594852c36..00000000000
--- a/db/migrate/011_create_variants.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class CreateVariants < ActiveRecord::Migration
- def self.up
- create_table :variants do |t|
- t.integer :product_id
- t.string :sku, :default => "", :null => false
- end
- end
-
- def self.down
- drop_table :variants
- end
-end
\ No newline at end of file
diff --git a/db/migrate/012_create_tax_treatments.rb b/db/migrate/012_create_tax_treatments.rb
deleted file mode 100644
index f8e67b91305..00000000000
--- a/db/migrate/012_create_tax_treatments.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class CreateTaxTreatments < ActiveRecord::Migration
- def self.up
- create_table :tax_treatments do |t|
- t.column :name, :string
- end
-
- create_table :products_tax_treatments, :id => false do |t|
- t.column :product_id, :integer
- t.column :tax_treatment_id, :integer
- end
-
- create_table :categories_tax_treatments, :id => false do |t|
- t.column :category_id, :integer
- t.column :tax_treatment_id, :integer
- end
- end
-
- def self.down
- drop_table :tax_treatments
- drop_table :categories_tax_treatments
- drop_table :products_tax_treatments
- end
-end
diff --git a/db/migrate/013_create_countries.rb b/db/migrate/013_create_countries.rb
deleted file mode 100644
index 7b8f8fa9046..00000000000
--- a/db/migrate/013_create_countries.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class CreateCountries< ActiveRecord::Migration
- def self.up
- create_table :countries do |t|
- t.column :name, :string
- end
- end
-
- def self.down
- drop_table :countries
- end
-end
\ No newline at end of file
diff --git a/db/migrate/014_create_states.rb b/db/migrate/014_create_states.rb
deleted file mode 100644
index 606d1e31411..00000000000
--- a/db/migrate/014_create_states.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class CreateStates< ActiveRecord::Migration
- def self.up
- create_table :states do |t|
- t.column :name, :string
- t.column :abbr, :string
- t.column :country_id, :integer
- end
- end
-
- def self.down
- drop_table :states
- end
-end
\ No newline at end of file
diff --git a/db/migrate/015_create_option_types.rb b/db/migrate/015_create_option_types.rb
deleted file mode 100644
index 2cb1422cf76..00000000000
--- a/db/migrate/015_create_option_types.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class CreateOptionTypes < ActiveRecord::Migration
- def self.up
- create_table :option_types do |t|
- t.string :name, :limit => 100
- t.string :presentation, :limit => 100
- t.timestamps
- end
- end
-
- def self.down
- drop_table :option_types
- end
-end
\ No newline at end of file
diff --git a/db/migrate/016_create_product_option_types.rb b/db/migrate/016_create_product_option_types.rb
deleted file mode 100644
index 6d8bb808016..00000000000
--- a/db/migrate/016_create_product_option_types.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class CreateProductOptionTypes < ActiveRecord::Migration
- def self.up
- create_table :product_option_types do |t|
- t.integer :product_id
- t.integer :option_type_id
- t.integer :position
- t.timestamps
- end
- end
-
- def self.down
- drop_table :product_option_types
- end
-end
\ No newline at end of file
diff --git a/db/migrate/017_create_option_values_variants.rb b/db/migrate/017_create_option_values_variants.rb
deleted file mode 100644
index fcde51cdecd..00000000000
--- a/db/migrate/017_create_option_values_variants.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class CreateOptionValuesVariants < ActiveRecord::Migration
- def self.up
- create_table :option_values_variants, :id=>false do |t|
- t.integer :variant_id
- t.integer :option_value_id
- end
- end
-
- def self.down
- drop_table :option_values_variants
- end
-end
\ No newline at end of file
diff --git a/db/migrate/018_create_images.rb b/db/migrate/018_create_images.rb
deleted file mode 100644
index 306146c274b..00000000000
--- a/db/migrate/018_create_images.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class CreateImages < ActiveRecord::Migration
- def self.up
- create_table :images do |t|
- t.integer :viewable_id
- t.string :viewable_type
- t.integer :parent_id
- t.string :content_type
- t.string :filename
- t.integer :size
- t.integer :height
- t.integer :width
- t.string :thumbnail
- t.integer :position
- end
- end
-
- def self.down
- drop_table :images
- end
-end
\ No newline at end of file
diff --git a/db/migrate/019_create_credit_cards.rb b/db/migrate/019_create_credit_cards.rb
deleted file mode 100644
index ce4d9447f90..00000000000
--- a/db/migrate/019_create_credit_cards.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class CreateCreditCards < ActiveRecord::Migration
- def self.up
- create_table :credit_cards do |t|
- t.integer :order_id
- t.string :number # IMPORTANT: Should be encrypted with the private key stored on a separate physical machine
- t.string :verification_value # IMPORTANT: Should be encrypted with the private key stored on a separate physical machine
- t.string :cc_type
- t.string :month
- t.string :year
- t.string :display_number
- t.string :first_name
- t.string :last_name
- t.timestamps
- end
- end
-
- def self.down
- drop_table :credit_cards
- end
-end
\ No newline at end of file
diff --git a/db/migrate/020_create_order_operations.rb b/db/migrate/020_create_order_operations.rb
deleted file mode 100644
index c6372d4b226..00000000000
--- a/db/migrate/020_create_order_operations.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class CreateOrderOperations < ActiveRecord::Migration
- def self.up
- create_table :order_operations do |t|
- t.integer :order_id
- t.integer :user_id
- t.integer :operation_type
- t.timestamps
- end
- end
-
- def self.down
- drop_table :order_operations
- end
-end
\ No newline at end of file
diff --git a/db/migrate/021_create_inventory_units.rb b/db/migrate/021_create_inventory_units.rb
deleted file mode 100644
index fbf7786de47..00000000000
--- a/db/migrate/021_create_inventory_units.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class CreateInventoryUnits < ActiveRecord::Migration
- def self.up
- create_table :inventory_units do |t|
- t.integer :variant_id
- t.integer :order_id
- t.integer :status
- t.integer :lock_version, :default => 0
- t.timestamps
- end
- end
-
- def self.down
- drop_table :inventory_units
- end
-end
\ No newline at end of file
diff --git a/db/migrate/022_create_users.rb b/db/migrate/022_create_users.rb
deleted file mode 100644
index 7e80d057268..00000000000
--- a/db/migrate/022_create_users.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-class CreateUsers < ActiveRecord::Migration
- def self.up
- create_table :users, :force => true do |t|
- t.string :login
- t.string :email
- t.string :crypted_password, :limit => 40
- t.string :salt, :limit => 40
- t.string :remember_token
- t.string :remember_token_expires_at
- t.timestamps
- end
- end
-
- def self.down
- drop_table :users
- end
-end
\ No newline at end of file
diff --git a/db/migrate/023_create_roles.rb b/db/migrate/023_create_roles.rb
deleted file mode 100644
index 87d12aedd85..00000000000
--- a/db/migrate/023_create_roles.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class CreateRoles < ActiveRecord::Migration
- def self.up
- create_table :roles do |t|
- t.string :name
- end
-
- # generate the join table
- create_table :roles_users, :id => false do |t|
- t.integer :role_id
- t.integer :user_id
- end
- add_index "roles_users", "role_id"
- add_index "roles_users", "user_id"
- end
-
- def self.down
- drop_table :roles
- drop_table :roles_users
- end
-end
\ No newline at end of file
diff --git a/db/migrate/024_create_tags_and_taggings.rb b/db/migrate/024_create_tags_and_taggings.rb
deleted file mode 100644
index efdd7f82e9b..00000000000
--- a/db/migrate/024_create_tags_and_taggings.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-
-# A migration to add tables for Tag and Tagging. This file is automatically generated and added to your app if you run the tagging generator included with has_many_polymorphs.
-
-class CreateTagsAndTaggings < ActiveRecord::Migration
-
- # Add the new tables.
- def self.up
- create_table :tags do |t|
- t.column :name, :string, :null => false
- end
- add_index :tags, :name, :unique => true
-
- create_table :taggings do |t|
- t.column :tag_id, :integer, :null => false
- t.column :taggable_id, :integer, :null => false
- t.column :taggable_type, :string, :null => false
- # t.column :position, :integer # Uncomment this if you need to use acts_as_list.
- end
- add_index :taggings, [:tag_id, :taggable_id, :taggable_type], :unique => true
- end
-
- # Remove the tables.
- def self.down
- drop_table :tags
- drop_table :taggings
- end
-
-end
diff --git a/db/sample/addresses.yml b/db/sample/addresses.yml
deleted file mode 100644
index a2b61b6cab1..00000000000
--- a/db/sample/addresses.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-frank_address:
- firstname: Frank
- lastname: Foo
- address1: 123 Foo St.
- address2: Apt. F
- city: Fooville
- state: new_york
- zipcode: 16804
- country: usa
- phone: 614-555-1212
-
-foo_street:
- id: 101
- firstname: joe
- lastname: blow
- address1: 123 Foo St.
- city: Fooville
- state: maryland
- zipcode: 14456
- country: usa
- phone: 555-555-1212
- created_at: <%= 3.days.ago.to_s :db %>
- updated_at: <%= 3.days.ago.to_s :db %>
-
\ No newline at end of file
diff --git a/db/sample/categories.yml b/db/sample/categories.yml
deleted file mode 100644
index e640500c0f0..00000000000
--- a/db/sample/categories.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-all:
- id: 1
- name: All
- position: 1
- created_at: <%= 2.days.ago.to_s :db %>
- updated_at: <%= 2.days.ago.to_s :db %>
-apparel:
- id: 2
- name: Apparel
- parent_id: 1
- position: 1
- created_at: <%= 2.days.ago.to_s :db %>
- updated_at: <%= 2.days.ago.to_s :db %>
-housewares:
- id: 3
- name: Housewares
- parent_id: 1
- position: 2
- created_at: <%= 2.days.ago.to_s :db %>
- updated_at: <%= 2.days.ago.to_s :db %>
-hats_and_bags:
- id: 4
- name: Hats and Bags
- parent_id: 1
- position: 3
- created_at: <%= 2.days.ago.to_s :db %>
- updated_at: <%= 2.days.ago.to_s :db %>
-stickers_etc:
- id: 5
- name: Stickers, Buttons & Magnets
- parent_id: 1
- position: 4
- created_at: <%= 2.days.ago.to_s :db %>
- updated_at: <%= 2.days.ago.to_s :db %>
-
\ No newline at end of file
diff --git a/db/sample/countries.yml b/db/sample/countries.yml
deleted file mode 100644
index 66fb8c5b8b1..00000000000
--- a/db/sample/countries.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-usa:
- name: United States
-
\ No newline at end of file
diff --git a/db/sample/credit_cards.yml b/db/sample/credit_cards.yml
deleted file mode 100644
index c1a59c210d0..00000000000
--- a/db/sample/credit_cards.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-<% 1.upto(30) do |i| %>
-visa_<%= i %>:
- order: order_<%= i %>
- number: 4111111111111111
- verification_value: 123
- cc_type: visa
- month: 12
- year: 2011
- display_number: XXXX-XXXX-XXXX-1111
- first_name: Sean
- last_name: Schofield
-<% end %>
\ No newline at end of file
diff --git a/db/sample/images.yml b/db/sample/images.yml
deleted file mode 100644
index c47093acc3a..00000000000
--- a/db/sample/images.yml
+++ /dev/null
@@ -1,300 +0,0 @@
-img_tote:
- id: 1
- viewable: ror_tote
- viewable_type: Product
- content_type: image/jpg
- filename: ror_tote.jpeg
- position: 1
-img_tote_small:
- id: 101
- parent_id: 1
- content_type: image/jpeg
- filename: ror_tote_small.jpeg
- position: 1
-img_tote_product:
- id: 102
- parent_id: 1
- content_type: image/jpeg
- filename: ror_tote_product.jpeg
- position: 2
-img_tote_mini:
- id: 103
- parent_id: 1
- content_type: image/jpeg
- filename: ror_tote_mini.jpeg
- position: 3
-img_tote_back:
- id: 2
- viewable: ror_tote
- viewable_type: Product
- content_type: image/jpg
- filename: ror_tote_back.jpeg
- position: 2
-img_tote_back_small:
- id: 201
- parent_id: 2
- content_type: image/jpeg
- filename: ror_tote_back_small.jpeg
- position: 1
-img_tote_back_product:
- id: 202
- parent_id: 2
- content_type: image/jpeg
- filename: ror_tote_back_product.jpeg
- position: 2
-img_tote_back_mini:
- id: 203
- parent_id: 2
- content_type: image/jpeg
- filename: ror_tote_back_mini.jpeg
- position: 3
-img_bag:
- id: 3
- viewable: ror_bag
- viewable_type: Product
- content_type: image/jpg
- filename: ror_bag.jpeg
- position: 1
-img_bag_small:
- id: 301
- parent_id: 3
- content_type: image/jpeg
- filename: ror_bag_small.jpeg
- position: 1
-img_bag_product:
- id: 302
- parent_id: 3
- content_type: image/jpeg
- filename: ror_bag_product.jpeg
- position: 2
-img_bag_mini:
- id: 303
- parent_id: 3
- content_type: image/jpeg
- filename: ror_bag_mini.jpeg
- position: 3
-img_baseball:
- id: 4
- viewable: ror_baseball_jersey
- viewable_type: Product
- content_type: image/jpg
- filename: ror_baseball.jpeg
- position: 1
-img_baseball_small:
- id: 401
- parent_id: 4
- content_type: image/jpeg
- filename: ror_baseball_small.jpeg
- position: 1
-img_baseball_product:
- id: 402
- parent_id: 4
- content_type: image/jpeg
- filename: ror_baseball_product.jpeg
- position: 2
-img_baseball_mini:
- id: 403
- parent_id: 4
- content_type: image/jpeg
- filename: ror_baseball_mini.jpeg
- position: 3
-img_baseball_back:
- id: 5
- viewable: ror_baseball_jersey
- viewable_type: Product
- content_type: image/jpg
- filename: ror_baseball_back.jpeg
- position: 2
-img_baseball_back_small:
- id: 501
- parent_id: 5
- content_type: image/jpeg
- filename: ror_baseball_back_small.jpeg
- position: 1
-img_baseball_back_product:
- id: 502
- parent_id: 5
- content_type: image/jpeg
- filename: ror_baseball_back_product.jpeg
- position: 2
-img_baseball_back_mini:
- id: 503
- parent_id: 5
- content_type: image/jpeg
- filename: ror_baseball_back_mini.jpeg
- position: 3
-img_jr_spaghetti:
- id: 6
- viewable: ror_jr_spaghetti
- viewable_type: Product
- content_type: image/jpg
- filename: ror_jr_spaghetti.jpeg
- position: 1
-img_jr_spaghetti_small:
- id: 601
- parent_id: 6
- content_type: image/jpeg
- filename: ror_jr_spaghetti_small.jpeg
- position: 1
-img_jr_spaghetti_product:
- id: 602
- parent_id: 6
- content_type: image/jpeg
- filename: ror_jr_spaghetti_product.jpeg
- position: 2
-img_jr_spaghetti_mini:
- id: 603
- parent_id: 6
- content_type: image/jpeg
- filename: ror_jr_spaghetti_mini.jpeg
- position: 3
-img_mug:
- id: 7
- viewable: ror_mug
- viewable_type: Product
- content_type: image/jpg
- filename: ror_mug.jpeg
- position: 1
-img_mug_small:
- id: 701
- parent_id: 7
- content_type: image/jpeg
- filename: ror_mug_small.jpeg
- position: 1
-img_mug_product:
- id: 702
- parent_id: 7
- content_type: image/jpeg
- filename: ror_mug_product.jpeg
- position: 2
-img_mug_mini:
- id: 703
- parent_id: 7
- content_type: image/jpeg
- filename: ror_mug_mini.jpeg
- position: 3
-img_mug_back:
- id: 8
- viewable: ror_mug
- viewable_type: Product
- content_type: image/jpg
- filename: ror_mug_back.jpeg
- position: 2
-img_mug_back_small:
- id: 801
- parent_id: 8
- content_type: image/jpeg
- filename: ror_mug_back_small.jpeg
- position: 1
-img_mug_back_product:
- id: 802
- parent_id: 8
- content_type: image/jpeg
- filename: ror_mug_back_product.jpeg
- position: 2
-img_mug_back_mini:
- id: 803
- parent_id: 8
- content_type: image/jpeg
- filename: ror_mug_back_mini.jpeg
- position: 3
-img_ringer:
- id: 9
- viewable: ror_ringer
- viewable_type: Product
- content_type: image/jpg
- filename: ror_ringer.jpeg
- position: 1
-img_ringer_small:
- id: 901
- parent_id: 9
- content_type: image/jpeg
- filename: ror_ringer_small.jpeg
- position: 1
-img_ringer_product:
- id: 902
- parent_id: 9
- content_type: image/jpeg
- filename: ror_ringer_product.jpeg
- position: 2
-img_ringer_mini:
- id: 903
- parent_id: 9
- content_type: image/jpeg
- filename: ror_ringer_mini.jpeg
- position: 3
-img_ringer_back:
- id: 10
- viewable: ror_ringer
- viewable_type: Product
- content_type: image/jpg
- filename: ror_ringer_back.jpeg
- position: 2
-img_ringer_back_small:
- id: 1001
- parent_id: 10
- content_type: image/jpeg
- filename: ror_ringer_back_small.jpeg
- position: 1
-img_ringer_back_product:
- id: 1002
- parent_id: 10
- content_type: image/jpeg
- filename: ror_ringer_back_product.jpeg
- position: 2
-img_ringer_back_mini:
- id: 1003
- parent_id: 10
- content_type: image/jpeg
- filename: ror_ringer_back_mini.jpeg
- position: 3
-img_stein:
- id: 11
- viewable: ror_stein
- viewable_type: Product
- content_type: image/jpg
- filename: ror_stein.jpeg
- position: 1
-img_stein_small:
- id: 111
- parent_id: 11
- content_type: image/jpeg
- filename: ror_stein_small.jpeg
- position: 1
-img_stein_product:
- id: 112
- parent_id: 11
- content_type: image/jpeg
- filename: ror_stein_product.jpeg
- position: 2
-img_stein_mini:
- id: 113
- parent_id: 11
- content_type: image/jpeg
- filename: ror_stein_mini.jpeg
- position: 3
-img_stein_back:
- id: 12
- viewable: ror_stein
- viewable_type: Product
- content_type: image/jpg
- filename: ror_stein_back.jpeg
- position: 2
-img_stein_back_small:
- id: 121
- parent_id: 12
- content_type: image/jpeg
- filename: ror_stein_back_small.jpeg
- position: 1
-img_stein_back_product:
- id: 122
- parent_id: 12
- content_type: image/jpeg
- filename: ror_stein_back_product.jpeg
- position: 2
-img_stein_back_mini:
- id: 123
- parent_id: 12
- content_type: image/jpeg
- filename: ror_stein_back_mini.jpeg
- position: 3
\ No newline at end of file
diff --git a/db/sample/images/ror_bag.jpg b/db/sample/images/ror_bag.jpg
deleted file mode 100644
index a7a7523c41a..00000000000
Binary files a/db/sample/images/ror_bag.jpg and /dev/null differ
diff --git a/db/sample/images/ror_baseball_jersey.jpg b/db/sample/images/ror_baseball_jersey.jpg
deleted file mode 100644
index 90007d85edd..00000000000
Binary files a/db/sample/images/ror_baseball_jersey.jpg and /dev/null differ
diff --git a/db/sample/images/ror_jr_spaghetti.jpg b/db/sample/images/ror_jr_spaghetti.jpg
deleted file mode 100644
index a8355bc9457..00000000000
Binary files a/db/sample/images/ror_jr_spaghetti.jpg and /dev/null differ
diff --git a/db/sample/images/ror_mug.jpg b/db/sample/images/ror_mug.jpg
deleted file mode 100644
index 43ed3529f80..00000000000
Binary files a/db/sample/images/ror_mug.jpg and /dev/null differ
diff --git a/db/sample/images/ror_ringer_tshirt.jpg b/db/sample/images/ror_ringer_tshirt.jpg
deleted file mode 100644
index 0fe4486f66d..00000000000
Binary files a/db/sample/images/ror_ringer_tshirt.jpg and /dev/null differ
diff --git a/db/sample/images/ror_stein.jpg b/db/sample/images/ror_stein.jpg
deleted file mode 100644
index 65270d5a3cc..00000000000
Binary files a/db/sample/images/ror_stein.jpg and /dev/null differ
diff --git a/db/sample/images/ror_tote.jpg b/db/sample/images/ror_tote.jpg
deleted file mode 100644
index dfbb0347823..00000000000
Binary files a/db/sample/images/ror_tote.jpg and /dev/null differ
diff --git a/db/sample/inventory_units.yml b/db/sample/inventory_units.yml
deleted file mode 100644
index 1d92981e898..00000000000
--- a/db/sample/inventory_units.yml
+++ /dev/null
@@ -1,98 +0,0 @@
-<% 1.upto(75) do |i| %>
-small-red-baseball_<%= i %>:
- variant: small-red-baseball
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-small-blue-baseball_<%= i %>:
- variant: small-blue-baseball
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-small-green-baseball_<%= i %>:
- variant: small-green-baseball
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-med-red-baseball_<%= i %>:
- variant: med-red-baseball
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-med-blue-baseball_<%= i %>:
- variant: med-blue-baseball
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-med-green-baseball_<%= i %>:
- variant: med-green-baseball
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-large-red-baseball_<%= i %>:
- variant: large-red-baseball
- status: 1
-<% end %>
-<% 1.upto(30) do |i| %>
-large-blue-baseball_<%= i %>:
- variant: large-blue-baseball
- status: 2
- order: order_<%= i %>
-<% end %>
-<% 31.upto(75) do |i| %>
-large-blue-baseball_<%= i %>:
- variant: large-blue-baseball
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-large-green-baseball_<%= i %>:
- variant: large-green-baseball
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-xlarge-green-baseball_<%= i %>:
- variant: xlarge-green-baseball
- status: 1
-<% end %>
-<% 1.upto(30) do |i| %>
-ror_tote_v_<%= i %>:
- variant: ror_tote_v
- order: order_<%= i %>
- status: 2
-<% end %>
-<% 31.upto(75) do |i| %>
-ror_tote_v_<%= i %>:
- variant: ror_tote_v
- status: 1
-<% end %>
-<% 1.upto(30) do |i| %>
-ror_bag_v_<%= i %>:
- variant: ror_bag_v
- order: order_<%= i %>
- status: 2
-<% end %>
-<% 31.upto(75) do |i| %>
-ror_bag_v_<%= i %>:
- variant: ror_bag_v
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-ror_jr_spaghetti_v_<%= i %>:
- variant: ror_jr_spaghetti_v
- status: 1
-<% end %>
-<% 1.upto(75) do |i| %>
-ror_mug_v_<%= i %>:
- variant: ror_mug_v
- status: 1
-<% end %>
-#<% 1.upto(75) do |i| %>
-#ror_ringer_v_<%= i %>:
-# variant: ror_ringer_v
-# status: 1
-#<% end %>
-<% 1.upto(75) do |i| %>
-ror_stein_v_<%= i %>:
- variant: ror_stein_v
- status: 1
-<% end %>
\ No newline at end of file
diff --git a/db/sample/line_items.yml b/db/sample/line_items.yml
deleted file mode 100644
index 64ebd505ed1..00000000000
--- a/db/sample/line_items.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-<% for i in 1..30 do%>
-li_<%= i %>:
- order: order_<%= i %>
- variant: ror_tote_v
- quantity: 1
- price: 8.99
-<% end %>
-
-<% for i in 1..30 do%>
-li_<%= i + 30 %>:
- order: order_<%= i %>
- variant: ror_bag_v
- quantity: 1
- price: 8.99
-<% end %>
-
-<% for i in 1..30 do%>
-li_<%= i + 60 %>:
- order: order_<%= i %>
- variant: large-blue-baseball
- quantity: 1
- price: 19.99
-<% end %>
-
-
\ No newline at end of file
diff --git a/db/sample/option_types.yml b/db/sample/option_types.yml
deleted file mode 100644
index 02acce52acc..00000000000
--- a/db/sample/option_types.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-size:
- name: tshirt-size
- presentation: Size
-color:
- name: tshirt-color
- presentation: Color
diff --git a/db/sample/option_values.yml b/db/sample/option_values.yml
deleted file mode 100644
index c7108eeb929..00000000000
--- a/db/sample/option_values.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-s:
- name: Small
- presentation: S
- position: 1
- option_type: size
-m:
- name: Medium
- presentation: M
- position: 2
- option_type: size
-l:
- name: Large
- presentation: L
- position: 3
- option_type: size
-xl:
- name: Extra Large
- presentation: XL
- position: 4
- option_type: size
-red:
- name: Red
- presentation: Red
- position: 1
- option_type: color
-green:
- name: Green
- presentation: Green
- position: 2
- option_type: color
-blue:
- name: Blue
- presentation: Blue
- position: 3
- option_type: color
\ No newline at end of file
diff --git a/db/sample/orders.yml b/db/sample/orders.yml
deleted file mode 100644
index 553d4fa50c6..00000000000
--- a/db/sample/orders.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-<% 1.upto(30) do |i| %>
-order_<%= i %>:
-# id: <%= i %>
- user: frank
- number: <%= Array.new(9){rand(9)}.join %>
- status: <%= Order::Status::AUTHORIZED %>
- ship_method: 1
- ship_amount: 5.00
- tax_amount: 2.78
- item_total: 37.97
- total: 45.75
- ship_address: frank_address
- bill_address: frank_address
- ip_address: 127.0.0.1
-<% end %>
\ No newline at end of file
diff --git a/db/sample/product_option_types.yml b/db/sample/product_option_types.yml
deleted file mode 100644
index 1ef6ba90962..00000000000
--- a/db/sample/product_option_types.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-baseball_size:
- product: ror_baseball_jersey
- option_type: size
- position: 1
-baseball_color:
- product: ror_baseball_jersey
- option_type: color
- position: 2
diff --git a/db/sample/products.yml b/db/sample/products.yml
deleted file mode 100644
index 43eb5bbc33f..00000000000
--- a/db/sample/products.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-ror_tote:
- name: Ruby on Rails Tote
- description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh.
- price: 15.99
- category_id: 4
-ror_bag:
- name: Ruby on Rails Bag
- description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh.
- price: 22.99
- category_id: 4
-ror_baseball_jersey:
- name: Ruby on Rails Baseball Jersey
- description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh.
- price: 19.99
- category_id: 2
-ror_jr_spaghetti:
- name: Ruby on Rails Jr. Spaghetti
- description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh.
- price: 19.99
- category_id: 2
-ror_mug:
- name: Ruby on Rails Mug
- description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh.
- price: 13.99
- category_id: 3
-ror_ringer:
- name: Ruby on Rails Ringer T-Shirt
- description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh.
- price: 17.99
- category_id: 2
-ror_stein:
- name: Ruby on Rails Stein
- description: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla nonummy aliquet mi. Proin lacus. Ut placerat. Proin consequat, justo sit amet tempus consequat, elit est adipiscing odio, ut egestas pede eros in diam. Proin varius, lacus vitae suscipit varius, ipsum eros convallis nisi, sit amet sodales lectus pede non est. Duis augue. Suspendisse hendrerit pharetra metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Curabitur nec pede. Quisque volutpat, neque ac porttitor sodales, sem lacus rutrum nulla, ullamcorper placerat ante tortor ac odio. Suspendisse vel libero. Nullam volutpat magna vel ligula. Suspendisse sit amet metus. Nunc quis massa. Nulla facilisi. In enim. In venenatis nisi id eros. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nunc sit amet felis sed lectus tincidunt egestas. Mauris nibh.
- price: 16.99
- category_id: 3
\ No newline at end of file
diff --git a/db/sample/roles.yml b/db/sample/roles.yml
deleted file mode 100644
index 3ad603d0851..00000000000
--- a/db/sample/roles.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-user_role:
- id: 2
- name: user
\ No newline at end of file
diff --git a/db/sample/states.yml b/db/sample/states.yml
deleted file mode 100644
index 754b44b0a68..00000000000
--- a/db/sample/states.yml
+++ /dev/null
@@ -1,154 +0,0 @@
-alabama:
- name: Alabama
- abbr: AL
-alaska:
- name: Alaska
- abbr: AK
-arizona:
- name: Arizona
- abbr: AZ
-arkansas:
- name: Arkansas
- abbr: AR
-california:
- name: California
- abbr: CA
-colorado:
- name: Colorado
- abbr: CO
-connecticut:
- name: Connecticut
- abbr: CT
-deleware:
- name: Delaware
- abbr: DE
-dc:
- name: District of Columbia
- abbr: DC
-florida:
- name: Florida
- abbr: FL
-georgia:
- name: Georgia
- abbr: GA
-hawaii:
- name: Hawaii
- abbr: HI
-idaho:
- name: Idaho
- abbr: ID
-illinois:
- name: Illinois
- abbr: IL
-indiana:
- name: Indiana
- abbr: IN
-iowa:
- name: Iowa
- abbr: IA
-kansas:
- name: Kansas
- abbr: KS
-kentucky:
- name: Kentucky
- abbr: KY
-louisiana:
- name: Louisiana
- abbr: LA
-maine:
- name: Maine
- abbr: ME
-maryland:
- name: Maryland
- abbr: MD
-massachusetts:
- name: Massachusetts
- abbr: MA
-michigan:
- name: Michigan
- abbr: MI
-minnesota:
- name: Minnesota
- abbr: MN
-mississippi:
- name: Mississippi
- abbr: MS
-missouri:
- name: Missouri
- abbr: MO
-montana:
- name: Montana
- abbr: MT
-nebraska:
- name: Nebraska
- abbr: NE
-nevada:
- name: Nevada
- abbr: NV
-new_hampshire:
- name: New Hampshire
- abbr: NH
-new_jersey:
- name: New Jersey
- abbr: NJ
-new_mexico:
- name: New Mexico
- abbr: NM
-new_york:
- name: New York
- abbr: NY
-north_carolina:
- name: North Carolina
- abbr: NC
-north_dakota:
- name: North Dakota
- abbr: ND
-ohio:
- name: Ohio
- abbr: OH
-oklahoma:
- name: Oklahoma
- abbr: OK
-oregon:
- name: Oregon
- abbr: OR
-pennsylvania:
- name: Pennsylvania
- abbr: PA
-rhode_island:
- name: Rhode Island
- abbr: RI
-south_carolina:
- name: South Carolina
- abbr: SC
-south_dakota:
- name: South Dakota
- abbr: SD
-tennesse:
- name: Tennesse
- abbr: TN
-texas:
- name: Texas
- abbr: TX
-utah:
- name: Utah
- abbr: UT
-vermont:
- name: Vermont
- abbr: VT
-virginia:
- name: Virginia
- abbr: VA
-washington:
- id: 48
- name: Washington
- abbr: WA
-west_virginia:
- name: West Virginia
- abbr: WV
-wisconsin:
- name: Wisconsin
- abbr: WI
-wyoming:
- name: Wyoming
- abbr: WY
\ No newline at end of file
diff --git a/db/sample/taggings.yml b/db/sample/taggings.yml
deleted file mode 100644
index 670f25778a8..00000000000
--- a/db/sample/taggings.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-taggings_1:
- id: 1
- tag_id: 1 # delicious
- taggable_id: 1
- taggable_type: Product
-taggings_2:
- id: 2
- tag_id: 1 # delicious
- taggable_id: 2
- taggable_type: Product
-taggings_3:
- id: 3
- tag_id: 2 # sexy
- taggable_id: 3
- taggable_type: Product
-
\ No newline at end of file
diff --git a/db/sample/tags.yml b/db/sample/tags.yml
deleted file mode 100644
index defce6cad7f..00000000000
--- a/db/sample/tags.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-tags_001:
- name: delicious
- id: "1"
-tags_002:
- name: sexy
- id: "2"
diff --git a/db/sample/txns.yml b/db/sample/txns.yml
deleted file mode 100644
index 2b45c81b8d1..00000000000
--- a/db/sample/txns.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-<% 1.upto(30) do |i| %>
-txn_<%= i %>:
- amount: 45.75
- txn_type: <%=Txn::TxnType::AUTHORIZE%>
- response_code: 12345
- credit_card: visa_<%=i%>
-<% end %>
\ No newline at end of file
diff --git a/db/sample/users.yml b/db/sample/users.yml
deleted file mode 100644
index df78035c1e5..00000000000
--- a/db/sample/users.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-frank:
- login: frank.foo@example.com
- email: frank.foo@example.com
- salt: 7e3041ebc2fc05a40c60028e2c4901a81035d3cd
- crypted_password: 00742970dc9e6319f8019fd54864d3ea740f04b1
- # test
\ No newline at end of file
diff --git a/db/sample/variants.yml b/db/sample/variants.yml
deleted file mode 100644
index 9bf30df8bf2..00000000000
--- a/db/sample/variants.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-small-red-baseball:
- product: ror_baseball_jersey
- option_values: s, red
- sku: ROR-00001
-small-blue-baseball:
- product: ror_baseball_jersey
- option_values: s, blue
- sku: ROR-00002
-small-green-baseball:
- product: ror_baseball_jersey
- option_values: s, green
- sku: ROR-00003
-med-red-baseball:
- product: ror_baseball_jersey
- option_values: m, red
- sku: ROR-00004
-med-blue-baseball:
- product: ror_baseball_jersey
- option_values: m, blue
- sku: ROR-00005
-med-green-baseball:
- product: ror_baseball_jersey
- option_values: m, green
- sku: ROR-00006
-large-red-baseball:
- product: ror_baseball_jersey
- option_values: l, red
- sku: ROR-00007
-large-blue-baseball:
- product: ror_baseball_jersey
- option_values: l, blue
- sku: ROR-00008
-large-green-baseball:
- product: ror_baseball_jersey
- option_values: l, green
- sku: ROR-00009
-xlarge-green-baseball:
- product: ror_baseball_jersey
- option_values: xl, red
- sku: ROR-00010
-ror_tote_v:
- product: ror_tote
- sku: ROR-00011
-ror_bag_v:
- product: ror_bag
- sku: ROR-00012
-ror_jr_spaghetti_v:
- product: ror_jr_spaghetti
- sku: ROR-00013
-ror_mug_v:
- product: ror_mug
- sku: ROR-00014
-ror_ringer_v:
- product: ror_ringer
- sku: ROR-00015
-ror_stein_v:
- product: ror_stein
- sku: ROR-00016
-#ror_bag_v:
-# product: ror_bag
-# sku: ROR-00017
\ No newline at end of file
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
new file mode 100644
index 00000000000..4d9450b1d08
--- /dev/null
+++ b/docker-entrypoint.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# This is entrypoint for docker image of spree sandbox on docker cloud
+
+RAILS_ENV=development cd sandbox && bundle exec rails s -p 3000 -b '0.0.0.0'
diff --git a/frontend/Gemfile b/frontend/Gemfile
new file mode 100644
index 00000000000..13f166ada73
--- /dev/null
+++ b/frontend/Gemfile
@@ -0,0 +1,6 @@
+eval(File.read(File.dirname(__FILE__) + '/../common_spree_dependencies.rb'))
+
+gem 'spree_core', path: '../core'
+gem 'spree_api', path: '../api'
+
+gemspec
diff --git a/frontend/LICENSE b/frontend/LICENSE
new file mode 100644
index 00000000000..3b57c94ed0e
--- /dev/null
+++ b/frontend/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2007-2015, Spree Commerce, Inc. and other contributors
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name Spree nor the names of its contributors may be used to
+ endorse or promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 00000000000..1ec0777f60d
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,50 @@
+# Spree Bootstrap Frontend
+
+### Bootstrap 3 powered frontend.
+
+This has several large advantages:
+
+- Fully responsive - Mobile, tablet and desktop. With custom grids for each, collapsing elements, and full HDPI support. Current spree only goes half way.
+- Just 44 lines of custom SCSS, replacing 1328 lines of undocumented spree CSS. Plus most of these lines only add some visual style to the header and footer and can be removed.
+- The entire frontend can be easily customized: colours, grid, spacing, etc, by just overriding [variables from bootstrap](https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss) - giving a custom store design in minutes.
+- Bootstrap has some of the most [robust documentation](http://getbootstrap.com/css) of any framework, and a hugely active community. As this port uses only default bootstrap it means that entire spree frontend layout is documented by default.
+- Sites like [bootswatch](http://bootswatch.com) allow for one-file bootstrap drop-in spree themes.
+- Lots of [spree community will for bootstrap](https://groups.google.com/forum/#!searchin/spree-user/bootstrap/spree-user/B17492QdnGA/AF9vEzRzf4cJ).
+- Though this uses ‘full bootstrap’ for simplicity, you can remove the unused Bootstrap components you don’t require for minimal file sizes / weight.
+- Bootstrap is one of the largest most active open source projects out there - maintaining an entire framework just for spree makes little sense. Forget about cross browser bugs. Woo!
+
+Overview
+-------
+
+This stays as closely to the original spree frontend markup as possible. Helper decorators have been kept to a bare minimum. It utilises the [SCSS port](https://github.com/twbs/bootstrap-sass) of bootstrap 3 to keep inline with existing spree practices. It also includes support for `spree_auth_devise`.
+
+[](http://i.imgur.com/2Ycr8w8.png)
+[](http://i.imgur.com/XLi5DAs.png)
+[](http://i.imgur.com/UdKueAQ.png)
+[](http://i.imgur.com/mis2XHY.png)
+[](http://i.imgur.com/hF0IjWI.png)
+[](http://i.imgur.com/U06g9Jn.png)
+[](http://i.imgur.com/Ozh5vQr.png)
+
+Customizing
+-------
+
+Override the stylesheet to `vendor/assets/stylesheets/spree/frontend/frontend_bootstrap.css.scss`. Use this as your base stylesheet and edit as required.
+
+To style your spree store just override the bootstrap 3 variables. The full list of bootstrap variables can be found [here](https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss). You can override these by simply redefining the variable before the `@import` directive.
+For example:
+
+```scss
+$navbar-default-bg: #312312;
+$light-orange: #ff8c00;
+$navbar-default-color: $light-orange;
+
+@import "bootstrap";
+```
+
+This uses the [bootstrap-sass](https://github.com/twbs/bootstrap-sass) gem. So check there for full cutomization instructions.
+
+It’s quite powerful, here are some examples created in ~10 minutes with a few extra SCSS variables, no actual css edits required:
+
+[](http://i.imgur.com/m3zKV0s.png)
+[](http://i.imgur.com/eNyNFSg.png)
diff --git a/frontend/Rakefile b/frontend/Rakefile
new file mode 100644
index 00000000000..cf2bff13cda
--- /dev/null
+++ b/frontend/Rakefile
@@ -0,0 +1,15 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rspec/core/rake_task'
+require 'spree/testing_support/common_rake'
+
+RSpec::Core::RakeTask.new
+
+task default: :spec
+
+desc "Generates a dummy app for testing"
+task :test_app do
+ ENV['LIB_NAME'] = 'spree/frontend'
+ Rake::Task['common:test_app'].invoke
+end
diff --git a/frontend/app/assets/images/credit_cards/amex_cid.gif b/frontend/app/assets/images/credit_cards/amex_cid.gif
new file mode 100644
index 00000000000..fb940dc511e
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/amex_cid.gif differ
diff --git a/frontend/app/assets/images/credit_cards/credit_card.gif b/frontend/app/assets/images/credit_cards/credit_card.gif
new file mode 100644
index 00000000000..2e61a23c310
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/credit_card.gif differ
diff --git a/frontend/app/assets/images/credit_cards/discover_cid.gif b/frontend/app/assets/images/credit_cards/discover_cid.gif
new file mode 100644
index 00000000000..083820f464e
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/discover_cid.gif differ
diff --git a/frontend/app/assets/images/credit_cards/icons/american_express.png b/frontend/app/assets/images/credit_cards/icons/american_express.png
new file mode 100644
index 00000000000..73fa1ea749d
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/american_express.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/cirrus.png b/frontend/app/assets/images/credit_cards/icons/cirrus.png
new file mode 100644
index 00000000000..81065defa11
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/cirrus.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/delta.png b/frontend/app/assets/images/credit_cards/icons/delta.png
new file mode 100644
index 00000000000..f7c79d9a646
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/delta.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/diners_club.png b/frontend/app/assets/images/credit_cards/icons/diners_club.png
new file mode 100644
index 00000000000..02d8c9508e5
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/diners_club.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/directdebit.png b/frontend/app/assets/images/credit_cards/icons/directdebit.png
new file mode 100644
index 00000000000..c76274ed01c
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/directdebit.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/discover.png b/frontend/app/assets/images/credit_cards/icons/discover.png
new file mode 100644
index 00000000000..e7d199b8e56
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/discover.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/egold.png b/frontend/app/assets/images/credit_cards/icons/egold.png
new file mode 100644
index 00000000000..abb2bba8cb1
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/egold.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/jcb.png b/frontend/app/assets/images/credit_cards/icons/jcb.png
new file mode 100644
index 00000000000..f2d6bc7af30
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/jcb.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/maestro.png b/frontend/app/assets/images/credit_cards/icons/maestro.png
new file mode 100644
index 00000000000..1dd6f42cad5
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/maestro.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/master.png b/frontend/app/assets/images/credit_cards/icons/master.png
new file mode 100644
index 00000000000..f8992cdfbbe
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/master.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/paypal.png b/frontend/app/assets/images/credit_cards/icons/paypal.png
new file mode 100644
index 00000000000..91051c00551
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/paypal.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/solo.png b/frontend/app/assets/images/credit_cards/icons/solo.png
new file mode 100644
index 00000000000..ad867f1dba3
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/solo.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/switch.png b/frontend/app/assets/images/credit_cards/icons/switch.png
new file mode 100644
index 00000000000..5d8315b1ebc
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/switch.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/visa.png b/frontend/app/assets/images/credit_cards/icons/visa.png
new file mode 100644
index 00000000000..7545c43fc0c
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/visa.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/visaelectron.png b/frontend/app/assets/images/credit_cards/icons/visaelectron.png
new file mode 100644
index 00000000000..4b0c4a8453f
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/visaelectron.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/westernunion.png b/frontend/app/assets/images/credit_cards/icons/westernunion.png
new file mode 100644
index 00000000000..2a1766ac683
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/westernunion.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/wirecard.png b/frontend/app/assets/images/credit_cards/icons/wirecard.png
new file mode 100644
index 00000000000..0cc55c5b7d6
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/wirecard.png differ
diff --git a/frontend/app/assets/images/credit_cards/icons/worldpay.png b/frontend/app/assets/images/credit_cards/icons/worldpay.png
new file mode 100644
index 00000000000..29b89ddb1c6
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/icons/worldpay.png differ
diff --git a/frontend/app/assets/images/credit_cards/master_cid.jpg b/frontend/app/assets/images/credit_cards/master_cid.jpg
new file mode 100644
index 00000000000..b27fa71a7f0
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/master_cid.jpg differ
diff --git a/frontend/app/assets/images/credit_cards/visa_cid.gif b/frontend/app/assets/images/credit_cards/visa_cid.gif
new file mode 100644
index 00000000000..4048a820e33
Binary files /dev/null and b/frontend/app/assets/images/credit_cards/visa_cid.gif differ
diff --git a/frontend/app/assets/images/favicon.ico b/frontend/app/assets/images/favicon.ico
new file mode 100644
index 00000000000..b5b89e35635
Binary files /dev/null and b/frontend/app/assets/images/favicon.ico differ
diff --git a/frontend/app/assets/images/logo/spree_50.png b/frontend/app/assets/images/logo/spree_50.png
new file mode 100644
index 00000000000..81e10ac368a
Binary files /dev/null and b/frontend/app/assets/images/logo/spree_50.png differ
diff --git a/frontend/app/assets/javascripts/spree/frontend.js b/frontend/app/assets/javascripts/spree/frontend.js
new file mode 100644
index 00000000000..0154bc1ef57
--- /dev/null
+++ b/frontend/app/assets/javascripts/spree/frontend.js
@@ -0,0 +1,16 @@
+//= require bootstrap-sprockets
+//= require jquery.payment
+//= require spree
+//= require polyfill.min
+//= require fetch.umd
+//= require spree/api/main
+//= require spree/frontend/api_tokens
+//= require spree/frontend/cart
+//= require spree/frontend/checkout
+//= require spree/frontend/checkout/address
+//= require spree/frontend/checkout/payment
+//= require spree/frontend/product
+
+Spree.routes.api_tokens = Spree.pathFor('api_tokens')
+Spree.routes.ensure_cart = Spree.pathFor('ensure_cart')
+Spree.routes.api_v2_storefront_cart_apply_coupon_code = Spree.pathFor('api/v2/storefront/cart/apply_coupon_code')
diff --git a/frontend/app/assets/javascripts/spree/frontend/api_tokens.js b/frontend/app/assets/javascripts/spree/frontend/api_tokens.js
new file mode 100644
index 00000000000..5c462728372
--- /dev/null
+++ b/frontend/app/assets/javascripts/spree/frontend/api_tokens.js
@@ -0,0 +1,17 @@
+Spree.fetchApiTokens = function () {
+ fetch(Spree.routes.api_tokens, {
+ method: 'GET',
+ credentials: 'same-origin'
+ }).then(function (response) {
+ switch (response.status) {
+ case 200:
+ response.json().then(function (json) {
+ SpreeAPI.orderToken = json.order_token
+ SpreeAPI.oauthToken = json.oauth_token
+ })
+ break
+ }
+ })
+}
+
+Spree.ready(function () { Spree.fetchApiTokens() })
diff --git a/frontend/app/assets/javascripts/spree/frontend/cart.js b/frontend/app/assets/javascripts/spree/frontend/cart.js
new file mode 100644
index 00000000000..c4e4390503f
--- /dev/null
+++ b/frontend/app/assets/javascripts/spree/frontend/cart.js
@@ -0,0 +1,59 @@
+//= require spree/frontend/coupon_manager
+
+Spree.ready(function ($) {
+ var formUpdateCart = $('form#update-cart')
+ if (formUpdateCart.length) {
+ $('form#update-cart a.delete').show().one('click', function () {
+ $(this).parents('.line-item').first().find('input.line_item_quantity').val(0)
+ $(this).parents('form').first().submit()
+ return false
+ })
+ }
+ formUpdateCart.submit(function (event) {
+ var input = {
+ couponCodeField: $('#order_coupon_code'),
+ couponStatus: $('#coupon_status')
+ }
+ var updateButton = $('form#update-cart #update-button')
+ updateButton.attr('disabled', true)
+ if ($.trim(input.couponCodeField.val()).length > 0) {
+ // eslint-disable-next-line no-undef
+ if (new CouponManager(input).applyCoupon()) {
+ this.submit()
+ return true
+ } else {
+ updateButton.attr('disabled', false)
+ event.preventDefault()
+ return false
+ }
+ }
+ })
+})
+
+Spree.fetch_cart = function () {
+ return $.ajax({
+ url: Spree.pathFor('cart_link')
+ }).done(function (data) {
+ return $('#link-to-cart').html(data)
+ })
+}
+
+Spree.ensureCart = function (successCallback) {
+ if (SpreeAPI.orderToken) {
+ successCallback()
+ } else {
+ fetch(Spree.routes.ensure_cart, {
+ method: 'POST',
+ credentials: 'same-origin'
+ }).then(function (response) {
+ switch (response.status) {
+ case 200:
+ response.json().then(function (json) {
+ SpreeAPI.orderToken = json.token
+ successCallback()
+ })
+ break
+ }
+ })
+ }
+}
diff --git a/frontend/app/assets/javascripts/spree/frontend/checkout.js b/frontend/app/assets/javascripts/spree/frontend/checkout.js
new file mode 100644
index 00000000000..1d5cd9b81a1
--- /dev/null
+++ b/frontend/app/assets/javascripts/spree/frontend/checkout.js
@@ -0,0 +1,19 @@
+Spree.disableSaveOnClick = function () {
+ $('form.edit_order').on('submit', function (event) {
+ if ($(this).data('submitted') === true) {
+ event.preventDefault()
+ } else {
+ $(this).data('submitted', true)
+ $(this).find(':submit, :image').removeClass('primary').addClass('disabled')
+ }
+ })
+}
+
+Spree.enableSave = function () {
+ $('form.edit_order').data('submitted', false).find(':submit, :image').attr('disabled', false).addClass('primary').removeClass('disabled')
+}
+
+Spree.ready(function () {
+ Spree.Checkout = {}
+ return Spree.Checkout
+})
diff --git a/frontend/app/assets/javascripts/spree/frontend/checkout/address.js b/frontend/app/assets/javascripts/spree/frontend/checkout/address.js
new file mode 100644
index 00000000000..711f983676d
--- /dev/null
+++ b/frontend/app/assets/javascripts/spree/frontend/checkout/address.js
@@ -0,0 +1,102 @@
+Spree.ready(function ($) {
+ Spree.onAddress = function () {
+ if ($('#checkout_form_address').length) {
+ Spree.updateState = function (region) {
+ var countryId = getCountryId(region)
+ if (countryId != null) {
+ if (Spree.Checkout[countryId] == null) {
+ $.get(Spree.routes.states_search, {
+ country_id: countryId
+ }).done(function (data) {
+ Spree.Checkout[countryId] = {
+ states: data.states,
+ states_required: data.states_required
+ }
+ Spree.fillStates(Spree.Checkout[countryId], region)
+ })
+ } else {
+ Spree.fillStates(Spree.Checkout[countryId], region)
+ }
+ }
+ }
+ Spree.fillStates = function (data, region) {
+ var selected, statesWithBlank
+ var statesRequired = data.states_required
+ var states = data.states
+ var statePara = $('#' + region + 'state')
+ var stateSelect = statePara.find('select')
+ var stateInput = statePara.find('input')
+ var stateSpanRequired = statePara.find('[id$="state-required"]')
+ if (states.length > 0) {
+ selected = parseInt(stateSelect.val())
+ stateSelect.html('')
+ statesWithBlank = [
+ {
+ name: '',
+ id: ''
+ }
+ ].concat(states)
+ $.each(statesWithBlank, function (idx, state) {
+ var opt = $(document.createElement('option')).attr('value', state.id).html(state.name)
+ if (selected === state.id) {
+ opt.prop('selected', true)
+ }
+ stateSelect.append(opt)
+ })
+ stateSelect.prop('disabled', false).show()
+ stateInput.hide().prop('disabled', true)
+ statePara.show()
+ stateSpanRequired.show()
+ if (statesRequired) {
+ stateSelect.addClass('required')
+ }
+ stateSelect.removeClass('hidden')
+ stateInput.removeClass('required')
+ } else {
+ stateSelect.hide().prop('disabled', true)
+ stateInput.show()
+ if (statesRequired) {
+ stateSpanRequired.show()
+ stateInput.addClass('required')
+ } else {
+ stateInput.val('')
+ stateSpanRequired.hide()
+ stateInput.removeClass('required')
+ }
+ statePara.toggle(!!statesRequired)
+ stateInput.prop('disabled', !statesRequired)
+ stateInput.removeClass('hidden')
+ stateSelect.removeClass('required')
+ }
+ }
+ $('#bcountry select').change(function () {
+ Spree.updateState('b')
+ })
+ $('#scountry select').change(function () {
+ Spree.updateState('s')
+ })
+ Spree.updateState('b')
+
+ var orderUseBilling = $('input#order_use_billing')
+ orderUseBilling.change(function () {
+ updateShippingFormState(orderUseBilling)
+ })
+ updateShippingFormState(orderUseBilling)
+ }
+ function updateShippingFormState (orderUseBilling) {
+ if (orderUseBilling.is(':checked')) {
+ $('#shipping .inner').hide()
+ $('#shipping .inner input, #shipping .inner select').prop('disabled', true)
+ } else {
+ $('#shipping .inner').show()
+ $('#shipping .inner input, #shipping .inner select').prop('disabled', false)
+ Spree.updateState('s')
+ }
+ }
+
+ function getCountryId (region) {
+ return $('#' + region + 'country select').val()
+ }
+ }
+ Spree.onAddress()
+})
diff --git a/frontend/app/assets/javascripts/spree/frontend/checkout/payment.js b/frontend/app/assets/javascripts/spree/frontend/checkout/payment.js
new file mode 100644
index 00000000000..278d2e2b184
--- /dev/null
+++ b/frontend/app/assets/javascripts/spree/frontend/checkout/payment.js
@@ -0,0 +1,57 @@
+//= require spree/frontend/coupon_manager
+Spree.ready(function ($) {
+ Spree.onPayment = function () {
+ if ($('#checkout_form_payment').length) {
+ if ($('#existing_cards').length) {
+ $('#payment-method-fields').hide()
+ $('#payment-methods').hide()
+ $('#use_existing_card_yes').click(function () {
+ $('#payment-method-fields').hide()
+ $('#payment-methods').hide()
+ $('.existing-cc-radio').prop('disabled', false)
+ })
+ $('#use_existing_card_no').click(function () {
+ $('#payment-method-fields').show()
+ $('#payment-methods').show()
+ $('.existing-cc-radio').prop('disabled', true)
+ })
+ }
+ $('.cardNumber').payment('formatCardNumber')
+ $('.cardExpiry').payment('formatCardExpiry')
+ $('.cardCode').payment('formatCardCVC')
+ $('.cardNumber').change(function () {
+ $(this).parent().siblings('.ccType').val($.payment.cardType(this.value))
+ })
+ $('input[type="radio"][name="order[payments_attributes][][payment_method_id]"]').click(function () {
+ $('#payment-methods li').hide()
+ if (this.checked) {
+ $('#payment_method_' + this.value).show()
+ }
+ })
+ $(document).on('click', '#cvv_link', function (event) {
+ var windowName = 'cvv_info'
+ var windowOptions = 'left=20,top=20,width=500,height=500,toolbar=0,resizable=0,scrollbars=1'
+ window.open($(this).attr('href'), windowName, windowOptions)
+ event.preventDefault()
+ })
+ $('input[type="radio"]:checked').click()
+ $('#checkout_form_payment').submit(function (event) {
+ var input = {
+ couponCodeField: $('#order_coupon_code'),
+ couponStatus: $('#coupon_status')
+ }
+ if ($.trim(input.couponCodeField.val()).length > 0) {
+ // eslint-disable-next-line no-undef
+ if (new CouponManager(input).applyCoupon()) {
+ return true
+ } else {
+ Spree.enableSave()
+ event.preventDefault()
+ return false
+ }
+ }
+ })
+ }
+ }
+ Spree.onPayment()
+})
diff --git a/frontend/app/assets/javascripts/spree/frontend/checkout/shipment.js b/frontend/app/assets/javascripts/spree/frontend/checkout/shipment.js
new file mode 100644
index 00000000000..d99cde1c16d
--- /dev/null
+++ b/frontend/app/assets/javascripts/spree/frontend/checkout/shipment.js
@@ -0,0 +1,47 @@
+/* global accounting */
+function ShippingTotalManager (input1) {
+ this.input = input1
+ this.shippingMethods = this.input.shippingMethods
+ this.shipmentTotal = this.input.shipmentTotal
+ this.orderTotal = this.input.orderTotal
+ this.formatOptions = {
+ symbol: this.shipmentTotal.data('currency'),
+ decimal: this.shipmentTotal.attr('decimal-mark'),
+ thousand: this.shipmentTotal.attr('thousands-separator'),
+ precision: 2
+ }
+}
+
+ShippingTotalManager.prototype.calculateShipmentTotal = function () {
+ var checked = $(this.shippingMethods).filter(':checked')
+ this.sum = 0
+ $.each(checked, function (idx, shippingMethod) {
+ this.sum += this.parseCurrencyToFloat($(shippingMethod).data('cost'))
+ }.bind(this))
+ return this.readjustSummarySection(this.parseCurrencyToFloat(this.orderTotal.html()), this.sum, this.parseCurrencyToFloat(this.shipmentTotal.html()))
+}
+
+ShippingTotalManager.prototype.parseCurrencyToFloat = function (input) {
+ return accounting.unformat(input, this.formatOptions.decimal)
+}
+
+ShippingTotalManager.prototype.readjustSummarySection = function (orderTotal, newShipmentTotal, oldShipmentTotal) {
+ var newOrderTotal = orderTotal + (newShipmentTotal - oldShipmentTotal)
+ this.shipmentTotal.html(accounting.formatMoney(newShipmentTotal, this.formatOptions))
+ return this.orderTotal.html(accounting.formatMoney(newOrderTotal, this.formatOptions))
+}
+
+ShippingTotalManager.prototype.bindEvent = function () {
+ this.shippingMethods.change(function () {
+ return this.calculateShipmentTotal()
+ }.bind(this))
+}
+
+Spree.ready(function ($) {
+ var input = {
+ orderTotal: $('#summary-order-total'),
+ shipmentTotal: $('[data-hook="shipping-total"]'),
+ shippingMethods: $('input[data-behavior="shipping-method-selector"]')
+ }
+ return new ShippingTotalManager(input).bindEvent()
+})
diff --git a/frontend/app/assets/javascripts/spree/frontend/coupon_manager.js b/frontend/app/assets/javascripts/spree/frontend/coupon_manager.js
new file mode 100644
index 00000000000..19f767ccf8f
--- /dev/null
+++ b/frontend/app/assets/javascripts/spree/frontend/coupon_manager.js
@@ -0,0 +1,45 @@
+function CouponManager (input) {
+ this.input = input
+ this.couponCodeField = this.input.couponCodeField
+ this.couponApplied = false
+ this.couponStatus = this.input.couponStatus
+}
+
+CouponManager.prototype.applyCoupon = function () {
+ this.couponCode = $.trim($(this.couponCodeField).val())
+ if (this.couponCode !== '') {
+ if (this.couponStatus.length === 0) {
+ this.couponStatus = $('', {
+ id: 'coupon_status'
+ })
+ this.couponCodeField.parent().append(this.couponStatus)
+ }
+ this.couponStatus.removeClass()
+ this.sendRequest()
+ return this.couponApplied
+ } else {
+ return true
+ }
+}
+
+CouponManager.prototype.sendRequest = function () {
+ return $.ajax({
+ async: false,
+ method: 'PATCH',
+ url: Spree.routes.api_v2_storefront_cart_apply_coupon_code,
+ dataType: 'json',
+ headers: {
+ 'X-Spree-Order-Token': Spree.current_order_token
+ },
+ data: {
+ coupon_code: this.couponCode
+ }
+ }).done(function () {
+ this.couponCodeField.val('')
+ this.couponStatus.addClass('alert-success').html(Spree.translations.coupon_code_applied)
+ this.couponApplied = true
+ }.bind(this)).fail(function (xhr) {
+ var handler = xhr.responseJSON
+ this.couponStatus.addClass('alert-error').html(handler['error'])
+ }.bind(this))
+}
diff --git a/frontend/app/assets/javascripts/spree/frontend/product.js b/frontend/app/assets/javascripts/spree/frontend/product.js
new file mode 100644
index 00000000000..7b05755891f
--- /dev/null
+++ b/frontend/app/assets/javascripts/spree/frontend/product.js
@@ -0,0 +1,128 @@
+//= require spree/api/storefront/cart
+//= require spree/frontend/cart
+
+Spree.ready(function ($) {
+ Spree.addImageHandlers = function () {
+ var thumbnails = $('#product-images ul.thumbnails');
+ ($('#main-image')).data('selectedThumb', ($('#main-image img')).attr('src'));
+ ($('#main-image')).data('selectedThumbAlt', ($('#main-image img')).attr('alt'))
+ thumbnails.find('li').eq(0).addClass('selected')
+
+ thumbnails.find('a').on('click', function (event) {
+ ($('#main-image')).data('selectedThumb', ($(event.currentTarget)).attr('href'));
+ ($('#main-image')).data('selectedThumbId', ($(event.currentTarget)).parent().attr('id'));
+ ($('#main-image')).data('selectedThumbAlt', ($(event.currentTarget)).find('img').attr('alt'))
+ thumbnails.find('li').removeClass('selected');
+ ($(event.currentTarget)).parent('li').addClass('selected')
+ return false
+ })
+
+ thumbnails.find('li').on('mouseenter', function (event) {
+ return ($('#main-image img'))
+ .attr({ 'src': ($(event.currentTarget)).find('a').attr('href'), 'alt': ($(event.currentTarget)).find('img').attr('alt') })
+ })
+
+ return thumbnails.find('li').on('mouseleave', function (event) {
+ return ($('#main-image img'))
+ .attr({ 'src': ($('#main-image')).data('selectedThumb'), 'alt': ($('#main-image')).data('selectedThumbAlt') })
+ })
+ }
+
+ Spree.showVariantImages = function (variantId) {
+ ($('li.vtmb')).hide();
+ ($('li.tmb-' + variantId)).show()
+ var currentThumb = $('#' + ($('#main-image')).data('selectedThumbId'))
+
+ if (!currentThumb.hasClass('vtmb + variantId')) {
+ var thumb = $(($('#product-images ul.thumbnails li:visible.vtmb')).eq(0))
+
+ if (!(thumb.length > 0)) {
+ thumb = $(($('#product-images ul.thumbnails li:visible')).eq(0))
+ }
+
+ var newImg = thumb.find('a').attr('href')
+
+ var newAlt = thumb.find('img').attr('alt');
+ ($('#product-images ul.thumbnails li')).removeClass('selected')
+ thumb.addClass('selected');
+ ($('#main-image img')).attr({ 'src': newImg, 'alt': newAlt });
+ ($('#main-image')).data({ 'selectedThumb': newImg, 'selectedThumbAlt': newAlt })
+ return ($('#main-image')).data('selectedThumbId', thumb.attr('id'))
+ }
+ }
+
+ Spree.updateVariantPrice = function (variant) {
+ var variantPrice = variant.data('price')
+
+ if (variantPrice) {
+ return ($('.price.selling')).text(variantPrice)
+ }
+ }
+
+ Spree.disableCartForm = function (variant) {
+ var inStock = variant.data('in-stock')
+ return $('#add-to-cart-button').attr('disabled', !inStock)
+ }
+
+ var radios = $("#product-variants input[type='radio']")
+
+ if (radios.length > 0) {
+ var selectedRadio = $("#product-variants input[type='radio'][checked='checked']")
+ Spree.showVariantImages(selectedRadio.attr('value'))
+ Spree.updateVariantPrice(selectedRadio)
+ Spree.disableCartForm(selectedRadio)
+
+ radios.click(function (event) {
+ Spree.showVariantImages(this.value)
+ Spree.updateVariantPrice($(this))
+ return Spree.disableCartForm($(this))
+ })
+ }
+
+ return Spree.addImageHandlers()
+})
+
+Spree.ready(function () {
+ var addToCartForm = document.getElementById('add-to-cart-form')
+ var addToCartButton = document.getElementById('add-to-cart-button')
+
+ if (addToCartForm) {
+ // enable add to cart button
+ if (addToCartButton) {
+ addToCartButton.removeAttribute('disabled')
+ }
+
+ addToCartForm.addEventListener('submit', function (event) {
+ event.preventDefault()
+
+ // prevent multiple clicks
+ if (addToCartButton) {
+ addToCartButton.setAttribute('disabled', 'disabled')
+ }
+
+ var variantId = addToCartForm.elements.namedItem('variant_id').value
+ var quantity = parseInt(addToCartForm.elements.namedItem('quantity').value, 10)
+
+ // we need to ensure that we have an existing cart we want to add the item to
+ // if we have already a cart assigned to this guest / user this won't create
+ // another one
+ Spree.ensureCart(
+ function () {
+ SpreeAPI.Storefront.addToCart(
+ variantId,
+ quantity,
+ {}, // options hash - you can pass additional parameters here, your backend
+ // needs to be aware of those, see API docs:
+ // https://github.com/spree/spree/blob/master/api/docs/v2/storefront/index.yaml#L42
+ function () {
+ // redirect with `variant_id` is crucial for analytics tracking
+ // provided by `spree_analytics_trackers` extension
+ window.location = Spree.routes.cart + '?variant_id=' + variantId.toString()
+ },
+ function (error) { alert(error) } // failure callback for 422 and 50x errors
+ )
+ }
+ )
+ })
+ }
+})
diff --git a/frontend/app/assets/stylesheets/spree/frontend.css b/frontend/app/assets/stylesheets/spree/frontend.css
new file mode 100644
index 00000000000..10c7eea4a27
--- /dev/null
+++ b/frontend/app/assets/stylesheets/spree/frontend.css
@@ -0,0 +1,5 @@
+/*
+* This is a manifest file that includes stylesheets for spree_frontend
+ *= require spree/frontend/frontend_bootstrap
+ *= require_self
+*/
diff --git a/frontend/app/assets/stylesheets/spree/frontend/_variables.scss b/frontend/app/assets/stylesheets/spree/frontend/_variables.scss
new file mode 100644
index 00000000000..730ddd59d69
--- /dev/null
+++ b/frontend/app/assets/stylesheets/spree/frontend/_variables.scss
@@ -0,0 +1,4 @@
+/*--------------------------------
+ Colors
+--------------------------------*/
+$brand-primary: #1c5c92;
\ No newline at end of file
diff --git a/frontend/app/assets/stylesheets/spree/frontend/frontend_bootstrap.css.scss b/frontend/app/assets/stylesheets/spree/frontend/frontend_bootstrap.css.scss
new file mode 100644
index 00000000000..84fd9c8e14e
--- /dev/null
+++ b/frontend/app/assets/stylesheets/spree/frontend/frontend_bootstrap.css.scss
@@ -0,0 +1,115 @@
+//Add your custom bootstrap variables here, see https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss for full list of variables.
+
+@import "bootstrap-sprockets";
+@import "variables";
+@import "bootstrap";
+
+// -- Spree Custom Header and Footer ---------------------
+#spree-header {
+ background-color: $brand-primary;
+ margin-bottom: $line-height-computed;
+ padding-top: 0.25em;
+
+ #header {
+ background: rgba($gray-darker, 0.4);
+ padding: $line-height-computed 0;
+ }
+
+ #logo {
+ margin-bottom: 10px;
+
+ img {
+ max-width: 100%;
+ max-height: 5.5vmax;
+ }
+ }
+
+ @media (max-width: 767px) {
+ .navbar-nav > li > a {
+ padding-top: 0;
+ padding-bottom: 10px;
+ line-height: $line-height-computed;
+ }
+ }
+
+ .nav a {
+ color: white;
+
+ &:hover, &:focus {
+ background: rgba($gray-darker, 0.4);
+ }
+ }
+
+ .navbar {
+ border: 0;
+ margin-bottom: 0;
+ }
+}
+
+#spree-footer {
+ background: $gray-dark;
+ padding-top: $padding-base-horizontal;
+ margin-top: $line-height-computed;
+ color: white;
+}
+
+// -- Spree Layout Custom Rules -------------------------
+
+.alert-notice { @extend .alert-success; }
+.alert-error { @extend .alert-danger; }
+.alert-alert { @extend .alert-info; }
+
+.product-body {
+ height: 170px;
+}
+
+.progress-steps {
+ margin-top: $line-height-computed;
+}
+
+h1 {
+ margin-bottom: 30px;
+}
+
+// Center cart line items
+table {
+ &.table > tbody > tr {
+ &.line-item td, &.stock-item td {
+ vertical-align: middle;
+ }
+ }
+
+ line-items {
+ > tbody, > tfoot {
+ > tr > td {
+ vertical-align: middle;
+
+ &.order-qty {
+ text-align: center;
+ }
+ }
+ }
+ }
+}
+
+// Footer links
+#footer-left a {
+ color: lighten($brand-primary, 20);
+
+ &:hover {
+ color: lighten($brand-primary, 10);
+ }
+}
+
+// Updated credit-card image
+#credit-card-image {
+ margin-top: -10px;
+}
+
+.existing-credit-card-list td {
+ padding: 5px;
+}
+
+.save-user-address-wrapper {
+ display: inline-block;
+}
diff --git a/frontend/app/controllers/spree/checkout_controller.rb b/frontend/app/controllers/spree/checkout_controller.rb
new file mode 100644
index 00000000000..3e9515938a6
--- /dev/null
+++ b/frontend/app/controllers/spree/checkout_controller.rb
@@ -0,0 +1,206 @@
+module Spree
+ # This is somewhat contrary to standard REST convention since there is not
+ # actually a Checkout object. There's enough distinct logic specific to
+ # checkout which has nothing to do with updating an order that this approach
+ # is waranted.
+ class CheckoutController < Spree::StoreController
+ before_action :set_cache_header, only: [:edit]
+ before_action :load_order_with_lock
+ before_action :ensure_valid_state_lock_version, only: [:update]
+ before_action :set_state_if_present
+
+ before_action :ensure_order_not_completed
+ before_action :ensure_checkout_allowed
+ before_action :ensure_sufficient_stock_lines
+ before_action :ensure_valid_state
+
+ before_action :associate_user
+ before_action :check_authorization
+
+ before_action :setup_for_current_state
+ before_action :add_store_credit_payments, :remove_store_credit_payments, only: [:update]
+
+ helper 'spree/orders'
+
+ rescue_from Spree::Core::GatewayError, with: :rescue_from_spree_gateway_error
+
+ # Updates the order and advances to the next state (when possible.)
+ def update
+ if @order.update_from_params(params, permitted_checkout_attributes, request.headers.env)
+ @order.temporary_address = !params[:save_user_address]
+ unless @order.next
+ flash[:error] = @order.errors.full_messages.join("\n")
+ redirect_to(checkout_state_path(@order.state)) && return
+ end
+
+ if @order.completed?
+ @current_order = nil
+ flash.notice = Spree.t(:order_processed_successfully)
+ flash['order_completed'] = true
+ redirect_to completion_route
+ else
+ redirect_to checkout_state_path(@order.state)
+ end
+ else
+ render :edit
+ end
+ end
+
+ private
+
+ def unknown_state?
+ (params[:state] && !@order.has_checkout_step?(params[:state])) ||
+ (!params[:state] && !@order.has_checkout_step?(@order.state))
+ end
+
+ def insufficient_payment?
+ params[:state] == 'confirm' &&
+ @order.payment_required? &&
+ @order.payments.valid.sum(:amount) != @order.total
+ end
+
+ def correct_state
+ if unknown_state?
+ @order.checkout_steps.first
+ elsif insufficient_payment?
+ 'payment'
+ else
+ @order.state
+ end
+ end
+
+ def ensure_valid_state
+ if @order.state != correct_state && !skip_state_validation?
+ flash.keep
+ @order.update_column(:state, correct_state)
+ redirect_to checkout_state_path(@order.state)
+ end
+ end
+
+ # Should be overriden if you have areas of your checkout that don't match
+ # up to a step within checkout_steps, such as a registration step
+ def skip_state_validation?
+ false
+ end
+
+ def load_order_with_lock
+ @order = current_order(lock: true)
+ redirect_to(spree.cart_path) && return unless @order
+ end
+
+ def ensure_valid_state_lock_version
+ if params[:order] && params[:order][:state_lock_version]
+ changes = @order.changes if @order.changed?
+ @order.reload.with_lock do
+ unless @order.state_lock_version == params[:order].delete(:state_lock_version).to_i
+ flash[:error] = Spree.t(:order_already_updated)
+ redirect_to(checkout_state_path(@order.state)) && return
+ end
+ @order.increment!(:state_lock_version)
+ end
+ @order.assign_attributes(changes) if changes
+ end
+ end
+
+ def set_state_if_present
+ if params[:state]
+ redirect_to checkout_state_path(@order.state) if @order.can_go_to_state?(params[:state]) && !skip_state_validation?
+ @order.state = params[:state]
+ end
+ end
+
+ def ensure_checkout_allowed
+ redirect_to spree.cart_path unless @order.checkout_allowed?
+ end
+
+ def ensure_order_not_completed
+ redirect_to spree.cart_path if @order.completed?
+ end
+
+ def ensure_sufficient_stock_lines
+ if @order.insufficient_stock_lines.present?
+ flash[:error] = Spree.t(:inventory_error_flash_for_insufficient_quantity)
+ redirect_to spree.cart_path
+ end
+ end
+
+ # Provides a route to redirect after order completion
+ def completion_route(custom_params = nil)
+ spree.order_path(@order, custom_params)
+ end
+
+ def setup_for_current_state
+ method_name = :"before_#{@order.state}"
+ send(method_name) if respond_to?(method_name, true)
+ end
+
+ def before_address
+ # if the user has a default address, a callback takes care of setting
+ # that; but if he doesn't, we need to build an empty one here
+ @order.bill_address ||= Address.build_default
+ @order.ship_address ||= Address.build_default if @order.checkout_steps.include?('delivery')
+ end
+
+ def before_delivery
+ return if params[:order].present?
+
+ packages = @order.shipments.map(&:to_package)
+ @differentiator = Spree::Stock::Differentiator.new(@order, packages)
+ end
+
+ def before_payment
+ if @order.checkout_steps.include? 'delivery'
+ packages = @order.shipments.map(&:to_package)
+ @differentiator = Spree::Stock::Differentiator.new(@order, packages)
+ @differentiator.missing.each do |variant, quantity|
+ Spree::Dependencies.cart_remove_item_service.constantize.call(order: @order, variant: variant, quantity: quantity)
+ end
+ end
+
+ @payment_sources = try_spree_current_user.payment_sources if try_spree_current_user&.respond_to?(:payment_sources)
+ end
+
+ def add_store_credit_payments
+ if params.key?(:apply_store_credit)
+ add_store_credit_service.call(order: @order)
+
+ # Remove other payment method parameters.
+ params[:order].delete(:payments_attributes)
+ params[:order].delete(:existing_card)
+ params.delete(:payment_source)
+
+ # Return to the Payments page if additional payment is needed.
+ redirect_to checkout_state_path(@order.state) and return if @order.payments.valid.sum(:amount) < @order.total
+ end
+ end
+
+ def remove_store_credit_payments
+ if params.key?(:remove_store_credit)
+ remove_store_credit_service.call(order: @order)
+ redirect_to checkout_state_path(@order.state) and return
+ end
+ end
+
+ def rescue_from_spree_gateway_error(exception)
+ flash.now[:error] = Spree.t(:spree_gateway_error_flash_for_checkout)
+ @order.errors.add(:base, exception.message)
+ render :edit
+ end
+
+ def check_authorization
+ authorize!(:edit, current_order, cookies.signed[:token])
+ end
+
+ def set_cache_header
+ response.headers['Cache-Control'] = 'no-store'
+ end
+
+ def add_store_credit_service
+ Spree::Dependencies.checkout_add_store_credit_service.constantize
+ end
+
+ def remove_store_credit_service
+ Spree::Dependencies.checkout_remove_store_credit_service.constantize
+ end
+ end
+end
diff --git a/frontend/app/controllers/spree/content_controller.rb b/frontend/app/controllers/spree/content_controller.rb
new file mode 100644
index 00000000000..16616f7ee4d
--- /dev/null
+++ b/frontend/app/controllers/spree/content_controller.rb
@@ -0,0 +1,18 @@
+module Spree
+ class ContentController < Spree::StoreController
+ # Don't serve local files or static assets
+ after_action :fire_visited_path, except: :cvv
+
+ respond_to :html
+
+ def test; end
+
+ def cvv
+ render layout: false
+ end
+
+ def fire_visited_path
+ Spree::PromotionHandler::Page.new(current_order, params[:action]).activate
+ end
+ end
+end
diff --git a/frontend/app/controllers/spree/home_controller.rb b/frontend/app/controllers/spree/home_controller.rb
new file mode 100644
index 00000000000..a724ab42a5c
--- /dev/null
+++ b/frontend/app/controllers/spree/home_controller.rb
@@ -0,0 +1,13 @@
+module Spree
+ class HomeController < Spree::StoreController
+ helper 'spree/products'
+ respond_to :html
+
+ def index
+ @searcher = build_searcher(params.merge(include_images: true))
+ @products = @searcher.retrieve_products
+ @products = @products.includes(:possible_promotions) if @products.respond_to?(:includes)
+ @taxonomies = Spree::Taxonomy.includes(root: :children)
+ end
+ end
+end
diff --git a/frontend/app/controllers/spree/locale_controller.rb b/frontend/app/controllers/spree/locale_controller.rb
new file mode 100644
index 00000000000..1d84195d3fa
--- /dev/null
+++ b/frontend/app/controllers/spree/locale_controller.rb
@@ -0,0 +1,14 @@
+module Spree
+ class LocaleController < Spree::StoreController
+ def set
+ session['user_return_to'] = request.referer if request.referer&.starts_with?('http://' + request.host)
+ if params[:locale] && I18n.available_locales.map(&:to_s).include?(params[:locale])
+ session[:locale] = I18n.locale = params[:locale]
+ flash.notice = Spree.t(:locale_changed)
+ else
+ flash[:error] = Spree.t(:locale_not_changed)
+ end
+ redirect_back_or_default(spree.root_path)
+ end
+ end
+end
diff --git a/frontend/app/controllers/spree/orders_controller.rb b/frontend/app/controllers/spree/orders_controller.rb
new file mode 100644
index 00000000000..dc29e343e57
--- /dev/null
+++ b/frontend/app/controllers/spree/orders_controller.rb
@@ -0,0 +1,144 @@
+module Spree
+ class OrdersController < Spree::StoreController
+ before_action :check_authorization
+ helper 'spree/products', 'spree/orders'
+
+ respond_to :html
+
+ before_action :assign_order_with_lock, only: :update
+ skip_before_action :verify_authenticity_token, only: [:populate]
+
+ def show
+ @order = Order.includes(line_items: [variant: [:option_values, :images, :product]], bill_address: :state, ship_address: :state).find_by!(number: params[:id])
+ end
+
+ def update
+ @variant = Spree::Variant.find(params[:variant_id]) if params[:variant_id]
+ if Cart::Update.call(order: @order, params: order_params).success?
+ respond_with(@order) do |format|
+ format.html do
+ if params.key?(:checkout)
+ @order.next if @order.cart?
+ redirect_to checkout_state_path(@order.checkout_steps.first)
+ else
+ redirect_to cart_path
+ end
+ end
+ end
+ else
+ respond_with(@order)
+ end
+ end
+
+ # Shows the current incomplete order from the session
+ def edit
+ @order = current_order || Order.incomplete.
+ includes(line_items: [variant: [:images, :option_values, :product]]).
+ find_or_initialize_by(token: cookies.signed[:token])
+ associate_user
+ end
+
+ # Adds a new item to the order (creating a new order if none already exists)
+ def populate
+ ActiveSupport::Deprecation.warn(<<-DEPRECATION, caller)
+ OrdersController#populate is deprecated and will be removed in Spree 4.0.
+ Please use `/api/v2/storefront/cart/add_item` endpoint instead.
+ See documentation: https://github.com/spree/spree/blob/master/api/docs/v2/storefront/index.yaml#L42
+ DEPRECATION
+
+ order = current_order(create_order_if_necessary: true)
+ variant = Spree::Variant.find(params[:variant_id])
+ quantity = params[:quantity].to_i
+ options = params[:options] || {}
+
+ # 2,147,483,647 is crazy. See issue #2695.
+ if quantity.between?(1, 2_147_483_647)
+ begin
+ result = cart_add_item_service.call(order: order,
+ variant: variant,
+ quantity: quantity,
+ options: options)
+ if result.failure?
+ error = result.value.errors.full_messages.join(', ')
+ else
+ order.update_line_item_prices!
+ order.create_tax_charge!
+ order.update_with_updater!
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ error = e.record.errors.full_messages.join(', ')
+ end
+ else
+ error = Spree.t(:please_enter_reasonable_quantity)
+ end
+
+ if error
+ flash[:error] = error
+ redirect_back_or_default(spree.root_path)
+ else
+ respond_with(order) do |format|
+ format.html { redirect_to(cart_path(variant_id: variant.id)) }
+ end
+ end
+ end
+
+ def populate_redirect
+ ActiveSupport::Deprecation.warn(<<-DEPRECATION, caller)
+ OrdersController#populate is deprecated and will be removed in Spree 4.0.
+ Please use `/api/v2/storefront/cart/add_item` endpoint instead.
+ See documentation: https://github.com/spree/spree/blob/master/api/docs/v2/storefront/index.yaml#L42
+ DEPRECATION
+ flash[:error] = Spree.t(:populate_get_error)
+ redirect_to cart_path
+ end
+
+ def empty
+ current_order.try(:empty!)
+
+ redirect_to spree.cart_path
+ end
+
+ private
+
+ def accurate_title
+ if @order&.completed?
+ Spree.t(:order_number, number: @order.number)
+ else
+ Spree.t(:shopping_cart)
+ end
+ end
+
+ def check_authorization
+ order = Spree::Order.find_by(number: params[:id]) if params[:id].present?
+ order ||= current_order
+
+ if order && action_name.to_sym == :show
+ authorize! :show, order, cookies.signed[:token]
+ elsif order
+ authorize! :edit, order, cookies.signed[:token]
+ else
+ authorize! :create, Spree::Order
+ end
+ end
+
+ def order_params
+ if params[:order]
+ params[:order].permit(*permitted_order_attributes)
+ else
+ {}
+ end
+ end
+
+ def assign_order_with_lock
+ @order = current_order(lock: true)
+ unless @order
+ flash[:error] = Spree.t(:order_not_found)
+ redirect_to root_path and return
+ end
+ end
+
+ def cart_add_item_service
+ Spree::Dependencies.cart_add_item_service.constantize
+ end
+ end
+end
diff --git a/frontend/app/controllers/spree/products_controller.rb b/frontend/app/controllers/spree/products_controller.rb
new file mode 100644
index 00000000000..e3f58648bc9
--- /dev/null
+++ b/frontend/app/controllers/spree/products_controller.rb
@@ -0,0 +1,62 @@
+module Spree
+ class ProductsController < Spree::StoreController
+ before_action :load_product, only: :show
+ before_action :load_taxon, only: :index
+
+ helper 'spree/taxons'
+
+ respond_to :html
+
+ def index
+ @searcher = build_searcher(params.merge(include_images: true))
+ @products = @searcher.retrieve_products
+ @products = @products.includes(:possible_promotions) if @products.respond_to?(:includes)
+ @taxonomies = Spree::Taxonomy.includes(root: :children)
+ end
+
+ def show
+ @variants = @product.variants_including_master.
+ spree_base_scopes.
+ active(current_currency).
+ includes([:option_values, :images])
+ @product_properties = @product.product_properties.includes(:property)
+ @taxon = params[:taxon_id].present? ? Spree::Taxon.find(params[:taxon_id]) : @product.taxons.first
+ redirect_if_legacy_path
+ end
+
+ private
+
+ def accurate_title
+ if @product
+ @product.meta_title.blank? ? @product.name : @product.meta_title
+ else
+ super
+ end
+ end
+
+ def load_product
+ @products = if try_spree_current_user.try(:has_spree_role?, 'admin')
+ Product.with_deleted
+ else
+ Product.active(current_currency)
+ end
+
+ @product = @products.includes(:variants_including_master, variant_images: :viewable).
+ friendly.distinct(false).find(params[:id])
+ end
+
+ def load_taxon
+ @taxon = Spree::Taxon.find(params[:taxon]) if params[:taxon].present?
+ end
+
+ def redirect_if_legacy_path
+ # If an old id or a numeric id was used to find the record,
+ # we should do a 301 redirect that uses the current friendly id.
+ if params[:id] != @product.friendly_id
+ params[:id] = @product.friendly_id
+ params.permit!
+ redirect_to url_for(params), status: :moved_permanently
+ end
+ end
+ end
+end
diff --git a/frontend/app/controllers/spree/store_controller.rb b/frontend/app/controllers/spree/store_controller.rb
new file mode 100644
index 00000000000..e11444cd9da
--- /dev/null
+++ b/frontend/app/controllers/spree/store_controller.rb
@@ -0,0 +1,38 @@
+module Spree
+ class StoreController < Spree::BaseController
+ include Spree::Core::ControllerHelpers::Order
+
+ skip_before_action :set_current_order, only: :cart_link
+ skip_before_action :verify_authenticity_token, only: :ensure_cart
+
+ def forbidden
+ render 'spree/shared/forbidden', layout: Spree::Config[:layout], status: 403
+ end
+
+ def unauthorized
+ render 'spree/shared/unauthorized', layout: Spree::Config[:layout], status: 401
+ end
+
+ def cart_link
+ render partial: 'spree/shared/link_to_cart'
+ fresh_when(simple_current_order)
+ end
+
+ def api_tokens
+ render json: {
+ order_token: current_order&.token,
+ oauth_token: current_oauth_token&.token
+ }
+ end
+
+ def ensure_cart
+ render json: current_order(create_order_if_necessary: true) # force creation of order if doesn't exists
+ end
+
+ protected
+
+ def config_locale
+ Spree::Frontend::Config[:locale]
+ end
+ end
+end
diff --git a/frontend/app/controllers/spree/taxons_controller.rb b/frontend/app/controllers/spree/taxons_controller.rb
new file mode 100644
index 00000000000..26b2435d93c
--- /dev/null
+++ b/frontend/app/controllers/spree/taxons_controller.rb
@@ -0,0 +1,22 @@
+module Spree
+ class TaxonsController < Spree::StoreController
+ helper 'spree/products'
+
+ respond_to :html
+
+ def show
+ @taxon = Taxon.friendly.find(params[:id])
+ return unless @taxon
+
+ @searcher = build_searcher(params.merge(taxon: @taxon.id, include_images: true))
+ @products = @searcher.retrieve_products
+ @taxonomies = Spree::Taxonomy.includes(root: :children)
+ end
+
+ private
+
+ def accurate_title
+ @taxon.try(:seo_title) || super
+ end
+ end
+end
diff --git a/frontend/app/helpers/spree/frontend_helper.rb b/frontend/app/helpers/spree/frontend_helper.rb
new file mode 100644
index 00000000000..382dc051d28
--- /dev/null
+++ b/frontend/app/helpers/spree/frontend_helper.rb
@@ -0,0 +1,94 @@
+module Spree
+ module FrontendHelper
+ def body_class
+ @body_class ||= content_for?(:sidebar) ? 'two-col' : 'one-col'
+ @body_class
+ end
+
+ def spree_breadcrumbs(taxon, separator = ' ')
+ return '' if current_page?('/') || taxon.nil?
+
+ separator = raw(separator)
+ crumbs = [content_tag(:li, content_tag(:span, link_to(content_tag(:span, Spree.t(:home), itemprop: 'name'), spree.root_path, itemprop: 'url') + separator, itemprop: 'item'), itemscope: 'itemscope', itemtype: 'https://schema.org/ListItem', itemprop: 'itemListElement')]
+ if taxon
+ crumbs << content_tag(:li, content_tag(:span, link_to(content_tag(:span, Spree.t(:products), itemprop: 'name'), spree.products_path, itemprop: 'url') + separator, itemprop: 'item'), itemscope: 'itemscope', itemtype: 'https://schema.org/ListItem', itemprop: 'itemListElement')
+ crumbs << taxon.ancestors.collect { |ancestor| content_tag(:li, content_tag(:span, link_to(content_tag(:span, ancestor.name, itemprop: 'name'), seo_url(ancestor), itemprop: 'url') + separator, itemprop: 'item'), itemscope: 'itemscope', itemtype: 'https://schema.org/ListItem', itemprop: 'itemListElement') } unless taxon.ancestors.empty?
+ crumbs << content_tag(:li, content_tag(:span, link_to(content_tag(:span, taxon.name, itemprop: 'name'), seo_url(taxon), itemprop: 'url'), itemprop: 'item'), class: 'active', itemscope: 'itemscope', itemtype: 'https://schema.org/ListItem', itemprop: 'itemListElement')
+ else
+ crumbs << content_tag(:li, content_tag(:span, Spree.t(:products), itemprop: 'item'), class: 'active', itemscope: 'itemscope', itemtype: 'https://schema.org/ListItem', itemprop: 'itemListElement')
+ end
+ crumb_list = content_tag(:ol, raw(crumbs.flatten.map(&:mb_chars).join), class: 'breadcrumb', itemscope: 'itemscope', itemtype: 'https://schema.org/BreadcrumbList')
+ content_tag(:nav, crumb_list, id: 'breadcrumbs', class: 'col-md-12')
+ end
+
+ def checkout_progress(numbers: false)
+ states = @order.checkout_steps
+ items = states.each_with_index.map do |state, i|
+ text = Spree.t("order_state.#{state}").titleize
+ text.prepend("#{i.succ}. ") if numbers
+
+ css_classes = []
+ current_index = states.index(@order.state)
+ state_index = states.index(state)
+
+ if state_index < current_index
+ css_classes << 'completed'
+ text = link_to text, checkout_state_path(state)
+ end
+
+ css_classes << 'next' if state_index == current_index + 1
+ css_classes << 'active' if state == @order.state
+ css_classes << 'first' if state_index == 0
+ css_classes << 'last' if state_index == states.length - 1
+ # No more joined classes. IE6 is not a target browser.
+ # Hack: Stops being wrapped round previous items twice.
+ if state_index < current_index
+ content_tag('li', text, class: css_classes.join(' '))
+ else
+ content_tag('li', content_tag('a', text), class: css_classes.join(' '))
+ end
+ end
+ content_tag('ul', raw(items.join("\n")), class: 'progress-steps nav nav-pills nav-justified', id: "checkout-step-#{@order.state}")
+ end
+
+ def flash_messages(opts = {})
+ ignore_types = ['order_completed'].concat(Array(opts[:ignore_types]).map(&:to_s) || [])
+
+ flash.each do |msg_type, text|
+ concat(content_tag(:div, text, class: "alert alert-#{msg_type}")) unless ignore_types.include?(msg_type)
+ end
+ nil
+ end
+
+ def link_to_cart(text = nil)
+ text = text ? h(text) : Spree.t('cart')
+ css_class = nil
+
+ if simple_current_order.nil? || simple_current_order.item_count.zero?
+ text = " #{text}: (#{Spree.t('empty')})"
+ css_class = 'empty'
+ else
+ text = " #{text}: (#{simple_current_order.item_count}) #{simple_current_order.display_total.to_html}"
+ css_class = 'full'
+ end
+
+ link_to text.html_safe, spree.cart_path, class: "cart-info #{css_class}"
+ end
+
+ def taxons_tree(root_taxon, current_taxon, max_level = 1)
+ return '' if max_level < 1 || root_taxon.leaf?
+
+ content_tag :div, class: 'list-group' do
+ taxons = root_taxon.children.map do |taxon|
+ css_class = current_taxon&.self_and_ancestors&.include?(taxon) ? 'list-group-item active' : 'list-group-item'
+ link_to(taxon.name, seo_url(taxon), class: css_class) + taxons_tree(taxon, current_taxon, max_level - 1)
+ end
+ safe_join(taxons, "\n")
+ end
+ end
+
+ def set_image_alt(image, size)
+ image.alt.present? ? image.alt : image_alt(main_app.url_for(image.url(size)))
+ end
+ end
+end
diff --git a/frontend/app/helpers/spree/orders_helper.rb b/frontend/app/helpers/spree/orders_helper.rb
new file mode 100644
index 00000000000..17444cf105f
--- /dev/null
+++ b/frontend/app/helpers/spree/orders_helper.rb
@@ -0,0 +1,7 @@
+module Spree
+ module OrdersHelper
+ def order_just_completed?(order)
+ flash[:order_completed] && order.present?
+ end
+ end
+end
diff --git a/frontend/app/helpers/spree/taxons_helper.rb b/frontend/app/helpers/spree/taxons_helper.rb
new file mode 100644
index 00000000000..1a17ea264a7
--- /dev/null
+++ b/frontend/app/helpers/spree/taxons_helper.rb
@@ -0,0 +1,19 @@
+module Spree
+ module TaxonsHelper
+ # Retrieves the collection of products to display when "previewing" a taxon. This is abstracted into a helper so
+ # that we can use configurations as well as make it easier for end users to override this determination. One idea is
+ # to show the most popular products for a particular taxon (that is an exercise left to the developer.)
+ def taxon_preview(taxon, max = 4)
+ products = taxon.active_products.select('DISTINCT (spree_products.id), spree_products.*, spree_products_taxons.position').limit(max)
+ if products.size < max
+ products_arel = Spree::Product.arel_table
+ taxon.descendants.each do |taxon|
+ to_get = max - products.length
+ products += taxon.active_products.select('DISTINCT (spree_products.id), spree_products.*, spree_products_taxons.position').where(products_arel[:id].not_in(products.map(&:id))).limit(to_get)
+ break if products.size >= max
+ end
+ end
+ products
+ end
+ end
+end
diff --git a/frontend/app/models/spree/frontend_configuration.rb b/frontend/app/models/spree/frontend_configuration.rb
new file mode 100644
index 00000000000..5c9ca9d4294
--- /dev/null
+++ b/frontend/app/models/spree/frontend_configuration.rb
@@ -0,0 +1,6 @@
+module Spree
+ class FrontendConfiguration < Preferences::Configuration
+ preference :coupon_codes_enabled, :boolean, default: true # Determines if we show coupon code form at cart and checkout
+ preference :locale, :string, default: Rails.application.config.i18n.default_locale
+ end
+end
diff --git a/frontend/app/views/kaminari/twitter-bootstrap-3/_first_page.html.erb b/frontend/app/views/kaminari/twitter-bootstrap-3/_first_page.html.erb
new file mode 100755
index 00000000000..42125d65b99
--- /dev/null
+++ b/frontend/app/views/kaminari/twitter-bootstrap-3/_first_page.html.erb
@@ -0,0 +1,13 @@
+<%# Link to the "First" page
+ - available local variables
+ url: url to the first page
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+-%>
+<% unless current_page.first? %>
+
\ No newline at end of file
diff --git a/frontend/app/views/kaminari/twitter-bootstrap-3/_last_page.html.erb b/frontend/app/views/kaminari/twitter-bootstrap-3/_last_page.html.erb
new file mode 100755
index 00000000000..4a5c43421f8
--- /dev/null
+++ b/frontend/app/views/kaminari/twitter-bootstrap-3/_last_page.html.erb
@@ -0,0 +1,13 @@
+<%# Link to the "Last" page
+ - available local variables
+ url: url to the last page
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+-%>
+<% unless current_page.last? %>
+
<%# "next" class present for border styling in twitter bootstrap %>
+ <%= link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote} %>
+
+<% end %>
diff --git a/frontend/app/views/kaminari/twitter-bootstrap-3/_next_page.html.erb b/frontend/app/views/kaminari/twitter-bootstrap-3/_next_page.html.erb
new file mode 100755
index 00000000000..be785d3725a
--- /dev/null
+++ b/frontend/app/views/kaminari/twitter-bootstrap-3/_next_page.html.erb
@@ -0,0 +1,13 @@
+<%# Link to the "Next" page
+ - available local variables
+ url: url to the next page
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+-%>
+<% unless current_page.last? %>
+
+<% end %>
diff --git a/frontend/app/views/kaminari/twitter-bootstrap-3/_page.html.erb b/frontend/app/views/kaminari/twitter-bootstrap-3/_page.html.erb
new file mode 100755
index 00000000000..de555d108d3
--- /dev/null
+++ b/frontend/app/views/kaminari/twitter-bootstrap-3/_page.html.erb
@@ -0,0 +1,12 @@
+<%# Link showing page number
+ - available local variables
+ page: a page object for "this" page
+ url: url to this page
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+-%>
+
diff --git a/frontend/app/views/kaminari/twitter-bootstrap-3/_paginator.html.erb b/frontend/app/views/kaminari/twitter-bootstrap-3/_paginator.html.erb
new file mode 100755
index 00000000000..f2e1233e828
--- /dev/null
+++ b/frontend/app/views/kaminari/twitter-bootstrap-3/_paginator.html.erb
@@ -0,0 +1,25 @@
+<%# The container tag
+ - available local variables
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+ paginator: the paginator that renders the pagination tags inside
+-%>
+<% pagination_class ||= '' %>
+
+<%= paginator.render do %>
+
+<% end %>
diff --git a/frontend/app/views/kaminari/twitter-bootstrap-3/_prev_page.html.erb b/frontend/app/views/kaminari/twitter-bootstrap-3/_prev_page.html.erb
new file mode 100755
index 00000000000..d57e271ad33
--- /dev/null
+++ b/frontend/app/views/kaminari/twitter-bootstrap-3/_prev_page.html.erb
@@ -0,0 +1,13 @@
+<%# Link to the "Previous" page
+ - available local variables
+ url: url to the previous page
+ current_page: a page object for the currently displayed page
+ total_pages: total number of pages
+ per_page: number of items to fetch per page
+ remote: data-remote
+-%>
+<% unless current_page.first? %>
+
+ <% end %>
+
+ <% end %>
+ <% end %>
+
+ <% if order.adjustments.nonzero.eligible.exists? %>
+
+ <% order.adjustments.nonzero.eligible.each do |adjustment| %>
+ <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %>
+
+<% end %>
diff --git a/frontend/app/views/spree/content/cvv.html.erb b/frontend/app/views/spree/content/cvv.html.erb
new file mode 100644
index 00000000000..bd063cc8767
--- /dev/null
+++ b/frontend/app/views/spree/content/cvv.html.erb
@@ -0,0 +1,13 @@
+
+
<%= Spree.t(:what_is_a_cvv) %>
+
For Visa, MasterCard, and Discover cards, the card code is the last 3 digit number located on the back of your card on or above your signature line. For an American Express card, it is the 4 digits on the FRONT above the end of your card number.
+
To help reduce fraud in the card-not-present environment, credit card companies have introduced a card code program. Visa calls this code Card Verification Value (CVV); MasterCard calls it Card Validation Code (CVC); Discover calls it Card ID (CID). The card code is a three- or four- digit security code that is printed on the back of cards. The number typically appears at the end of the signature panel.
+<% end %>
diff --git a/frontend/app/views/spree/products/_taxons.html.erb b/frontend/app/views/spree/products/_taxons.html.erb
new file mode 100644
index 00000000000..d2a8d68319a
--- /dev/null
+++ b/frontend/app/views/spree/products/_taxons.html.erb
@@ -0,0 +1,8 @@
+<% if @product.taxons.present? %>
+
<%= Spree.t(:look_for_similar_items) %>
+
+ <% @product.taxons.each do |taxon| %>
+
<%= link_to taxon.name, seo_url(taxon) %>
+ <% end %>
+
+<% end %>
diff --git a/frontend/app/views/spree/products/_thumbnails.html.erb b/frontend/app/views/spree/products/_thumbnails.html.erb
new file mode 100644
index 00000000000..22f4cf82a40
--- /dev/null
+++ b/frontend/app/views/spree/products/_thumbnails.html.erb
@@ -0,0 +1,21 @@
+<%# no need for thumbnails unless there is more than one image %>
+<% if (@product.images + @product.variant_images).uniq.size > 1 %>
+
+<% end %>
\ No newline at end of file
diff --git a/frontend/app/views/spree/shared/_login_bar.html.erb b/frontend/app/views/spree/shared/_login_bar.html.erb
new file mode 100755
index 00000000000..a27a706ac2a
--- /dev/null
+++ b/frontend/app/views/spree/shared/_login_bar.html.erb
@@ -0,0 +1,6 @@
+<% if spree_current_user %>
+
+ <% end %>
+
+ <% end %>
+
+
+ <% order.adjustments.eligible.each do |adjustment| %>
+ <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %>
+
+
<%= adjustment.label %>
+
<%= adjustment.display_amount.to_html %>
+
+ <% end %>
+
+
diff --git a/frontend/app/views/spree/shared/_payment.html.erb b/frontend/app/views/spree/shared/_payment.html.erb
new file mode 100644
index 00000000000..bc9e187ae3d
--- /dev/null
+++ b/frontend/app/views/spree/shared/_payment.html.erb
@@ -0,0 +1,20 @@
+<% source = payment.source %>
+
+<% if source.is_a?(Spree::CreditCard) %>
+
+ <% unless (cc_type = source.cc_type).blank? %>
+ <%= image_tag "credit_cards/icons/#{cc_type}.png" %>
+ <% end %>
+ <% if source.last_digits %>
+ <%= Spree.t(:ending_in) %> <%= source.last_digits %>
+ <% end %>
+
+
+ <%= source.name %>
+<% else %>
+ <%= content_tag(:span, payment.payment_method.name) %>
+<% end %>
+
+(<%= payment.display_amount %>)
+
+
diff --git a/frontend/app/views/spree/shared/_products.html.erb b/frontend/app/views/spree/shared/_products.html.erb
new file mode 100644
index 00000000000..22f959aa7bc
--- /dev/null
+++ b/frontend/app/views/spree/shared/_products.html.erb
@@ -0,0 +1,27 @@
+<% content_for :head do %>
+ <% if products.respond_to?(:total_pages) %>
+ <%= rel_next_prev_link_tags products %>
+ <% end %>
+<% end %>
+
+
+ <% cache [I18n.locale, @taxon] do %>
+ <%= render partial: 'taxon', collection: @taxon.children %>
+ <% end %>
+
+<% end %>
diff --git a/frontend/config/initializers/assets.rb b/frontend/config/initializers/assets.rb
new file mode 100644
index 00000000000..aea44f19227
--- /dev/null
+++ b/frontend/config/initializers/assets.rb
@@ -0,0 +1 @@
+Rails.application.config.assets.precompile += %w(favicon.ico credit_cards/* spree/frontend/checkout/shipment)
diff --git a/frontend/config/initializers/canonical_rails.rb b/frontend/config/initializers/canonical_rails.rb
new file mode 100644
index 00000000000..39889ca5811
--- /dev/null
+++ b/frontend/config/initializers/canonical_rails.rb
@@ -0,0 +1,14 @@
+CanonicalRails.setup do |config|
+ # http://en.wikipedia.org/wiki/URL_normalization
+ # Trailing slash represents semantics of a directory, ie a collection view - implying an :index get route;
+ # otherwise we have to assume semantics of an instance of a resource type, a member view - implying a :show get route
+ #
+ # Acts as a whitelist for routes to have trailing slashes
+
+ config.collection_actions # = [:index]
+
+ # Parameter spamming can cause index dilution by creating seemingly different URLs with identical or near-identical content.
+ # Unless whitelisted, these parameters will be omitted
+
+ config.whitelisted_parameters = [:keywords, :page, :search, :taxon]
+end
diff --git a/frontend/config/routes.rb b/frontend/config/routes.rb
new file mode 100644
index 00000000000..11577c90c4a
--- /dev/null
+++ b/frontend/config/routes.rb
@@ -0,0 +1,33 @@
+Spree::Core::Engine.add_routes do
+ root to: 'home#index'
+
+ resources :products, only: [:index, :show]
+
+ get '/locale/set', to: 'locale#set'
+
+ # non-restful checkout stuff
+ patch '/checkout/update/:state', to: 'checkout#update', as: :update_checkout
+ get '/checkout/:state', to: 'checkout#edit', as: :checkout_state
+ get '/checkout', to: 'checkout#edit', as: :checkout
+
+ get '/orders/populate', to: 'orders#populate_redirect'
+
+ resources :orders, except: [:index, :new, :create, :destroy] do
+ post :populate, on: :collection
+ end
+
+ get '/cart', to: 'orders#edit', as: :cart
+ patch '/cart', to: 'orders#update', as: :update_cart
+ put '/cart/empty', to: 'orders#empty', as: :empty_cart
+
+ # route globbing for pretty nested taxon and product paths
+ get '/t/*id', to: 'taxons#show', as: :nested_taxons
+
+ get '/unauthorized', to: 'home#unauthorized', as: :unauthorized
+ get '/content/cvv', to: 'content#cvv', as: :cvv
+ get '/content/test', to: 'content#test'
+ get '/cart_link', to: 'store#cart_link', as: :cart_link
+
+ get '/api_tokens', to: 'store#api_tokens'
+ post '/ensure_cart', to: 'store#ensure_cart'
+end
diff --git a/frontend/lib/generators/spree/frontend/copy_views/copy_views_generator.rb b/frontend/lib/generators/spree/frontend/copy_views/copy_views_generator.rb
new file mode 100644
index 00000000000..acdbd703d6a
--- /dev/null
+++ b/frontend/lib/generators/spree/frontend/copy_views/copy_views_generator.rb
@@ -0,0 +1,15 @@
+module Spree
+ module Frontend
+ class CopyViewsGenerator < Rails::Generators::Base
+ desc 'Copies views from spree frontend to your application'
+
+ def self.source_paths
+ [File.expand_path('../../../../../app', __dir__)]
+ end
+
+ def copy_views
+ directory 'views', './app/views'
+ end
+ end
+ end
+end
diff --git a/frontend/lib/spree/frontend.rb b/frontend/lib/spree/frontend.rb
new file mode 100644
index 00000000000..a6020d0f218
--- /dev/null
+++ b/frontend/lib/spree/frontend.rb
@@ -0,0 +1,11 @@
+require 'rails/all'
+require 'sprockets/rails'
+
+require 'bootstrap-sass'
+require 'canonical-rails'
+require 'deface'
+require 'jquery-rails'
+require 'spree/core'
+require 'spree/frontend/middleware/seo_assist'
+require 'spree/frontend/engine'
+require 'spree/responder'
diff --git a/frontend/lib/spree/frontend/engine.rb b/frontend/lib/spree/frontend/engine.rb
new file mode 100644
index 00000000000..b31ced48f86
--- /dev/null
+++ b/frontend/lib/spree/frontend/engine.rb
@@ -0,0 +1,18 @@
+module Spree
+ module Frontend
+ class Engine < ::Rails::Engine
+ config.middleware.use 'Spree::Frontend::Middleware::SeoAssist'
+
+ # sets the manifests / assets to be precompiled, even when initialize_on_precompile is false
+ initializer 'spree.assets.precompile', group: :all do |app|
+ app.config.assets.precompile += %w[
+ spree/frontend/all*
+ ]
+ end
+
+ initializer 'spree.frontend.environment', before: :load_config_initializers do |_app|
+ Spree::Frontend::Config = Spree::FrontendConfiguration.new
+ end
+ end
+ end
+end
diff --git a/frontend/lib/spree/frontend/middleware/seo_assist.rb b/frontend/lib/spree/frontend/middleware/seo_assist.rb
new file mode 100644
index 00000000000..a5c377aa581
--- /dev/null
+++ b/frontend/lib/spree/frontend/middleware/seo_assist.rb
@@ -0,0 +1,50 @@
+# Make redirects for SEO needs
+module Spree
+ module Frontend
+ module Middleware
+ class SeoAssist
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = Rack::Request.new(env)
+ params = request.params
+
+ taxon_id = params['taxon']
+
+ # redirect requests using taxon id's to their permalinks
+ if !taxon_id.blank? && !taxon_id.is_a?(Hash) && taxon = Taxon.find(taxon_id)
+ params.delete('taxon')
+
+ return build_response(params, "#{request.script_name}t/#{taxon.permalink}")
+ elsif env['PATH_INFO'] =~ /^\/(t|products)(\/\S+)?\/$/
+ # ensures no trailing / for taxon and product urls
+
+ return build_response(params, env['PATH_INFO'][0...-1])
+ end
+
+ @app.call(env)
+ end
+
+ private
+
+ def build_response(params, location)
+ query = build_query(params)
+ location += '?' + query unless query.blank?
+ [301, { 'Location' => location }, []]
+ end
+
+ def build_query(params)
+ params.map do |k, v|
+ if v.class == Array
+ build_query(v.map { |x| ["#{k}[]", x] })
+ else
+ k + '=' + Rack::Utils.escape(v)
+ end
+ end.join('&')
+ end
+ end
+ end
+ end
+end
diff --git a/frontend/lib/spree_frontend.rb b/frontend/lib/spree_frontend.rb
new file mode 100644
index 00000000000..46fe1ff18a0
--- /dev/null
+++ b/frontend/lib/spree_frontend.rb
@@ -0,0 +1 @@
+require 'spree/frontend'
diff --git a/frontend/script/rails b/frontend/script/rails
new file mode 100755
index 00000000000..1a685aba06e
--- /dev/null
+++ b/frontend/script/rails
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
+
+ENGINE_ROOT = File.expand_path('../..', __FILE__)
+ENGINE_PATH = File.expand_path('../../lib/spree/frontend/engine', __FILE__)
+
+require 'rails/all'
+require 'rails/engine/commands'
+
diff --git a/frontend/spec/controllers/controller_extension_spec.rb b/frontend/spec/controllers/controller_extension_spec.rb
new file mode 100644
index 00000000000..e4436638442
--- /dev/null
+++ b/frontend/spec/controllers/controller_extension_spec.rb
@@ -0,0 +1,126 @@
+require 'spec_helper'
+
+# This test tests the functionality within
+# spree/core/controller_helpers/respond_with.rb
+# Rather than duck-punching the existing controllers, let's define a custom one:
+class Spree::CustomController < Spree::BaseController
+ def index
+ respond_with(Spree::Address.new) do |format|
+ format.html { render plain: 'neutral' }
+ end
+ end
+
+ def create
+ # Just need a model with validations
+ # Address is good enough, so let's go with that
+ address = Spree::Address.new(params[:address])
+ respond_with(address)
+ end
+end
+
+describe Spree::CustomController, type: :controller do
+ after do
+ Spree::CustomController.clear_overrides!
+ end
+
+ before do
+ @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
+ r.draw do
+ get 'index', to: 'spree/custom#index'
+ post 'create', to: 'spree/custom#create'
+ end
+ end
+ end
+
+ context 'extension testing' do
+ context 'index' do
+ context 'specify symbol for handler instead of Proc' do
+ before do
+ Spree::CustomController.class_eval do
+ respond_override(index: { html: { success: :success_method } })
+
+ private
+
+ def success_method
+ render plain: 'success!!!'
+ end
+ end
+ end
+
+ describe 'GET' do
+ it 'has value success' do
+ spree_get :index
+ expect(response).to be_successful
+ assert (response.body =~ /success!!!/)
+ end
+ end
+ end
+
+ context 'render' do
+ before do
+ Spree::CustomController.instance_eval do
+ respond_override(index: { html: { success: -> { render(plain: 'success!!!') } } })
+ respond_override(index: { html: { failure: -> { render(plain: 'failure!!!') } } })
+ end
+ end
+
+ describe 'GET' do
+ it 'has value success' do
+ spree_get :index
+ expect(response).to be_successful
+ assert (response.body =~ /success!!!/)
+ end
+ end
+ end
+
+ context 'redirect' do
+ before do
+ Spree::CustomController.instance_eval do
+ respond_override(index: { html: { success: -> { redirect_to('/cart') } } })
+ respond_override(index: { html: { failure: -> { render(plain: 'failure!!!') } } })
+ end
+ end
+
+ describe 'GET' do
+ it 'has value success' do
+ spree_get :index
+ expect(response).to be_redirect
+ end
+ end
+ end
+
+ context 'validation error' do
+ before do
+ Spree::CustomController.instance_eval do
+ respond_to :html
+ respond_override(create: { html: { success: -> { render(plain: 'success!!!') } } })
+ respond_override(create: { html: { failure: -> { render(plain: 'failure!!!') } } })
+ end
+ end
+
+ describe 'POST' do
+ it 'has value success' do
+ spree_post :create
+ expect(response).to be_successful
+ assert (response.body =~ /success!/)
+ end
+ end
+ end
+
+ context 'A different controllers respond_override. Regression test for #1301' do
+ before do
+ Spree::CheckoutController.instance_eval do
+ respond_override(index: { html: { success: -> { render(plain: 'success!!!') } } })
+ end
+ end
+
+ describe 'POST' do
+ it 'does not effect the wrong controller' do
+ spree_get :index
+ assert (response.body =~ /neutral/)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/frontend/spec/controllers/controller_helpers_spec.rb b/frontend/spec/controllers/controller_helpers_spec.rb
new file mode 100644
index 00000000000..cb4ea14fd78
--- /dev/null
+++ b/frontend/spec/controllers/controller_helpers_spec.rb
@@ -0,0 +1,121 @@
+require 'spec_helper'
+
+# In this file, we want to test that the controller helpers function correctly
+# So we need to use one of the controllers inside Spree.
+# ProductsController is good.
+describe Spree::ProductsController, type: :controller do
+ let!(:available_locales) { [:en, :de] }
+ let!(:available_locale) { :de }
+ let!(:unavailable_locale) { :ru }
+
+ before do
+ I18n.enforce_available_locales = false
+ expect(I18n).to receive(:available_locales).and_return(available_locales)
+ end
+
+ after do
+ Spree::Frontend::Config[:locale] = :en
+ Rails.application.config.i18n.default_locale = :en
+ I18n.locale = :en
+ I18n.enforce_available_locales = true
+ end
+
+ # Regression test for #1184
+ context 'when session locale not set' do
+ before do
+ session[:locale] = nil
+ end
+
+ context 'when Spree::Frontend::Config[:locale] not present' do
+ before do
+ Spree::Frontend::Config[:locale] = nil
+ end
+
+ context 'when rails application default locale not set' do
+ before do
+ Rails.application.config.i18n.default_locale = nil
+ end
+
+ it 'sets the I18n default locale' do
+ spree_get :index
+ expect(I18n.locale).to eq(I18n.default_locale)
+ end
+ end
+
+ context 'when rails application default locale is set' do
+ context 'and not in available_locales' do
+ before do
+ Rails.application.config.i18n.default_locale = unavailable_locale
+ end
+
+ it 'sets the I18n default locale' do
+ spree_get :index
+ expect(I18n.locale).to eq(I18n.default_locale)
+ end
+ end
+
+ context 'and in available_locales' do
+ before do
+ Rails.application.config.i18n.default_locale = available_locale
+ end
+
+ it 'sets the rails app locale' do
+ expect(I18n.locale).to eq(:en)
+ spree_get :index
+ expect(I18n.locale).to eq(available_locale)
+ end
+ end
+ end
+ end
+
+ context 'when Spree::Frontend::Config[:locale] is present' do
+ context 'and not in available_locales' do
+ before do
+ Spree::Frontend::Config[:locale] = unavailable_locale
+ end
+
+ it 'sets the I18n default locale' do
+ spree_get :index
+ expect(I18n.locale).to eq(I18n.default_locale)
+ end
+ end
+
+ context 'and not in available_locales' do
+ before do
+ Spree::Frontend::Config[:locale] = available_locale
+ end
+
+ it 'sets the default locale based on Spree::Frontend::Config[:locale]' do
+ expect(I18n.locale).to eq(:en)
+ spree_get :index
+ expect(I18n.locale).to eq(available_locale)
+ end
+ end
+ end
+ end
+
+ context 'when session locale is set' do
+ context 'and not in available_locales' do
+ before do
+ session[:locale] = unavailable_locale
+ end
+
+ it 'sets the I18n default locale' do
+ spree_get :index
+ expect(I18n.locale).to eq(I18n.default_locale)
+ end
+ end
+
+ context 'and in available_locales' do
+ before do
+ session[:locale] = available_locale
+ end
+
+ it 'sets the session locale' do
+ expect(I18n.locale).to eq(:en)
+ spree_get :index
+ expect(I18n.locale).to eq(available_locale)
+ end
+ end
+ end
+end
diff --git a/frontend/spec/controllers/spree/checkout_controller_spec.rb b/frontend/spec/controllers/spree/checkout_controller_spec.rb
new file mode 100644
index 00000000000..f4d930f1579
--- /dev/null
+++ b/frontend/spec/controllers/spree/checkout_controller_spec.rb
@@ -0,0 +1,549 @@
+require 'spec_helper'
+
+describe Spree::CheckoutController, type: :controller do
+ let(:token) { 'some_token' }
+ let(:user) { stub_model(Spree::LegacyUser) }
+ let(:order) { FactoryBot.create(:order_with_totals) }
+
+ let(:address_params) do
+ address = FactoryBot.build(:address)
+ address.attributes.except('created_at', 'updated_at')
+ end
+
+ before do
+ allow(controller).to receive_messages try_spree_current_user: user
+ allow(controller).to receive_messages spree_current_user: user
+ allow(controller).to receive_messages current_order: order
+ end
+
+ context '#edit' do
+ it 'checks if the user is authorized for :edit' do
+ expect(controller).to receive(:authorize!).with(:edit, order, token)
+ request.cookie_jar.signed[:token] = token
+ spree_get :edit, state: 'address'
+ end
+
+ it 'redirects to the cart path unless checkout_allowed?' do
+ allow(order).to receive_messages checkout_allowed?: false
+ spree_get :edit, state: 'delivery'
+ expect(response).to redirect_to(spree.cart_path)
+ end
+
+ it 'redirects to the cart path if current_order is nil' do
+ allow(controller).to receive(:current_order).and_return(nil)
+ spree_get :edit, state: 'delivery'
+ expect(response).to redirect_to(spree.cart_path)
+ end
+
+ it 'redirects to cart if order is completed' do
+ allow(order).to receive_messages(completed?: true)
+ spree_get :edit, state: 'address'
+ expect(response).to redirect_to(spree.cart_path)
+ end
+
+ # Regression test for #2280
+ it 'redirects to current step trying to access a future step' do
+ order.update_column(:state, 'address')
+ spree_get :edit, state: 'delivery'
+ expect(response).to redirect_to spree.checkout_state_path('address')
+ end
+
+ context 'when entering the checkout' do
+ before do
+ # The first step for checkout controller is address
+ # Transitioning into this state first is required
+ order.update_column(:state, 'address')
+ end
+
+ it 'associates the order with a user' do
+ order.update_column :user_id, nil
+ expect(order).to receive(:associate_user!).with(user)
+ spree_get :edit, {}, order_id: 1
+ end
+ end
+ end
+
+ context '#update' do
+ it 'checks if the user is authorized for :edit' do
+ expect(controller).to receive(:authorize!).with(:edit, order, token)
+ request.cookie_jar.signed[:token] = token
+ spree_post :update, state: 'address'
+ end
+
+ context 'save successful' do
+ def spree_post_address
+ spree_post :update,
+ state: 'address',
+ order: {
+ bill_address_attributes: address_params,
+ use_billing: true
+ }
+ end
+
+ before do
+ # Must have *a* shipping method and a payment method so updating from address works
+ allow(order).to receive(:available_shipping_methods).
+ and_return [stub_model(Spree::ShippingMethod)]
+ allow(order).to receive(:available_payment_methods).
+ and_return [stub_model(Spree::PaymentMethod)]
+ allow(order).to receive(:ensure_available_shipping_rates).
+ and_return true
+ order.line_items << FactoryBot.create(:line_item)
+ end
+
+ context 'with the order in the cart state' do
+ before do
+ order.update_column(:state, 'cart')
+ allow(order).to receive_messages user: user
+ end
+
+ it 'assigns order' do
+ spree_post :update, state: 'address'
+ expect(assigns[:order]).not_to be_nil
+ end
+
+ it 'advances the state' do
+ spree_post_address
+ expect(order.reload.state).to eq('delivery')
+ end
+
+ it 'redirects the next state' do
+ spree_post_address
+ expect(response).to redirect_to spree.checkout_state_path('delivery')
+ end
+
+ context 'current_user respond to save address method' do
+ it 'calls persist order address on user' do
+ expect(user).to receive(:persist_order_address)
+ spree_post :update,
+ state: 'address',
+ order: {
+ bill_address_attributes: address_params,
+ use_billing: true
+ },
+ save_user_address: '1'
+ end
+ end
+
+ context 'current_user doesnt respond to persist_order_address' do
+ it 'doesnt raise any error' do
+ expect do
+ spree_post :update,
+ state: 'address',
+ order: {
+ bill_address_attributes: address_params,
+ use_billing: true
+ },
+ save_user_address: '1'
+ end.not_to raise_error
+ end
+ end
+ end
+
+ context 'with the order in the address state' do
+ before do
+ order.update_columns(ship_address_id: create(:address).id, state: 'address')
+ allow(order).to receive_messages user: user
+ end
+
+ context 'with a billing and shipping address' do
+ let(:bill_address_params) do
+ order.bill_address.attributes.except('created_at', 'updated_at')
+ end
+ let(:ship_address_params) do
+ order.ship_address.attributes.except('created_at', 'updated_at')
+ end
+ let(:update_params) do
+ {
+ state: 'address',
+ order: {
+ bill_address_attributes: bill_address_params,
+ ship_address_attributes: ship_address_params,
+ use_billing: false
+ }
+ }
+ end
+
+ before do
+ @expected_bill_address_id = order.bill_address.id
+ @expected_ship_address_id = order.ship_address.id
+
+ spree_post :update, update_params
+ order.reload
+ end
+
+ it 'updates the same billing and shipping address' do
+ expect(order.bill_address.id).to eq(@expected_bill_address_id)
+ expect(order.ship_address.id).to eq(@expected_ship_address_id)
+ end
+ end
+ end
+
+ context 'when in the confirm state' do
+ before do
+ allow(order).to receive_messages confirmation_required?: true
+ order.update_column(:state, 'confirm')
+ allow(order).to receive_messages user: user
+ # An order requires a payment to reach the complete state
+ # This is because payment_required? is true on the order
+ create(:payment, amount: order.total, order: order)
+ order.payments.reload
+ end
+
+ # This inadvertently is a regression test for #2694
+ it 'redirects to the order view' do
+ spree_post :update, state: 'confirm'
+ expect(response).to redirect_to spree.order_path(order)
+ end
+
+ it 'populates the flash message' do
+ spree_post :update, state: 'confirm'
+ expect(flash.notice).to eq(Spree.t(:order_processed_successfully))
+ end
+
+ it 'removes completed order from current_order' do
+ spree_post :update, { state: 'confirm' }, order_id: 'foofah'
+ expect(assigns(:current_order)).to be_nil
+ expect(assigns(:order)).to eql controller.current_order
+ end
+ end
+
+ # Regression test for #4190
+ context 'state_lock_version' do
+ let(:post_params) do
+ {
+ state: 'address',
+ order: {
+ bill_address_attributes: order.bill_address.attributes.except('created_at', 'updated_at'),
+ state_lock_version: 0,
+ use_billing: true
+ }
+ }
+ end
+
+ context 'correct' do
+ it 'properly updates and increment version' do
+ spree_post :update, post_params
+ expect(order.state_lock_version).to eq 1
+ end
+ end
+
+ context 'incorrect' do
+ before do
+ order.update_columns(state_lock_version: 1, state: 'address')
+ end
+
+ it 'order should receieve ensure_valid_order_version callback' do
+ expect_any_instance_of(described_class).to receive(:ensure_valid_state_lock_version)
+ spree_post :update, post_params
+ end
+
+ it 'order should receieve with_lock message' do
+ expect(order).to receive(:with_lock)
+ spree_post :update, post_params
+ end
+
+ it 'redirects back to current state' do
+ spree_post :update, post_params
+ expect(response).to redirect_to spree.checkout_state_path('address')
+ expect(flash[:error]).to eq 'The order has already been updated.'
+ end
+ end
+ end
+ end
+
+ context 'save unsuccessful' do
+ before do
+ allow(order).to receive_messages user: user
+ allow(order).to receive_messages update_attributes: false
+ end
+
+ it 'does not assign order' do
+ spree_post :update, state: 'address'
+ expect(assigns[:order]).not_to be_nil
+ end
+
+ it 'does not change the order state' do
+ spree_post :update, state: 'address'
+ end
+
+ it 'renders the edit template' do
+ spree_post :update, state: 'address'
+ expect(response).to render_template :edit
+ end
+
+ it 'renders order in payment state when payment fails' do
+ order.update_column(:state, 'confirm')
+ allow(controller).to receive(:insufficient_payment?).and_return(true)
+ spree_post :update, state: 'confirm'
+ expect(order.state).to eq('payment')
+ end
+ end
+
+ context 'when current_order is nil' do
+ before { allow(controller).to receive_messages current_order: nil }
+
+ it 'does not change the state if order is completed' do
+ expect(order).not_to receive(:update_attribute)
+ spree_post :update, state: 'confirm'
+ end
+
+ it 'redirects to the cart_path' do
+ spree_post :update, state: 'confirm'
+ expect(response).to redirect_to spree.cart_path
+ end
+ end
+
+ context 'Spree::Core::GatewayError' do
+ before do
+ allow(order).to receive_messages user: user
+ allow(order).to receive(:update_attributes).and_raise(Spree::Core::GatewayError.new('Invalid something or other.'))
+ spree_post :update, state: 'address'
+ end
+
+ it 'renders the edit template and display exception message' do
+ expect(response).to render_template :edit
+ expect(flash.now[:error]).to eq(Spree.t(:spree_gateway_error_flash_for_checkout))
+ expect(assigns(:order).errors[:base]).to include('Invalid something or other.')
+ end
+ end
+
+ context 'fails to transition from address' do
+ let(:order) do
+ FactoryBot.create(:order_with_line_items).tap do |order|
+ order.next!
+ expect(order.state).to eq('address')
+ end
+ end
+
+ before do
+ allow(controller).to receive_messages current_order: order
+ allow(controller).to receive_messages check_authorization: true
+ end
+
+ context 'when the country is not a shippable country' do
+ before do
+ order.ship_address.tap do |address|
+ # A different country which is not included in the list of shippable countries
+ address.country = FactoryBot.create(:country, name: 'Australia')
+ address.state_name = 'Victoria'
+ address.save
+ end
+ end
+
+ it 'due to no available shipping rates for any of the shipments' do
+ expect(order.shipments.count).to eq(1)
+ order.shipments.first.shipping_rates.delete_all
+
+ spree_put :update, state: order.state, order: {}
+ expect(flash[:error]).to eq(Spree.t(:items_cannot_be_shipped))
+ expect(response).to redirect_to(spree.checkout_state_path('address'))
+ end
+ end
+
+ context 'when the order is invalid' do
+ before do
+ allow(order).to receive_messages(update_from_params: true, next: nil)
+ order.errors.add(:base, 'Base error')
+ order.errors.add(:adjustments, 'error')
+ end
+
+ it 'due to the order having errors' do
+ spree_put :update, state: order.state, order: {}
+ expect(flash[:error]).to eql("Base error\nAdjustments error")
+ expect(response).to redirect_to(spree.checkout_state_path('address'))
+ end
+ end
+ end
+
+ context 'fails to transition from payment to complete' do
+ let(:order) do
+ FactoryBot.create(:order_with_line_items).tap do |order|
+ order.next! until order.state == 'payment'
+ # So that the confirmation step is skipped and we get straight to the action.
+ payment_method = FactoryBot.create(:simple_credit_card_payment_method)
+ payment = FactoryBot.create(:payment, payment_method: payment_method)
+ order.payments << payment
+ end
+ end
+
+ before do
+ allow(controller).to receive_messages current_order: order
+ allow(controller).to receive_messages check_authorization: true
+ end
+
+ it 'when GatewayError is raised' do
+ allow_any_instance_of(Spree::Payment).to receive(:process!).and_raise(Spree::Core::GatewayError.new(Spree.t(:payment_processing_failed)))
+ spree_put :update, state: order.state, order: {}
+ expect(flash[:error]).to eq(Spree.t(:payment_processing_failed))
+ end
+ end
+ end
+
+ context 'When last inventory item has been purchased' do
+ let(:product) { mock_model(Spree::Product, name: 'Amazing Object') }
+ let(:variant) { mock_model(Spree::Variant) }
+ let(:line_item) { mock_model Spree::LineItem, insufficient_stock?: true, amount: 0 }
+ let(:order) { create(:order_with_line_items) }
+
+ before do
+ allow(order).to receive_messages(insufficient_stock_lines: [line_item], state: 'payment')
+
+ configure_spree_preferences do |config|
+ config.track_inventory_levels = true
+ end
+ end
+
+ context 'and back orders are not allowed' do
+ before do
+ spree_post :update, state: 'payment'
+ end
+
+ it 'redirects to cart' do
+ expect(response).to redirect_to spree.cart_path
+ end
+
+ it 'sets flash message for no inventory' do
+ expect(flash[:error]).to eq(
+ Spree.t(:inventory_error_flash_for_insufficient_quantity, names: "'#{product.name}'")
+ )
+ end
+ end
+ end
+
+ context "order doesn't have a delivery step" do
+ before do
+ allow(order).to receive_messages(checkout_steps: ['cart', 'address', 'payment'])
+ allow(order).to receive_messages state: 'address'
+ allow(controller).to receive_messages check_authorization: true
+ end
+
+ it "doesn't set shipping address on the order" do
+ expect(order).not_to receive(:ship_address=)
+ spree_post :update, state: order.state
+ end
+
+ it "doesn't remove unshippable items before payment" do
+ expect { spree_post :update, state: 'payment' }.
+ not_to change(order, :line_items)
+ end
+ end
+
+ it 'does remove unshippable items before payment' do
+ allow(order).to receive_messages payment_required?: true
+ allow(controller).to receive_messages check_authorization: true
+
+ expect { spree_post :update, state: 'payment' }.
+ to change { order.reload.line_items.length }
+ end
+
+ context 'in the payment step' do
+ let(:order) { OrderWalkthrough.up_to(:payment) }
+ let(:payment_method_id) { Spree::PaymentMethod.first.id }
+
+ before do
+ expect(order.state).to eq 'payment'
+ allow(order).to receive_messages user: user
+ allow(order).to receive_messages confirmation_required?: true
+ end
+
+ it 'does not advance the order extra even when called twice' do
+ spree_put :update, state: 'payment',
+ order: { payments_attributes: [{ payment_method_id: payment_method_id }] }
+ order.reload
+ expect(order.state).to eq 'confirm'
+ spree_put :update, state: 'payment',
+ order: { payments_attributes: [{ payment_method_id: payment_method_id }] }
+ order.reload
+ expect(order.state).to eq 'confirm'
+ end
+
+ context 'with store credits payment' do
+ let(:user) { create(:user) }
+ let(:credit_amount) { order.total + 1.00 }
+ let(:put_attrs) do
+ {
+ state: 'payment',
+ apply_store_credit: 'Apply Store Credit',
+ order: {
+ payments_attributes: [{ payment_method_id: payment_method_id }]
+ }
+ }
+ end
+
+ before do
+ create(:store_credit_payment_method)
+ create(:store_credit, user: user, amount: credit_amount)
+ end
+
+ def expect_one_store_credit_payment(order, amount)
+ expect(order.payments.count).to eq 1
+ expect(order.payments.first.source).to be_a Spree::StoreCredit
+ expect(order.payments.first.amount).to eq amount
+ end
+
+ it 'can fully pay with store credits while removing other payment attributes' do
+ spree_put :update, put_attrs
+
+ order.reload
+ expect(order.state).to eq 'confirm'
+ expect_one_store_credit_payment(order, order.total)
+ end
+
+ it 'can fully pay with store credits while removing an existing card' do
+ credit_card = create(:credit_card, user: user, payment_method: Spree::PaymentMethod.first)
+ put_attrs[:order][:existing_card] = credit_card.id
+ spree_put :update, put_attrs
+
+ order.reload
+ expect(order.state).to eq 'confirm'
+ expect_one_store_credit_payment(order, order.total)
+ end
+
+ context 'partial payment' do
+ let(:credit_amount) { order.total - 1.00 }
+
+ it 'returns to payment for partial store credit' do
+ spree_put :update, put_attrs
+
+ order.reload
+ expect(order.state).to eq 'payment'
+ expect_one_store_credit_payment(order, credit_amount)
+ end
+ end
+ end
+
+ context 'remove store credits payment' do
+ let(:user) { create(:user) }
+ let(:credit_amount) { order.total - 1.00 }
+ let(:put_attrs) do
+ {
+ state: 'payment',
+ remove_store_credit: 'Remove Store Credit',
+ order: {
+ payments_attributes: [{ payment_method_id: payment_method_id }]
+ }
+ }
+ end
+
+ before do
+ create(:store_credit_payment_method)
+ create(:store_credit, user: user, amount: credit_amount)
+ Spree::Checkout::AddStoreCredit.call(order: order)
+ end
+
+ def expect_invalid_store_credit_payment(order)
+ expect(order.payments.store_credits.with_state(:invalid).count).to eq 1
+ expect(order.payments.store_credits.with_state(:invalid).first.source).to be_a Spree::StoreCredit
+ end
+
+ it 'can fully pay with store credits while removing other payment attributes' do
+ spree_put :update, put_attrs
+
+ order.reload
+ expect(order.state).to eq 'payment'
+ expect_invalid_store_credit_payment(order)
+ end
+ end
+ end
+end
diff --git a/frontend/spec/controllers/spree/checkout_controller_with_views_spec.rb b/frontend/spec/controllers/spree/checkout_controller_with_views_spec.rb
new file mode 100644
index 00000000000..b2303c3d0a4
--- /dev/null
+++ b/frontend/spec/controllers/spree/checkout_controller_with_views_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+# This spec is useful for when we just want to make sure a view is rendering correctly
+# Walking through the entire checkout process is rather tedious, don't you think?
+describe Spree::CheckoutController, type: :controller do
+ render_views
+ let(:token) { 'some_token' }
+ let(:user) { stub_model(Spree::LegacyUser) }
+
+ before do
+ allow(controller).to receive_messages try_spree_current_user: user
+ end
+
+ # Regression test for #3246
+ context 'when using GBP' do
+ before do
+ Spree::Config[:currency] = 'GBP'
+ FactoryBot.create(:store, default_currency: 'GBP')
+ end
+
+ context 'when order is in delivery' do
+ before do
+ # Using a let block won't acknowledge the currency setting
+ # Therefore we just do it like this...
+ order = OrderWalkthrough.up_to(:delivery)
+ allow(controller).to receive_messages current_order: order
+ end
+
+ it 'displays rate cost in correct currency' do
+ spree_get :edit
+ html = Nokogiri::HTML(response.body)
+ expect(html.css('.rate-cost').text).to eq '£10.00'
+ end
+ end
+ end
+end
diff --git a/frontend/spec/controllers/spree/content_controller_spec.rb b/frontend/spec/controllers/spree/content_controller_spec.rb
new file mode 100644
index 00000000000..ebd45910df0
--- /dev/null
+++ b/frontend/spec/controllers/spree/content_controller_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+describe Spree::ContentController, type: :controller do
+ it 'displays CVV page' do
+ spree_get :cvv
+ expect(response.response_code).to eq(200)
+ end
+end
diff --git a/frontend/spec/controllers/spree/current_order_tracking_spec.rb b/frontend/spec/controllers/spree/current_order_tracking_spec.rb
new file mode 100644
index 00000000000..750a6cb7161
--- /dev/null
+++ b/frontend/spec/controllers/spree/current_order_tracking_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'current order tracking', type: :controller do # rubocop:disable RSpec/MultipleDescribes
+ let(:user) { create(:user) }
+
+ before { create(:order) }
+
+ controller(Spree::StoreController) do
+ def index
+ head :ok
+ end
+ end
+
+ it 'automatically tracks who the order was created by & IP address' do
+ allow(controller).to receive_messages(try_spree_current_user: user)
+ get :index
+ expect(controller.current_order(create_order_if_necessary: true).created_by).to eq controller.try_spree_current_user
+ expect(controller.current_order.last_ip_address).to eq '0.0.0.0'
+ end
+
+ context 'current order creation' do
+ before { allow(controller).to receive_messages(try_spree_current_user: user) }
+
+ it "doesn't create a new order out of the blue" do
+ expect do
+ spree_get :index
+ end.not_to change { Spree::Order.count }
+ end
+ end
+end
+
+describe Spree::OrdersController, type: :controller do
+ let(:user) { create(:user) }
+
+ before { allow(controller).to receive_messages(try_spree_current_user: user) }
+
+ describe Spree::OrdersController do
+ it "doesn't create a new order out of the blue" do
+ expect do
+ spree_get :edit
+ end.not_to change { Spree::Order.count }
+ end
+ end
+end
diff --git a/frontend/spec/controllers/spree/home_controller_spec.rb b/frontend/spec/controllers/spree/home_controller_spec.rb
new file mode 100644
index 00000000000..3100ef9f2b2
--- /dev/null
+++ b/frontend/spec/controllers/spree/home_controller_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Spree::HomeController, type: :controller do
+ it 'provides current user to the searcher class' do
+ user = mock_model(Spree.user_class, last_incomplete_spree_order: nil, spree_api_key: 'fake')
+ allow(controller).to receive_messages try_spree_current_user: user
+ expect_any_instance_of(Spree::Config.searcher_class).to receive(:current_user=).with(user)
+ spree_get :index
+ expect(response.status).to eq(200)
+ end
+
+ context 'layout' do
+ it 'renders default layout' do
+ spree_get :index
+ expect(response).to render_template(layout: 'spree/layouts/spree_application')
+ end
+
+ context 'different layout specified in config' do
+ before { Spree::Config.layout = 'layouts/application' }
+
+ it 'renders specified layout' do
+ spree_get :index
+ expect(response).to render_template(layout: 'layouts/application')
+ end
+ end
+ end
+
+ context 'index products' do
+ it 'calls includes when the retrieved_products object responds to it' do
+ searcher = double('Searcher')
+ allow(controller).to receive_messages build_searcher: searcher
+ expect(searcher).to receive_message_chain('retrieve_products.includes')
+
+ spree_get :index
+ end
+
+ it "does not call includes when it's not available" do
+ searcher = double('Searcher')
+ allow(controller).to receive_messages build_searcher: searcher
+ allow(searcher).to receive(:retrieve_products).and_return([])
+
+ spree_get :index
+
+ expect(assigns(:products)).to eq([])
+ end
+ end
+end
diff --git a/frontend/spec/controllers/spree/orders_controller_ability_spec.rb b/frontend/spec/controllers/spree/orders_controller_ability_spec.rb
new file mode 100644
index 00000000000..7e9b2953e03
--- /dev/null
+++ b/frontend/spec/controllers/spree/orders_controller_ability_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+module Spree
+ describe OrdersController, type: :controller do
+ let(:user) { create(:user) }
+ let(:guest_user) { create(:user) }
+ let(:order) { Spree::Order.create }
+
+ context 'when an order exists in the cookies.signed' do
+ let(:token) { 'some_token' }
+ let(:specified_order) { create(:order) }
+ let!(:variant) { create(:variant) }
+
+ before do
+ cookies.signed[:token] = token
+ allow(controller).to receive_messages current_order: order
+ allow(controller).to receive_messages spree_current_user: user
+ end
+
+ context '#populate' do
+ it 'checks if user is authorized for :edit' do
+ expect(controller).to receive(:authorize!).with(:edit, order, token)
+ spree_post :populate, variant_id: variant.id
+ end
+ it 'checks against the specified order' do
+ expect(controller).to receive(:authorize!).with(:edit, specified_order, token)
+ spree_post :populate, id: specified_order.number, variant_id: variant.id
+ end
+ end
+
+ context '#edit' do
+ it 'checks if user is authorized for :edit' do
+ expect(controller).to receive(:authorize!).with(:edit, order, token)
+ spree_get :edit
+ end
+ it 'checks against the specified order' do
+ expect(controller).to receive(:authorize!).with(:edit, specified_order, token)
+ spree_get :edit, id: specified_order.number
+ end
+ end
+
+ context '#update' do
+ it 'checks if user is authorized for :edit' do
+ allow(order).to receive :update_attributes
+ expect(controller).to receive(:authorize!).with(:edit, order, token)
+ spree_post :update, order: { email: 'foo@bar.com' }
+ end
+ it 'checks against the specified order' do
+ allow(order).to receive :update_attributes
+ expect(controller).to receive(:authorize!).with(:edit, specified_order, token)
+ spree_post :update, order: { email: 'foo@bar.com' }, id: specified_order.number
+ end
+ end
+
+ context '#empty' do
+ it 'checks if user is authorized for :edit' do
+ expect(controller).to receive(:authorize!).with(:edit, order, token)
+ spree_post :empty
+ end
+ it 'checks against the specified order' do
+ expect(controller).to receive(:authorize!).with(:edit, specified_order, token)
+ spree_post :empty, id: specified_order.number
+ end
+ end
+
+ context '#show' do
+ it 'checks against the specified order' do
+ expect(controller).to receive(:authorize!).with(:show, specified_order, token)
+ spree_get :show, id: specified_order.number
+ end
+ end
+ end
+
+ context 'when no authenticated user' do
+ let(:order) { create(:order, number: 'R123') }
+
+ context '#show' do
+ context 'when token correct' do
+ before { cookies.signed[:token] = order.token }
+
+ it 'displays the page' do
+ expect(controller).to receive(:authorize!).with(:show, order, order.token)
+ spree_get :show, id: 'R123'
+ expect(response.code).to eq('200')
+ end
+ end
+
+ context 'when token not present' do
+ it 'raises ActiveRecord::RecordNotFound' do
+ expect { spree_get :show, id: 'R123' }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/frontend/spec/controllers/spree/orders_controller_spec.rb b/frontend/spec/controllers/spree/orders_controller_spec.rb
new file mode 100644
index 00000000000..7638d03e66f
--- /dev/null
+++ b/frontend/spec/controllers/spree/orders_controller_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe Spree::OrdersController, type: :controller do
+ let(:user) { create(:user) }
+
+ context 'Order model mock' do
+ let(:order) do
+ Spree::Order.create!
+ end
+ let(:variant) { create(:variant) }
+
+ before do
+ allow(controller).to receive_messages(try_spree_current_user: user)
+ end
+
+ context '#populate' do
+ it 'creates a new order when none specified' do
+ spree_post :populate, variant_id: variant.id
+ expect(cookies.signed[:token]).not_to be_blank
+ expect(Spree::Order.find_by(token: cookies.signed[:token])).to be_persisted
+ end
+
+ context 'with Variant' do
+ it 'handles population' do
+ expect do
+ spree_post :populate, variant_id: variant.id, quantity: 5
+ end.to change { user.orders.count }.by(1)
+ order = user.orders.last
+ expect(response).to redirect_to spree.cart_path(variant_id: variant.id)
+ expect(order.line_items.size).to eq(1)
+ line_item = order.line_items.first
+ expect(line_item.variant_id).to eq(variant.id)
+ expect(line_item.quantity).to eq(5)
+ end
+
+ it 'shows an error when population fails' do
+ request.env['HTTP_REFERER'] = '/dummy_redirect'
+ allow_any_instance_of(Spree::LineItem).to(
+ receive(:valid?).and_return(false)
+ )
+ allow_any_instance_of(Spree::LineItem).to(
+ receive_message_chain(:errors, :full_messages).
+ and_return(['Order population failed'])
+ )
+
+ spree_post :populate, variant_id: variant.id, quantity: 5
+
+ expect(response).to redirect_to('/dummy_redirect')
+ expect(flash[:error]).to eq('Order population failed')
+ end
+
+ it 'shows an error when quantity is invalid' do
+ request.env['HTTP_REFERER'] = '/dummy_redirect'
+
+ spree_post(
+ :populate,
+ variant_id: variant.id, quantity: -1
+ )
+
+ expect(response).to redirect_to('/dummy_redirect')
+ expect(flash[:error]).to eq(
+ Spree.t(:please_enter_reasonable_quantity)
+ )
+ end
+ end
+ end
+
+ context '#update' do
+ context 'with authorization' do
+ before do
+ allow(controller).to receive :check_authorization
+ allow(controller).to receive_messages current_order: order
+ end
+
+ it 'renders the edit view (on failure)' do
+ # email validation is only after address state
+ order.update_column(:state, 'delivery')
+ spree_put :update, { order: { email: '' } }, order_id: order.id
+ expect(response).to render_template :edit
+ end
+
+ it 'redirects to cart path (on success)' do
+ allow(order).to receive(:update_attributes).and_return true
+ spree_put :update, {}, order_id: 1
+ expect(response).to redirect_to(spree.cart_path)
+ end
+ end
+ end
+
+ context '#empty' do
+ before do
+ allow(controller).to receive :check_authorization
+ end
+
+ it 'destroys line items in the current order' do
+ allow(controller).to receive(:current_order).and_return(order)
+ expect(order).to receive(:empty!)
+ spree_put :empty
+ expect(response).to redirect_to(spree.cart_path)
+ end
+ end
+
+ # Regression test for #2750
+ context '#update' do
+ before do
+ allow(user).to receive :last_incomplete_spree_order
+ allow(controller).to receive :set_current_order
+ end
+
+ it 'cannot update a blank order' do
+ spree_put :update, order: { email: 'foo' }
+ expect(flash[:error]).to eq(Spree.t(:order_not_found))
+ expect(response).to redirect_to(spree.root_path)
+ end
+ end
+ end
+
+ context 'line items quantity is 0' do
+ let(:order) { Spree::Order.create }
+ let(:variant) { create(:variant) }
+ let!(:line_item) { Spree::Cart::AddItem.call(order: order, variant: variant).value }
+
+ before do
+ allow(controller).to receive(:check_authorization)
+ allow(controller).to receive_messages(current_order: order)
+ end
+
+ it 'removes line items on update' do
+ expect(order.line_items.count).to eq 1
+ spree_put :update, order: { line_items_attributes: { '0' => { id: line_item.id, quantity: 0 } } }
+ expect(order.reload.line_items.count).to eq 0
+ end
+ end
+end
diff --git a/frontend/spec/controllers/spree/orders_controller_transitions_spec.rb b/frontend/spec/controllers/spree/orders_controller_transitions_spec.rb
new file mode 100644
index 00000000000..4f48b31e4a4
--- /dev/null
+++ b/frontend/spec/controllers/spree/orders_controller_transitions_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+Spree::Order.class_eval do
+ attr_accessor :did_transition
+end
+
+module Spree
+ describe OrdersController, type: :controller do
+ # Regression test for #2004
+ context 'with a transition callback on first state' do
+ let(:order) { Spree::Order.new }
+
+ before do
+ allow(controller).to receive_messages current_order: order
+ expect(controller).to receive(:authorize!).at_least(:once).and_return(true)
+
+ first_state, = Spree::Order.checkout_steps.first
+ Spree::Order.state_machine.after_transition to: first_state do |order|
+ order.did_transition = true
+ end
+ end
+
+ it 'correctly calls the transition callback' do
+ expect(order.did_transition).to be_nil
+ order.line_items << FactoryBot.create(:line_item)
+ spree_put :update, { checkout: 'checkout' }, order_id: 1
+ expect(order.did_transition).to be true
+ end
+ end
+ end
+end
diff --git a/frontend/spec/controllers/spree/products_controller_spec.rb b/frontend/spec/controllers/spree/products_controller_spec.rb
new file mode 100644
index 00000000000..a7a376f7de2
--- /dev/null
+++ b/frontend/spec/controllers/spree/products_controller_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Spree::ProductsController, type: :controller do
+ let!(:product) { create(:product, available_on: 1.year.from_now) }
+ let(:taxon) { create(:taxon) }
+
+ # Regression test for #1390
+ it 'allows admins to view non-active products' do
+ allow(controller).to receive_messages spree_current_user: mock_model(Spree.user_class, has_spree_role?: true, last_incomplete_spree_order: nil, spree_api_key: 'fake')
+ spree_get :show, id: product.to_param
+ expect(response.status).to eq(200)
+ end
+
+ it 'cannot view non-active products' do
+ expect { spree_get :show, id: product.to_param }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'provides the current user to the searcher class' do
+ user = mock_model(Spree.user_class, last_incomplete_spree_order: nil, spree_api_key: 'fake')
+ allow(controller).to receive_messages spree_current_user: user
+ expect_any_instance_of(Spree::Config.searcher_class).to receive(:current_user=).with(user)
+ spree_get :index
+ expect(response.status).to eq(200)
+ end
+
+ # Regression test for #2249
+ it "doesn't error when given an invalid referer" do
+ current_user = mock_model(Spree.user_class, has_spree_role?: true, last_incomplete_spree_order: nil, generate_spree_api_key!: nil)
+ allow(controller).to receive_messages spree_current_user: current_user
+ request.env['HTTP_REFERER'] = 'not|a$url'
+
+ # Previously a URI::InvalidURIError exception was being thrown
+ expect { spree_get :show, id: product.to_param }.not_to raise_error
+ end
+
+ context 'with history slugs present' do
+ let!(:product) { create(:product, available_on: 1.day.ago) }
+
+ it 'will redirect with a 301 with legacy url used' do
+ legacy_params = product.to_param
+ product.name = product.name + ' Brand New'
+ product.slug = nil
+ product.save!
+ spree_get :show, id: legacy_params
+ expect(response.status).to eq(301)
+ end
+
+ it 'will redirect with a 301 with id used' do
+ product.name = product.name + ' Brand New'
+ product.slug = nil
+ product.save!
+ spree_get :show, id: product.id
+ expect(response.status).to eq(301)
+ end
+
+ it 'will keep url params on legacy url redirect' do
+ legacy_params = product.to_param
+ product.name = product.name + ' Brand New'
+ product.slug = nil
+ product.save!
+ spree_get :show, id: legacy_params, taxon_id: taxon.id
+ expect(response.status).to eq(301)
+ expect(response.header['Location']).to include("taxon_id=#{taxon.id}")
+ end
+ end
+
+ context 'index products' do
+ it 'calls includes when the retrieved_products object responds to it' do
+ searcher = double('Searcher')
+ allow(controller).to receive_messages build_searcher: searcher
+ expect(searcher).to receive_message_chain('retrieve_products.includes')
+
+ spree_get :index
+ end
+
+ it "does not call includes when it's not available" do
+ searcher = double('Searcher')
+ allow(controller).to receive_messages build_searcher: searcher
+ allow(searcher).to receive(:retrieve_products).and_return([])
+
+ spree_get :index
+
+ expect(assigns(:products)).to eq([])
+ end
+ end
+end
diff --git a/frontend/spec/controllers/spree/taxons_controller_spec.rb b/frontend/spec/controllers/spree/taxons_controller_spec.rb
new file mode 100644
index 00000000000..8fe675ea09e
--- /dev/null
+++ b/frontend/spec/controllers/spree/taxons_controller_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Spree::TaxonsController, type: :controller do
+ it 'provides the current user to the searcher class' do
+ taxon = create(:taxon, permalink: 'test')
+ user = mock_model(Spree.user_class, last_incomplete_spree_order: nil, spree_api_key: 'fake')
+ allow(controller).to receive_messages spree_current_user: user
+ expect_any_instance_of(Spree::Config.searcher_class).to receive(:current_user=).with(user)
+ spree_get :show, id: taxon.permalink
+ expect(response.status).to eq(200)
+ end
+end
diff --git a/frontend/spec/features/address_spec.rb b/frontend/spec/features/address_spec.rb
new file mode 100644
index 00000000000..a347c540f3e
--- /dev/null
+++ b/frontend/spec/features/address_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe 'Address', type: :feature, inaccessible: true do
+ stub_authorization!
+
+ after do
+ Capybara.ignore_hidden_elements = true
+ end
+
+ before do
+ create(:product, name: 'RoR Mug')
+ create(:order_with_totals, state: 'cart')
+
+ Capybara.ignore_hidden_elements = false
+
+ visit spree.root_path
+
+ add_to_cart('RoR Mug')
+
+ address = 'order_bill_address_attributes'
+ @country_css = "#{address}_country_id"
+ @state_select_css = "##{address}_state_id"
+ @state_name_css = "##{address}_state_name"
+ end
+
+ context 'country requires state', js: true do
+ let!(:canada) { create(:country, name: 'Canada', states_required: true, iso: 'CA') }
+ let!(:uk) { create(:country, name: 'United Kingdom', states_required: true, iso: 'UK') }
+
+ before { Spree::Config[:default_country_id] = uk.id }
+
+ context 'but has no state' do
+ it 'shows the state input field' do
+ click_button 'Checkout'
+
+ select canada.name, from: @country_css
+ expect(page).to have_selector(@state_select_css, visible: false)
+ expect(page).to have_selector(@state_name_css, visible: true)
+ expect(find(@state_name_css)['class']).not_to match(/hidden/)
+ expect(find(@state_name_css)['class']).to match(/required/)
+ expect(find(@state_select_css)['class']).not_to match(/required/)
+ expect(page).not_to have_selector("input#{@state_name_css}[disabled]")
+ end
+ end
+
+ context 'and has state' do
+ before { create(:state, name: 'Ontario', country: canada) }
+
+ it 'shows the state collection selection' do
+ click_button 'Checkout'
+
+ select canada.name, from: @country_css
+ expect(page).to have_selector(@state_select_css, visible: true)
+ expect(page).to have_selector(@state_name_css, visible: false)
+ expect(find(@state_select_css)['class']).to match(/required/)
+ expect(find(@state_select_css)['class']).not_to match(/hidden/)
+ expect(find(@state_name_css)['class']).not_to match(/required/)
+ end
+ end
+
+ context 'user changes to country without states required' do
+ let!(:france) { create(:country, name: 'France', states_required: false, iso: 'FRA') }
+
+ it 'clears the state name' do
+ skip 'This is failing on the CI server, but not when you run the tests manually... It also does not fail locally on a machine.'
+ click_button 'Checkout'
+ select canada.name, from: @country_css
+ page.find(@state_name_css).set('Toscana')
+
+ select france.name, from: @country_css
+ expect(page.find(@state_name_css)).to have_content('')
+ until page.evaluate_script('$.active').to_i.zero?
+ expect(find(@state_name_css)['class']).not_to match(/hidden/)
+ expect(find(@state_name_css)['class']).not_to match(/required/)
+ expect(find(@state_select_css)['class']).not_to match(/required/)
+ end
+ end
+ end
+ end
+
+ context 'country does not require state', js: true do
+ let!(:france) { create(:country, name: 'France', states_required: false, iso: 'FRA') }
+
+ it 'shows a disabled state input field' do
+ click_button 'Checkout'
+
+ select france.name, from: @country_css
+ expect(page).to have_selector(@state_select_css, visible: false)
+ expect(page).to have_selector(@state_name_css, visible: false)
+ end
+ end
+end
diff --git a/frontend/spec/features/automatic_promotion_adjustments_spec.rb b/frontend/spec/features/automatic_promotion_adjustments_spec.rb
new file mode 100644
index 00000000000..12eb18ead89
--- /dev/null
+++ b/frontend/spec/features/automatic_promotion_adjustments_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe 'Automatic promotions', type: :feature, js: true do
+ let!(:country) { create(:country, name: 'United States of America', states_required: true) }
+ let!(:product) { create(:product, name: 'RoR Mug', price: 20) }
+
+ before do
+ create(:state, name: 'Alabama', country: country)
+ create(:zone)
+ create(:shipping_method)
+ create(:check_payment_method)
+
+ promotion = Spree::Promotion.create!(name: '$10 off when you spend more than $100')
+
+ calculator = Spree::Calculator::FlatRate.new
+ calculator.preferred_amount = 10
+
+ rule = Spree::Promotion::Rules::ItemTotal.create
+ rule.preferred_amount_min = 100
+ rule.save
+
+ promotion.rules << rule
+
+ action = Spree::Promotion::Actions::CreateAdjustment.create
+ action.calculator = calculator
+ action.save
+
+ promotion.actions << action
+ end
+
+ context 'on the cart page' do
+ before do
+ add_to_cart(product.name)
+ end
+
+ it 'automatically applies the promotion once the order crosses the threshold' do
+ fill_in 'order_line_items_attributes_0_quantity', with: 10
+ click_button 'Update'
+ expect(page).to have_content('Promotion ($10 off when you spend more than $100) -$10.00')
+ fill_in 'order_line_items_attributes_0_quantity', with: 1
+ click_button 'Update'
+ expect(page).not_to have_content('Promotion ($10 off when you spend more than $100) -$10.00')
+ end
+ end
+end
diff --git a/frontend/spec/features/caching/products_spec.rb b/frontend/spec/features/caching/products_spec.rb
new file mode 100644
index 00000000000..cf568d036b6
--- /dev/null
+++ b/frontend/spec/features/caching/products_spec.rb
@@ -0,0 +1,59 @@
+# require 'spec_helper'
+
+# describe 'products', type: :feature, caching: true do
+# let!(:product) { create(:product) }
+# let!(:product2) { create(:product) }
+# let!(:taxonomy) { create(:taxonomy) }
+
+# after { Timecop.return }
+
+# before do
+# create(:taxon, taxonomy: taxonomy)
+
+# Timecop.scale(1000)
+
+# product2.update_column(:updated_at, 1.day.ago)
+# # warm up the cache
+# visit spree.root_path
+# assert_written_to_cache("views/en/USD/spree/products/all--#{product.updated_at.utc.to_s(:number)}")
+# assert_written_to_cache("views/en/USD/spree/products/#{product.id}-#{product.updated_at.utc.to_s(:number)}")
+# assert_written_to_cache("views/en/spree/taxonomies/#{taxonomy.id}")
+
+# clear_cache_events
+# end
+
+# it 'reads from cache upon a second viewing' do
+# visit spree.root_path
+# expect(cache_writes.count).to eq(0)
+# end
+
+# it 'busts the cache when a product is updated' do
+# product.update_column(:updated_at, 1.day.from_now)
+# visit spree.root_path
+# assert_written_to_cache("views/en/USD/spree/products/all--#{product.updated_at.utc.to_s(:number)}")
+# assert_written_to_cache("views/en/USD/spree/products/#{product.id}-#{product.updated_at.utc.to_s(:number)}")
+# expect(cache_writes.count).to eq(2)
+# end
+
+# it 'busts the cache when all products are deleted' do
+# product.destroy
+# product2.destroy
+# visit spree.root_path
+# assert_written_to_cache("views/en/USD/spree/products/all--#{Date.today.to_s(:number)}-0")
+# expect(cache_writes.count).to eq(1)
+# end
+
+# it 'busts the cache when the newest product is deleted' do
+# product.destroy
+# visit spree.root_path
+# assert_written_to_cache("views/en/USD/spree/products/all--#{product2.updated_at.utc.to_s(:number)}")
+# expect(cache_writes.count).to eq(1)
+# end
+
+# it 'busts the cache when an older product is deleted' do
+# product2.destroy
+# visit spree.root_path
+# assert_written_to_cache("views/en/USD/spree/products/all--#{product.updated_at.utc.to_s(:number)}")
+# expect(cache_writes.count).to eq(1)
+# end
+# end
diff --git a/frontend/spec/features/caching/taxons_spec.rb b/frontend/spec/features/caching/taxons_spec.rb
new file mode 100644
index 00000000000..35a8e14a462
--- /dev/null
+++ b/frontend/spec/features/caching/taxons_spec.rb
@@ -0,0 +1,21 @@
+# require 'spec_helper'
+
+# describe 'taxons', type: :feature, caching: true do
+# let!(:taxonomy) { create(:taxonomy) }
+
+# before do
+# create(:taxon, taxonomy: taxonomy)
+# # warm up the cache
+# visit spree.root_path
+# assert_written_to_cache("views/en/spree/taxonomies/#{taxonomy.id}")
+
+# clear_cache_events
+# end
+
+# it 'busts the cache when max_level_in_taxons_menu conf changes' do
+# Spree::Config[:max_level_in_taxons_menu] = 5
+# visit spree.root_path
+# assert_written_to_cache("views/en/spree/taxonomies/#{taxonomy.id}")
+# expect(cache_writes.count).to eq(1)
+# end
+# end
diff --git a/frontend/spec/features/cart_spec.rb b/frontend/spec/features/cart_spec.rb
new file mode 100644
index 00000000000..4907b4ca55b
--- /dev/null
+++ b/frontend/spec/features/cart_spec.rb
@@ -0,0 +1,131 @@
+require 'spec_helper'
+
+describe 'Cart', type: :feature, inaccessible: true, js: true do
+ before { Timecop.scale(100) }
+
+ after { Timecop.return }
+
+ let!(:variant) { create(:variant) }
+ let!(:product) { variant.product }
+
+ def add_mug_to_cart
+ add_to_cart(product.name)
+ end
+
+ it 'shows cart icon on non-cart pages' do
+ visit spree.root_path
+ expect(page).to have_selector('li#link-to-cart a', visible: true)
+ end
+
+ it 'prevents double clicking the remove button on cart' do
+ add_mug_to_cart
+ # prevent form submit to verify button is disabled
+ page.execute_script("$('#update-cart').submit(function(){return false;})")
+
+ expect(page).not_to have_selector('button#update-button[disabled]')
+ page.find(:css, '.delete span').click
+ expect(page).to have_selector('button#update-button[disabled]')
+ end
+
+ # Regression test for #2006
+ it "does not error out with a 404 when GET'ing to /orders/populate" do
+ visit '/orders/populate'
+ within('.alert-error') do
+ expect(page).to have_content(Spree.t(:populate_get_error))
+ end
+ end
+
+ it 'allows you to remove an item from the cart' do
+ add_mug_to_cart
+ line_item = Spree::LineItem.first!
+ within('#line_items') do
+ click_link "delete_line_item_#{line_item.id}"
+ end
+
+ expect(page).not_to have_content('Line items quantity must be an integer')
+ expect(page).not_to have_content(product.name)
+ expect(page).to have_content('Your cart is empty')
+
+ within '#link-to-cart' do
+ expect(page).to have_content('Empty')
+ end
+ end
+
+ it 'allows you to empty the cart' do
+ add_mug_to_cart
+ expect(page).to have_content(product.name)
+ click_on 'Empty Cart'
+ expect(page).to have_content('Your cart is empty')
+
+ within '#link-to-cart' do
+ expect(page).to have_content('Empty')
+ end
+ end
+
+ # regression for #2276
+ context 'product contains variants but no option values' do
+ before { variant.option_values.destroy_all }
+
+ it 'still adds product to cart' do
+ add_mug_to_cart
+ visit spree.cart_path
+ expect(page).to have_content(product.name)
+ end
+ end
+
+ it "has a surrounding element with data-hook='cart_container'" do
+ visit spree.cart_path
+ expect(page).to have_selector("div[data-hook='cart_container']")
+ end
+
+ describe 'add promotion coupon on cart page' do
+ let!(:promotion) { Spree::Promotion.create(name: 'Huhuhu', code: 'huhu') }
+ let!(:calculator) { Spree::Calculator::FlatPercentItemTotal.create(preferred_flat_percent: '10') }
+ let!(:action) { Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: calculator) }
+
+ before do
+ promotion.actions << action
+ add_mug_to_cart
+ expect(page).to have_current_path(spree.cart_path(variant_id: variant))
+ end
+
+ def apply_coupon(code)
+ fill_in 'Coupon Code', with: code
+ click_on 'Update'
+ end
+
+ context 'valid coupon' do
+ before { apply_coupon(promotion.code) }
+
+ context 'for the first time' do
+ it 'makes sure payment reflects order total with discounts' do
+ expect(page).to have_content(promotion.name)
+ end
+ end
+
+ context 'same coupon for the second time' do
+ before { apply_coupon(promotion.code) }
+
+ it 'reflects an error that coupon already applied' do
+ apply_coupon(promotion.code)
+ expect(page).to have_content(Spree.t(:coupon_code_already_applied))
+ expect(page).to have_content(promotion.name)
+ end
+ end
+ end
+
+ context 'invalid coupon' do
+ it 'doesnt create a payment record' do
+ apply_coupon('invalid')
+ expect(page).to have_content(Spree.t(:coupon_code_not_found))
+ end
+ end
+
+ context "doesn't fill in coupon code input" do
+ it 'advances just fine' do
+ click_on 'Update'
+ expect(page).to have_current_path(spree.cart_path)
+ end
+ end
+ end
+end
diff --git a/frontend/spec/features/checkout_spec.rb b/frontend/spec/features/checkout_spec.rb
new file mode 100644
index 00000000000..9003fe04f06
--- /dev/null
+++ b/frontend/spec/features/checkout_spec.rb
@@ -0,0 +1,749 @@
+require 'spec_helper'
+
+describe 'Checkout', type: :feature, inaccessible: true, js: true do
+ include_context 'checkout setup'
+
+ let(:country) { create(:country, name: 'United States of America', iso_name: 'UNITED STATES') }
+ let(:state) { create(:state, name: 'Alabama', abbr: 'AL', country: country) }
+
+ context 'visitor makes checkout as guest without registration' do
+ before do
+ stock_location.stock_items.update_all(count_on_hand: 1)
+ end
+
+ context 'defaults to use billing address' do
+ before do
+ add_mug_to_cart
+ Spree::Order.last.update_column(:email, 'test@example.com')
+ click_button 'Checkout'
+ end
+
+ it 'defaults checkbox to checked' do
+ expect(find('input#order_use_billing')).to be_checked
+ end
+
+ it 'remains checked when used and visitor steps back to address step' do
+ fill_in_address
+ expect(find('input#order_use_billing')).to be_checked
+ end
+ end
+
+ # Regression test for #4079
+ context 'persists state when on address page' do
+ before do
+ add_mug_to_cart
+ click_button 'Checkout'
+ end
+
+ specify do
+ expect(Spree::Order.count).to eq 1
+ expect(Spree::Order.last.state).to eq 'address'
+ end
+ end
+
+ # Regression test for #1596
+ context 'full checkout' do
+ before do
+ shipping_method.calculator.update!(preferred_amount: 10)
+ mug.shipping_category = shipping_method.shipping_categories.first
+ mug.save!
+ end
+
+ it 'does not break the per-item shipping method calculator', js: true do
+ add_mug_to_cart
+ click_button 'Checkout'
+
+ fill_in 'order_email', with: 'test@example.com'
+ click_on 'Continue'
+ fill_in_address
+
+ click_button 'Save and Continue'
+ expect(page).not_to have_content("undefined method `promotion'")
+ click_button 'Save and Continue'
+ expect(page).to have_content('Shipping total: $10.00')
+ end
+ end
+
+ # Regression test for #4306
+ context 'free shipping' do
+ before do
+ add_mug_to_cart
+ click_button 'Checkout'
+ fill_in 'order_email', with: 'test@example.com'
+ click_on 'Continue'
+ end
+
+ it "does not show 'Free Shipping' when there are no shipments" do
+ within('#checkout-summary') do
+ expect(page).not_to have_content('Free Shipping')
+ end
+ end
+ end
+
+ # Regression test for #4190
+ it 'updates state_lock_version on form submission', js: true do
+ add_mug_to_cart
+ click_button 'Checkout'
+
+ expect(find('input#order_state_lock_version', visible: false).value).to eq '0'
+
+ fill_in 'order_email', with: 'test@example.com'
+ fill_in_address
+ click_button 'Save and Continue'
+
+ expect(find('input#order_state_lock_version', visible: false).value).to eq '1'
+ end
+ end
+
+ # Regression test for #2694 and #4117
+ context "doesn't allow bad credit card numbers" do
+ before do
+ order = OrderWalkthrough.up_to(:payment)
+ allow(order).to receive_messages confirmation_required?: true
+ allow(order).to receive_messages(available_payment_methods: [create(:credit_card_payment_method)])
+
+ user = create(:user)
+ order.user = user
+ order.update_with_updater!
+
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user)
+ end
+
+ it 'redirects to payment page', inaccessible: true, js: true do
+ visit spree.checkout_state_path(:payment)
+ click_button 'Save and Continue'
+ choose 'Credit Card'
+ fill_in 'Card Number', with: '123'
+ fill_in 'card_expiry', with: '04 / 20'
+ fill_in 'Card Code', with: '123'
+ click_button 'Save and Continue'
+ click_button 'Place Order'
+ expect(page).to have_content('Bogus Gateway: Forced failure')
+ expect(page.current_url).to include('/checkout/payment')
+ end
+ end
+
+ # regression test for #3945
+ context 'when Spree::Config[:always_include_confirm_step] is true' do
+ before do
+ Spree::Config[:always_include_confirm_step] = true
+ end
+
+ it 'displays confirmation step', js: true do
+ add_mug_to_cart
+ click_button 'Checkout'
+
+ fill_in 'order_email', with: 'test@example.com'
+ click_on 'Continue'
+ fill_in_address
+
+ click_button 'Save and Continue'
+ click_button 'Save and Continue'
+ click_button 'Save and Continue'
+
+ continue_button = find('#checkout .btn-success')
+ expect(continue_button.value).to eq 'Place Order'
+ end
+ end
+
+ context 'and likes to double click buttons' do
+ let!(:user) { create(:user) }
+
+ let!(:order) do
+ order = OrderWalkthrough.up_to(:payment)
+ allow(order).to receive_messages confirmation_required?: true
+
+ order.reload
+ order.user = user
+ order.update_with_updater!
+ order
+ end
+
+ before do
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(skip_state_validation?: true)
+ end
+
+ it 'prevents double clicking the payment button on checkout', js: true do
+ visit spree.checkout_state_path(:payment)
+
+ # prevent form submit to verify button is disabled
+ page.execute_script("$('#checkout_form_payment').submit(function(){return false;})")
+
+ expect(page).not_to have_selector('input.btn.disabled')
+ click_button 'Save and Continue'
+ expect(page).to have_selector('input.btn.disabled')
+ end
+
+ it 'prevents double clicking the confirm button on checkout', js: true do
+ order.payments << create(:payment, amount: order.amount)
+ visit spree.checkout_state_path(:confirm)
+
+ # prevent form submit to verify button is disabled
+ page.execute_script("$('#checkout_form_confirm').submit(function(){return false;})")
+
+ expect(page).not_to have_selector('input.btn.disabled')
+ click_button 'Place Order'
+ expect(page).to have_selector('input.btn.disabled')
+ end
+ end
+
+ context 'when several payment methods are available', js: true do
+ let(:credit_cart_payment) { create(:credit_card_payment_method) }
+ let(:check_payment) { create(:check_payment_method) }
+
+ after do
+ Capybara.ignore_hidden_elements = true
+ end
+
+ before do
+ Capybara.ignore_hidden_elements = false
+ order = OrderWalkthrough.up_to(:payment)
+ allow(order).to receive_messages(available_payment_methods: [check_payment, credit_cart_payment])
+ order.user = create(:user)
+ order.update_with_updater!
+
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: order.user)
+
+ visit spree.checkout_state_path(:payment)
+ end
+
+ it 'the first payment method should be selected' do
+ payment_method_css = '#order_payments_attributes__payment_method_id_'
+ expect(find("#{payment_method_css}#{check_payment.id}")).to be_checked
+ expect(find("#{payment_method_css}#{credit_cart_payment.id}")).not_to be_checked
+ end
+
+ it 'the fields for the other payment methods should be hidden' do
+ payment_method_css = '#payment_method_'
+ expect(find("#{payment_method_css}#{check_payment.id}")).to be_visible
+ expect(find("#{payment_method_css}#{credit_cart_payment.id}")).not_to be_visible
+ end
+ end
+
+ context 'user has payment sources', js: true do
+ let(:bogus) { create(:credit_card_payment_method) }
+ let(:user) { create(:user) }
+
+ before do
+ create(:credit_card, user_id: user.id, payment_method: bogus, gateway_customer_profile_id: 'BGS-WEFWF')
+
+ order = OrderWalkthrough.up_to(:payment)
+ allow(order).to receive_messages(available_payment_methods: [bogus])
+
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user)
+ allow_any_instance_of(Spree::OrdersController).to receive_messages(try_spree_current_user: user)
+
+ visit spree.checkout_state_path(:payment)
+ end
+
+ it 'selects first source available and customer moves on' do
+ expect(find('#use_existing_card_yes')).to be_checked
+
+ expect { click_on 'Save and Continue' }.not_to change { Spree::CreditCard.count }
+
+ click_on 'Place Order'
+ expect(page).to have_current_path(spree.order_path(Spree::Order.last))
+ end
+
+ it 'allows user to enter a new source' do
+ choose 'use_existing_card_no'
+
+ native_fill_in 'Name on card', 'Spree Commerce'
+ native_fill_in 'Card Number', '4111111111111111'
+ native_fill_in 'card_expiry', '04 / 20'
+ native_fill_in 'Card Code', '123'
+
+ expect { click_on 'Save and Continue' }.to change { Spree::CreditCard.count }.by 1
+
+ click_on 'Place Order'
+
+ expect(page).to have_content(Spree.t(:thank_you_for_your_order))
+ expect(page).to have_current_path(spree.order_path(Spree::Order.last))
+ end
+ end
+
+ # regression for #2921
+ context 'goes back from payment to add another item', js: true do
+ let!(:bag) { create(:product, name: 'RoR Bag') }
+
+ it 'transit nicely through checkout steps again' do
+ add_mug_to_cart
+ click_on 'Checkout'
+ fill_in 'order_email', with: 'test@example.com'
+ click_on 'Continue'
+ fill_in_address
+ click_on 'Save and Continue'
+ click_on 'Save and Continue'
+ expect(page).to have_current_path(spree.checkout_state_path('payment'))
+
+ add_to_cart(bag.name)
+
+ click_on 'Checkout'
+ click_on 'Save and Continue'
+ click_on 'Save and Continue'
+ click_on 'Save and Continue'
+
+ expect(page).to have_current_path(spree.order_path(Spree::Order.last))
+ end
+ end
+
+ context 'from payment step customer goes back to cart', js: true do
+ before do
+ add_mug_to_cart
+ click_on 'Checkout'
+ fill_in 'order_email', with: 'test@example.com'
+ click_on 'Continue'
+ fill_in_address
+ click_on 'Save and Continue'
+ click_on 'Save and Continue'
+ expect(page).to have_current_path(spree.checkout_state_path('payment'))
+ end
+
+ context 'and updates line item quantity and try to reach payment page' do
+ let(:cart_quantity) { 3 }
+
+ before do
+ visit spree.cart_path
+ within '.cart-item-quantity' do
+ fill_in first('input')['name'], with: cart_quantity
+ end
+
+ click_on 'Update'
+ end
+
+ it 'redirects user back to address step' do
+ visit spree.checkout_state_path('payment')
+ expect(page).to have_current_path(spree.checkout_state_path('address'))
+ end
+
+ it 'updates shipments properly through step address -> delivery transitions' do
+ visit spree.checkout_state_path('payment')
+ click_on 'Save and Continue'
+ click_on 'Save and Continue'
+
+ expect(Spree::InventoryUnit.count).to eq 1
+ expect(Spree::InventoryUnit.first.quantity).to eq cart_quantity
+ end
+ end
+
+ context 'and adds new product to cart and try to reach payment page' do
+ let!(:bag) { create(:product, name: 'RoR Bag') }
+
+ before do
+ add_to_cart(bag.name)
+ end
+
+ it 'redirects user back to address step' do
+ visit spree.checkout_state_path('payment')
+ expect(page).to have_current_path(spree.checkout_state_path('address'))
+ end
+
+ it 'updates shipments properly through step address -> delivery transitions' do
+ visit spree.checkout_state_path('payment')
+ click_on 'Save and Continue'
+ click_on 'Save and Continue'
+
+ expect(Spree::InventoryUnit.count).to eq 2
+ end
+ end
+ end
+
+ # Regression test for #7734
+ context 'if multiple coupon promotions applied' do
+ let(:promotion) { Spree::Promotion.create(name: 'Order Promotion', code: 'o_promotion') }
+ let(:calculator) { Spree::Calculator::FlatPercentItemTotal.create(preferred_flat_percent: '90') }
+ let(:action) { Spree::Promotion::Actions::CreateAdjustment.create(calculator: calculator) }
+
+ let(:promotion_2) { Spree::Promotion.create(name: 'Line Item Promotion', code: 'li_promotion') }
+ let(:calculator_2) { Spree::Calculator::FlatRate.create(preferred_amount: '1000') }
+ let(:action_2) { Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: calculator_2) }
+
+ before do
+ promotion.actions << action
+ promotion_2.actions << action_2
+
+ add_mug_to_cart
+ end
+
+ it "totals aren't negative" do
+ fill_in 'Coupon Code', with: promotion.code
+ click_on 'Apply'
+
+ fill_in 'Coupon Code', with: promotion_2.code
+ click_on 'Apply'
+
+ expect(page).to have_content(promotion.name)
+ expect(page).to have_content(promotion_2.name)
+ expect(Spree::Order.last.total.to_f).to eq 0.0
+ end
+ end
+
+ context 'if coupon promotion, submits coupon along with payment', js: true do
+ let!(:promotion) { Spree::Promotion.create(name: 'Huhuhu', code: 'huhu') }
+ let!(:calculator) { Spree::Calculator::FlatPercentItemTotal.create(preferred_flat_percent: '10') }
+ let!(:action) { Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: calculator) }
+
+ before do
+ promotion.actions << action
+
+ add_mug_to_cart
+ click_on 'Checkout'
+
+ fill_in 'order_email', with: 'test@example.com'
+ click_on 'Continue'
+ fill_in_address
+ click_on 'Save and Continue'
+
+ click_on 'Save and Continue'
+ expect(page).to have_current_path(spree.checkout_state_path('payment'))
+ end
+
+ it 'makes sure payment reflects order total with discounts' do
+ fill_in 'Coupon Code', with: promotion.code
+ click_on 'Save and Continue'
+
+ expect(page).to have_content(promotion.name)
+ expect(Spree::Payment.first.amount.to_f).to eq Spree::Order.last.total.to_f
+ end
+
+ context 'invalid coupon' do
+ it 'doesnt create a payment record' do
+ fill_in 'Coupon Code', with: 'invalid'
+ click_on 'Save and Continue'
+
+ expect(Spree::Payment.count).to eq 0
+ expect(page).to have_content(Spree.t(:coupon_code_not_found))
+ end
+ end
+
+ context "doesn't fill in coupon code input" do
+ it 'advances just fine' do
+ click_on 'Save and Continue'
+ expect(page).to have_current_path(spree.order_path(Spree::Order.last))
+ end
+ end
+
+ context 'the promotion makes order free (downgrade it total to 0.0)' do
+ let(:promotion2) { Spree::Promotion.create(name: 'test-7450', code: 'test-7450') }
+ let(:calculator2) do
+ Spree::Calculator::FlatRate.create(preferences: { currency: 'USD', amount: BigDecimal('99999') })
+ end
+ let(:action2) { Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: calculator2) }
+
+ before { promotion2.actions << action2 }
+
+ context 'user choose to pay by check' do
+ it 'move user to complete checkout step' do
+ fill_in 'Coupon Code', with: promotion2.code
+ click_on 'Save and Continue'
+
+ expect(page).to have_content(promotion2.name)
+ expect(Spree::Order.last.total.to_f).to eq(0)
+ expect(page).to have_current_path(spree.order_path(Spree::Order.last))
+ end
+ end
+
+ context 'user choose to pay by card' do
+ let(:bogus) { create(:credit_card_payment_method) }
+
+ before do
+ order = Spree::Order.last
+ allow(order).to receive_messages(available_payment_methods: [bogus])
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order)
+
+ visit spree.checkout_state_path(:payment)
+ end
+
+ it 'move user to confirmation checkout step' do
+ fill_in 'Name on card', with: 'Spree Commerce'
+ fill_in 'Card Number', with: '4111111111111111'
+ fill_in 'card_expiry', with: '04 / 20'
+ fill_in 'Card Code', with: '123'
+
+ fill_in 'Coupon Code', with: promotion2.code
+ click_on 'Save and Continue'
+
+ expect(page).to have_content(promotion2.name)
+ expect(Spree::Order.last.total.to_f).to eq(0)
+ expect(page).to have_current_path(spree.checkout_state_path('confirm'))
+ end
+ end
+ end
+ end
+
+ context 'order has only payment step' do
+ before do
+ create(:credit_card_payment_method)
+ @old_checkout_flow = Spree::Order.checkout_flow
+ Spree::Order.class_eval do
+ checkout_flow do
+ go_to_state :payment
+ go_to_state :confirm
+ end
+ end
+
+ allow_any_instance_of(Spree::Order).to receive_messages email: 'spree@commerce.com'
+
+ add_mug_to_cart
+ click_on 'Checkout'
+ end
+
+ after do
+ Spree::Order.checkout_flow(&@old_checkout_flow)
+ end
+
+ it 'goes right payment step and place order just fine' do
+ expect(page).to have_current_path(spree.checkout_state_path('payment'))
+
+ choose 'Credit Card'
+ fill_in 'Name on card', with: 'Spree Commerce'
+ fill_in 'Card Number', with: '4111111111111111'
+ fill_in 'card_expiry', with: '04 / 20'
+ fill_in 'Card Code', with: '123'
+ click_button 'Save and Continue'
+
+ expect(page).to have_current_path(spree.checkout_state_path('confirm'))
+ click_button 'Place Order'
+ end
+ end
+
+ context 'save my address' do
+ before do
+ stock_location.stock_items.update_all(count_on_hand: 1)
+ add_mug_to_cart
+ end
+
+ context 'as a guest' do
+ before do
+ Spree::Order.last.update_column(:email, 'test@example.com')
+ click_button 'Checkout'
+ end
+
+ it 'is not displayed' do
+ expect(page).not_to have_css('[data-hook=save_user_address]')
+ end
+ end
+
+ context 'as a User' do
+ before do
+ user = create(:user)
+ Spree::Order.last.update_column :user_id, user.id
+ allow_any_instance_of(Spree::OrdersController).to receive_messages(try_spree_current_user: user)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user)
+ click_button 'Checkout'
+ end
+
+ it 'is displayed' do
+ expect(page).to have_css('[data-hook=save_user_address]')
+ end
+ end
+ end
+
+ context 'when order is completed' do
+ let!(:user) { create(:user) }
+ let!(:order) { OrderWalkthrough.up_to(:payment) }
+
+ before do
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user)
+ allow_any_instance_of(Spree::OrdersController).to receive_messages(try_spree_current_user: user)
+
+ visit spree.checkout_state_path(:payment)
+ click_button 'Save and Continue'
+ end
+
+ it 'displays a thank you message' do
+ expect(page).to have_content(Spree.t(:thank_you_for_your_order))
+ end
+
+ it 'does not display a thank you message on that order future visits' do
+ visit spree.order_path(order)
+ expect(page).not_to have_content(Spree.t(:thank_you_for_your_order))
+ end
+ end
+
+ context "order's address is outside the default included tax zone" do
+ context 'so that no taxation applies to its product' do
+ before do
+ usa = Spree::Country.find_by(name: 'United States of America')
+ north_america_zone = create(:zone,
+ name: 'North America',
+ kind: 'country',
+ default_tax: true).tap do |zone|
+ zone.members << create(:zone_member, zoneable: usa)
+ end
+
+ australia = create(:country,
+ name: 'Australia',
+ iso: 'AU',
+ iso_name: 'AUSTRALIA',
+ iso3: 'AUS',
+ states_required: true).tap do |country|
+ country.states << create(:state,
+ name: 'New South Wales',
+ abbr: 'NSW')
+ end
+ australia_zone = create(:zone,
+ name: 'Australia',
+ kind: 'country',
+ default_tax: false).tap do |zone|
+ zone.members << create(:zone_member, zoneable: australia)
+ end
+
+ default_tax_category = create(:tax_category, name: 'Default', is_default: true)
+
+ create(:shipping_method,
+ name: 'Default',
+ display_on: 'both',
+ zones: [australia_zone],
+ tax_category: default_tax_category).tap do |sm|
+ sm.calculator.preferred_amount = 10
+ sm.calculator.preferred_currency = Spree::Config[:currency]
+ sm.calculator.save
+ end
+
+ create(:tax_rate,
+ name: 'USA included',
+ amount: 0.23,
+ zone: north_america_zone,
+ tax_category: default_tax_category,
+ show_rate_in_label: true,
+ included_in_price: true)
+
+ create(:product, name: 'Spree Bag', price: 100, tax_category: default_tax_category)
+ create(:product, name: 'Spree T-Shirt', price: 100, tax_category: default_tax_category)
+ end
+
+ it 'correctly displays other product taxless price which has been added to cart later' do
+ visit spree.root_path
+
+ click_link 'Spree Bag'
+ click_on 'Add To Cart'
+ click_on 'Checkout'
+
+ fill_in 'order_email', with: 'test@example.com'
+
+ within '#checkout_form_address' do
+ address = 'order_bill_address_attributes'
+
+ fill_in "#{address}_firstname", with: 'John'
+ fill_in "#{address}_lastname", with: 'Doe'
+ fill_in "#{address}_address1", with: '199 George Street'
+ fill_in "#{address}_city", with: 'Sydney'
+ select 'Australia', from: "#{address}_country_id"
+ select 'New South Wales', from: "#{address}_state_id"
+ fill_in "#{address}_zipcode", with: '2000'
+ fill_in "#{address}_phone", with: '123456789'
+ end
+ click_on 'Save and Continue'
+
+ visit spree.root_path
+
+ click_link 'Spree T-Shirt'
+ click_on 'Add To Cart'
+
+ expect(page).not_to have_content('$100.00')
+ expect(page.all('td.cart-item-price')).to all(have_content('$81.30'))
+ end
+ end
+ end
+
+ context 'user has store credits', js: true do
+ let(:bogus) { create(:credit_card_payment_method) }
+ let(:store_credit_payment_method) { create(:store_credit_payment_method) }
+ let(:user) { create(:user) }
+ let(:order) { OrderWalkthrough.up_to(:payment) }
+
+ let(:prepare_checkout!) do
+ order.update(user: user)
+ allow(order).to receive_messages(available_payment_methods: [bogus, store_credit_payment_method])
+
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user)
+ allow_any_instance_of(Spree::OrdersController).to receive_messages(try_spree_current_user: user)
+ visit spree.checkout_state_path(:payment)
+ end
+
+ context 'when not all Store Credits are used' do
+ let!(:store_credit) { create(:store_credit, user: user) }
+ let!(:additional_store_credit) { create(:store_credit, user: user, amount: 13) }
+
+ before { prepare_checkout! }
+
+ it 'page has data for (multiple) Store Credits' do
+ expect(page).to have_selector('[data-hook="checkout_payment_store_credit_available"]')
+ expect(page).to have_selector('button[name="apply_store_credit"]')
+
+ amount = Spree::Money.new(store_credit.amount_remaining + additional_store_credit.amount_remaining)
+ expect(page).to have_content(Spree.t('store_credit.available_amount', amount: amount))
+ end
+
+ it 'apply store credits button should move checkout to next step if amount is sufficient' do
+ click_button 'Apply Store Credit'
+ expect(page).to have_current_path(spree.order_path(order))
+ expect(page).to have_content(Spree.t('order_processed_successfully'))
+ end
+
+ it 'apply store credits button should wait on payment step for other payment' do
+ store_credit.update(amount_used: 145)
+ additional_store_credit.update(amount_used: 12)
+ click_button 'Apply Store Credit'
+
+ expect(page).to have_current_path(spree.checkout_state_path(:payment))
+ amount = Spree::Money.new(store_credit.amount_remaining + additional_store_credit.amount_remaining)
+ remaining_amount = Spree::Money.new(order.total - amount.money.to_f)
+ expect(page).to have_content(Spree.t('store_credit.applicable_amount', amount: amount))
+ expect(page).to have_content(Spree.t('store_credit.additional_payment_needed', amount: remaining_amount))
+ expect(page).to have_content(Spree.t('store_credit.remove'))
+ end
+
+ context 'remove store credits payments' do
+ before do
+ store_credit.update(amount: 5)
+ additional_store_credit.update(amount: 5)
+ click_button 'Apply Store Credit'
+ end
+
+ it 'remove store credits button should remove store_credits' do
+ click_button 'Remove Store Credit'
+ expect(page).to have_current_path(spree.checkout_state_path(:payment))
+ expect(page).to have_content(Spree.t('store_credit.available_amount', amount: order.display_total_available_store_credit))
+ expect(page).to have_selector('button[name="apply_store_credit"]')
+ end
+ end
+ end
+
+ context 'when all Store Credits are used' do
+ before do
+ create(:store_credit, user: user, amount_used: 150)
+ prepare_checkout!
+ end
+
+ it 'page has no data for Store Credits when all Store Credits are used' do
+ expect(page).not_to have_selector('[data-hook="checkout_payment_store_credit_available"]')
+ expect(page).not_to have_selector('button[name="apply_store_credit"]')
+ end
+ end
+ end
+
+ def fill_in_address
+ address = 'order_bill_address_attributes'
+ fill_in "#{address}_firstname", with: 'Ryan'
+ fill_in "#{address}_lastname", with: 'Bigg'
+ fill_in "#{address}_address1", with: '143 Swan Street'
+ fill_in "#{address}_city", with: 'Richmond'
+ select country.name, from: "#{address}_country_id"
+ select state.name, from: "#{address}_state_id"
+ fill_in "#{address}_zipcode", with: '12345'
+ fill_in "#{address}_phone", with: '(555) 555-5555'
+ end
+
+ def add_mug_to_cart
+ add_to_cart(mug.name)
+ end
+end
diff --git a/frontend/spec/features/checkout_unshippable_spec.rb b/frontend/spec/features/checkout_unshippable_spec.rb
new file mode 100644
index 00000000000..3ebeb3605d1
--- /dev/null
+++ b/frontend/spec/features/checkout_unshippable_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe 'checkout with unshippable items', type: :feature, inaccessible: true do
+ let!(:stock_location) { create(:stock_location) }
+ let(:order) { OrderWalkthrough.up_to(:delivery) }
+
+ before do
+ OrderWalkthrough.add_line_item!(order)
+ line_item = order.line_items.last
+ stock_item = stock_location.stock_item(line_item.variant)
+ stock_item.adjust_count_on_hand(-999)
+ stock_item.backorderable = false
+ stock_item.save!
+
+ user = create(:user)
+ order.user = user
+ order.update_with_updater!
+
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(current_order: order)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(try_spree_current_user: user)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(skip_state_validation?: true)
+ allow_any_instance_of(Spree::CheckoutController).to receive_messages(ensure_sufficient_stock_lines: true)
+ end
+
+ it 'displays and removes' do
+ visit spree.checkout_state_path(:delivery)
+ expect(page).to have_content('Unshippable Items')
+
+ click_button 'Save and Continue'
+
+ order.reload
+ expect(order.line_items.count).to eq 1
+ end
+end
diff --git a/frontend/spec/features/coupon_code_spec.rb b/frontend/spec/features/coupon_code_spec.rb
new file mode 100644
index 00000000000..a29bf1302f0
--- /dev/null
+++ b/frontend/spec/features/coupon_code_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe 'Coupon code promotions', type: :feature, js: true do
+ let!(:country) { create(:country, name: 'United States of America', states_required: true) }
+ let!(:state) { create(:state, name: 'Alabama', country: country) }
+
+ before do
+ create(:zone)
+ create(:shipping_method)
+ create(:check_payment_method)
+ create(:product, name: 'RoR Mug', price: 20)
+ create(:store)
+ end
+
+ context 'visitor makes checkout as guest without registration' do
+ def create_basic_coupon_promotion(code)
+ promotion = Spree::Promotion.create!(name: code.titleize,
+ code: code,
+ starts_at: 1.day.ago,
+ expires_at: 1.day.from_now)
+
+ calculator = Spree::Calculator::FlatRate.new
+ calculator.preferred_amount = 10
+
+ action = Spree::Promotion::Actions::CreateItemAdjustments.new
+ action.calculator = calculator
+ action.promotion = promotion
+ action.save
+
+ promotion.reload # so that promotion.actions is available
+ end
+
+ let!(:promotion) { create_basic_coupon_promotion('onetwo') }
+
+ # OrdersController
+ context 'on the payment page' do
+ include_context 'proceed to payment step'
+
+ it 'informs about an invalid coupon code' do
+ fill_in 'order_coupon_code', with: 'coupon_codes_rule_man'
+ click_button 'Save and Continue'
+ expect(page).to have_content(Spree.t(:coupon_code_not_found))
+ end
+
+ it 'informs the user about a coupon code which has exceeded its usage' do
+ promotion.update_column(:usage_limit, 5)
+ allow_any_instance_of(promotion.class).to receive_messages(credits_count: 10)
+
+ fill_in 'order_coupon_code', with: 'onetwo'
+ click_button 'Save and Continue'
+ expect(page).to have_content(Spree.t(:coupon_code_max_usage))
+ end
+
+ it 'can enter an invalid coupon code, then a real one' do
+ fill_in 'order_coupon_code', with: 'coupon_codes_rule_man'
+ click_button 'Save and Continue'
+ expect(page).to have_content(Spree.t(:coupon_code_not_found))
+ fill_in 'order_coupon_code', with: 'onetwo'
+ click_button 'Save and Continue'
+ expect(page).to have_content('Promotion (Onetwo) -$10.00')
+ end
+
+ context 'with a promotion' do
+ it 'applies a promotion to an order' do
+ fill_in 'order_coupon_code', with: 'onetwo'
+ click_button 'Save and Continue'
+ expect(page).to have_content('Promotion (Onetwo) -$10.00')
+ end
+ end
+ end
+ end
+end
diff --git a/frontend/spec/features/currency_spec.rb b/frontend/spec/features/currency_spec.rb
new file mode 100644
index 00000000000..a420ccbf65a
--- /dev/null
+++ b/frontend/spec/features/currency_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'Switching currencies in backend', type: :feature do
+ before do
+ create(:base_product, name: 'RoR Mug')
+ end
+
+ # Regression test for #2340
+ it 'does not cause current_order to become nil', inaccessible: true, js: true do
+ add_to_cart('RoR Mug')
+ # Now that we have an order...
+ Spree::Config[:currency] = 'AUD'
+ expect { visit spree.root_path }.not_to raise_error
+ end
+end
diff --git a/frontend/spec/features/delivery_spec.rb b/frontend/spec/features/delivery_spec.rb
new file mode 100644
index 00000000000..5b60828fe60
--- /dev/null
+++ b/frontend/spec/features/delivery_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe 'Delivery', type: :feature, inaccessible: true, js: true do
+ include_context 'checkout setup'
+
+ let(:country) { create(:country, name: 'United States of America', iso_name: 'UNITED STATES') }
+ let(:state) { create(:state, name: 'Alabama', abbr: 'AL', country: country) }
+ let(:user) { create(:user) }
+ let!(:shipping_method2) do
+ sm = create(:shipping_method, name: 'Shipping Method2')
+ sm.calculator.preferred_amount = 20
+ sm.calculator.save
+ sm
+ end
+
+ let(:add_mug_and_navigate_to_delivery_page) do
+ add_to_cart(mug.name)
+ click_button 'Checkout'
+
+ fill_in 'order_email', with: 'test@example.com'
+ click_on 'Continue'
+ fill_in_address
+
+ click_button 'Save and Continue'
+ end
+
+ before do
+ create(:product) # product 2
+ shipping_method.calculator.preferred_amount = 10
+ shipping_method.calculator.save
+ end
+
+ describe 'shipping total gets updated when shipping method is changed in the delivery step' do
+ before do
+ add_mug_and_navigate_to_delivery_page
+ end
+
+ it 'contains the shipping total' do
+ expect(page).to have_content('Shipping total: $10.00')
+ end
+
+ context 'shipping method is changed' do
+ before { choose(shipping_method2.name) }
+
+ it 'shipping total and order total both are updates' do
+ expect(page).to have_content('Shipping total: $20.00')
+ end
+ end
+ end
+
+ context 'custom currency markers' do
+ before do
+ Spree::Money.default_formatting_rules[:decimal_mark] = ','
+ Spree::Money.default_formatting_rules[:thousands_separator] = '.'
+
+ add_mug_and_navigate_to_delivery_page
+
+ choose(shipping_method2.name)
+ end
+
+ after do
+ Spree::Money.default_formatting_rules.delete(:decimal_mark)
+ Spree::Money.default_formatting_rules.delete(:thousands_separator)
+ end
+
+ it 'calculates shipping total correctly with different currency marker' do
+ expect(page).to have_content('Shipping total: $20,00')
+ end
+
+ it 'calculates order total correctly with different currency marker' do
+ expect(page).to have_content('Order Total: $39,99')
+ end
+ end
+
+ def fill_in_address
+ address = 'order_bill_address_attributes'
+ fill_in "#{address}_firstname", with: FFaker::Name.first_name
+ fill_in "#{address}_lastname", with: FFaker::Name.last_name
+ fill_in "#{address}_address1", with: FFaker::Address.street_address
+ fill_in "#{address}_city", with: FFaker::Address.city
+ select country.name, from: "#{address}_country_id"
+ select state.name, from: "#{address}_state_id"
+ fill_in "#{address}_zipcode", with: FFaker::AddressUS.zip_code
+ fill_in "#{address}_phone", with: FFaker::PhoneNumber.phone_number
+ end
+end
diff --git a/frontend/spec/features/free_shipping_promotions_spec.rb b/frontend/spec/features/free_shipping_promotions_spec.rb
new file mode 100644
index 00000000000..c1c46fbe68d
--- /dev/null
+++ b/frontend/spec/features/free_shipping_promotions_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe 'Free shipping promotions', type: :feature, js: true do
+ let!(:country) { create(:country, name: 'United States of America', states_required: true) }
+ let!(:state) { create(:state, name: 'Alabama', country: country) }
+
+ before do
+ create(:zone)
+ sm = create(:shipping_method)
+ sm.calculator.preferred_amount = 10
+ sm.calculator.save
+
+ create(:check_payment_method)
+ create(:product, name: 'RoR Mug', price: 20)
+
+ promotion = Spree::Promotion.create!(name: 'Free Shipping',
+ starts_at: 1.day.ago,
+ expires_at: 1.day.from_now)
+
+ action = Spree::Promotion::Actions::FreeShipping.new
+ action.promotion = promotion
+ action.save
+
+ promotion.reload # so that promotion.actions is available
+ end
+
+ context 'free shipping promotion automatically applied' do
+ include_context 'proceed to payment step'
+
+ # Regression test for #4428
+ it 'applies the free shipping promotion' do
+ within('#checkout-summary') do
+ expect(page).to have_content('Shipping total: $10.00')
+ expect(page).to have_content('Promotion (Free Shipping): -$10.00')
+ end
+ end
+ end
+end
diff --git a/frontend/spec/features/locale_spec.rb b/frontend/spec/features/locale_spec.rb
new file mode 100644
index 00000000000..17de9994863
--- /dev/null
+++ b/frontend/spec/features/locale_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe 'setting locale', type: :feature do
+ def with_locale(locale)
+ I18n.locale = locale
+ Spree::Frontend::Config[:locale] = locale
+ yield
+ I18n.locale = 'en'
+ Spree::Frontend::Config[:locale] = 'en'
+ end
+
+ context 'shopping cart link and page' do
+ before do
+ I18n.backend.store_translations(:fr,
+ spree: {
+ cart: 'Panier',
+ shopping_cart: 'Panier'
+ })
+ end
+
+ it 'is in french' do
+ with_locale('fr') do
+ visit spree.root_path
+ click_link 'Panier'
+ expect(page).to have_content('Panier')
+ end
+ end
+ end
+
+ context 'checkout form validation messages' do
+ include_context 'checkout setup'
+
+ let(:error_messages) do
+ {
+ 'en' => 'This field is required.',
+ 'fr' => 'Ce champ est obligatoire.',
+ 'de' => 'Dieses Feld ist ein Pflichtfeld.'
+ }
+ end
+
+ def check_error_text(text)
+ %w(firstname lastname address1 city).each do |attr|
+ expect(find("#b#{attr} label.error").text).to eq(text)
+ end
+ end
+ end
+end
diff --git a/vendor/plugins/rspec_on_rails/spec_resources/views/render_spec/some_action.rhtml b/frontend/spec/features/microdata_spec.rb
similarity index 100%
rename from vendor/plugins/rspec_on_rails/spec_resources/views/render_spec/some_action.rhtml
rename to frontend/spec/features/microdata_spec.rb
diff --git a/frontend/spec/features/order_spec.rb b/frontend/spec/features/order_spec.rb
new file mode 100644
index 00000000000..364eb5974e1
--- /dev/null
+++ b/frontend/spec/features/order_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe 'orders', type: :feature do
+ let(:order) { OrderWalkthrough.up_to(:complete) }
+ let(:user) { create(:user) }
+
+ before do
+ order.update_attribute(:user_id, user.id)
+ order.shipments.destroy_all
+ allow_any_instance_of(Spree::OrdersController).to receive_messages(try_spree_current_user: user)
+ end
+
+ it 'can visit an order' do
+ # Regression test for current_user call on orders/show
+ expect { visit spree.order_path(order) }.not_to raise_error
+ end
+
+ it 'displays line item price' do
+ # Regression test for #2772
+ line_item = order.line_items.first
+ line_item.target_shipment = create(:shipment)
+ line_item.price = 19.00
+ line_item.save!
+
+ visit spree.order_path(order)
+
+ # Tests view spree/shared/_order_details
+ within 'td.price' do
+ expect(page).to have_content '19.00'
+ end
+ end
+
+ it 'has credit card info if paid with credit card' do
+ create(:payment, order: order)
+ visit spree.order_path(order)
+ within '.payment-info' do
+ expect(page).to have_content 'Ending in 1111'
+ end
+ end
+
+ it 'has payment method name visible if not paid with credit card' do
+ create(:check_payment, order: order)
+ visit spree.order_path(order)
+ within '.payment-info' do
+ expect(page).to have_content 'Check'
+ end
+ end
+
+ # Regression test for #2282
+ context 'can support a credit card with blank information' do
+ before do
+ credit_card = create(:credit_card)
+ credit_card.update_column(:cc_type, '')
+ payment = order.payments.first
+ payment.source = credit_card
+ payment.save!
+ end
+
+ specify do
+ visit spree.order_path(order)
+ within '.payment-info' do
+ expect { find('img') }.to raise_error(Capybara::ElementNotFound)
+ end
+ end
+ end
+
+ it 'returns the correct title when displaying a completed order' do
+ visit spree.order_path(order)
+
+ within '#order_summary' do
+ expect(page).to have_content("#{Spree.t(:order)} #{order.number}")
+ end
+ end
+
+ # Regression test for #6733
+ context 'address_requires_state preference' do
+ context 'when set to true' do
+ before do
+ configure_spree_preferences { |config| config.address_requires_state = true }
+ end
+
+ it 'shows state text' do
+ visit spree.order_path(order)
+
+ within '#order' do
+ expect(page).to have_content(order.bill_address.state_text)
+ expect(page).to have_content(order.ship_address.state_text)
+ end
+ end
+ end
+
+ context 'when set to false' do
+ before do
+ configure_spree_preferences { |config| config.address_requires_state = false }
+ end
+
+ it 'does not show state text' do
+ visit spree.order_path(order)
+
+ within '#order' do
+ expect(page).not_to have_content(order.bill_address.state_text)
+ expect(page).not_to have_content(order.ship_address.state_text)
+ end
+ end
+ end
+ end
+end
diff --git a/frontend/spec/features/page_promotions_spec.rb b/frontend/spec/features/page_promotions_spec.rb
new file mode 100644
index 00000000000..addee75c59f
--- /dev/null
+++ b/frontend/spec/features/page_promotions_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'page promotions', type: :feature, js: true do
+ before do
+ create(:product, name: 'RoR Mug', price: 20)
+
+ promotion = Spree::Promotion.create!(name: '$10 off',
+ path: 'test',
+ starts_at: 1.day.ago,
+ expires_at: 1.day.from_now)
+
+ calculator = Spree::Calculator::FlatRate.new
+ calculator.preferred_amount = 10
+
+ action = Spree::Promotion::Actions::CreateItemAdjustments.create(calculator: calculator)
+ promotion.actions << action
+
+ add_to_cart('RoR Mug')
+ end
+
+ it 'automatically applies a page promotion upon visiting' do
+ expect(page).not_to have_content('Promotion ($10 off) -$10.00')
+ visit '/content/test'
+ visit '/cart'
+ expect(page).to have_content('Promotion ($10 off) -$10.00')
+ expect(page).to have_content('Subtotal (1 item) $20.00')
+ end
+
+ it "does not activate an adjustment for a path that doesn't have a promotion" do
+ expect(page).not_to have_content('Promotion ($10 off) -$10.00')
+ visit '/content/cvv'
+ visit '/cart'
+ expect(page).not_to have_content('Promotion ($10 off) -$10.00')
+ end
+end
diff --git a/frontend/spec/features/products_spec.rb b/frontend/spec/features/products_spec.rb
new file mode 100644
index 00000000000..2799c8e8ea6
--- /dev/null
+++ b/frontend/spec/features/products_spec.rb
@@ -0,0 +1,357 @@
+require 'spec_helper'
+
+describe 'Visiting Products', type: :feature, inaccessible: true do
+ include_context 'custom products'
+
+ let(:store_name) do
+ ((first_store = Spree::Store.first) && first_store.name).to_s
+ end
+
+ before do
+ visit spree.root_path
+ allow(ENV).to receive(:[]).and_call_original
+ allow(ENV).to receive(:[]).with(:SPREE_USE_PAPERCLIP).and_return(true)
+ end
+
+ it 'is able to show the shopping cart after adding a product to it', js: true do
+ click_link 'Ruby on Rails Ringer T-Shirt'
+ expect(page).to have_content('$19.99')
+
+ expect(page).to have_selector('form#add-to-cart-form')
+ expect(page).to have_selector('button#add-to-cart-button')
+ wait_for_condition do
+ expect(page.find('#add-to-cart-button').disabled?).to eq(false)
+ end
+ click_button 'add-to-cart-button'
+ wait_for_condition do
+ expect(page).to have_content(Spree.t(:shopping_cart))
+ end
+ end
+
+ describe 'correct displaying of microdata' do
+ let(:product) { Spree::Product.find_by(name: 'Ruby on Rails Ringer T-Shirt') }
+
+ it 'on products page' do
+ within("#product_#{product.id}") do
+ within('[itemprop=name]') do
+ expect(page).to have_content('Ruby on Rails Ringer T-Shirt')
+ end
+ expect(page).to have_css("[itemprop='price'][content='19.99']")
+ expect(page).to have_css("[itemprop='priceCurrency'][content='USD']")
+ expect(page).to have_css("[itemprop='url'][href='/products/ruby-on-rails-ringer-t-shirt']")
+ expect(page).to have_css("[itemprop='image'][src*='/assets/noimage/small']")
+ end
+ end
+
+ it 'on product page' do
+ click_link product.name
+ within('[data-hook=product_show]') do
+ within('[itemprop=name]') do
+ expect(page).to have_content('Ruby on Rails Ringer T-Shirt')
+ end
+ expect(page).to have_css("[itemprop='price'][content='19.99']")
+ expect(page).to have_css("[itemprop='priceCurrency'][content='USD']")
+ expect(page).to have_css("[itemprop='image'][src*='/assets/noimage/product']")
+ end
+ end
+ end
+
+ describe 'meta tags and title' do
+ let(:jersey) { Spree::Product.find_by(name: 'Ruby on Rails Baseball Jersey') }
+ let(:metas) { { meta_description: 'Brand new Ruby on Rails Jersey', meta_title: 'Ruby on Rails Baseball Jersey Buy High Quality Geek Apparel', meta_keywords: 'ror, jersey, ruby' } }
+
+ it 'returns the correct title when displaying a single product' do
+ click_link jersey.name
+ expect(page).to have_title('Ruby on Rails Baseball Jersey - ' + store_name)
+ within('div#product-description') do
+ within('h1.product-title') do
+ expect(page).to have_content('Ruby on Rails Baseball Jersey')
+ end
+ end
+ end
+
+ it 'displays metas' do
+ jersey.update_attributes metas
+ click_link jersey.name
+ expect(page).to have_meta(:description, 'Brand new Ruby on Rails Jersey')
+ expect(page).to have_meta(:keywords, 'ror, jersey, ruby')
+ end
+
+ it 'displays title if set' do
+ jersey.update_attributes metas
+ click_link jersey.name
+ expect(page).to have_title('Ruby on Rails Baseball Jersey Buy High Quality Geek Apparel')
+ end
+
+ it "doesn't use meta_title as heading on page" do
+ jersey.update_attributes metas
+ click_link jersey.name
+ within('h1') do
+ expect(page).to have_content(jersey.name)
+ expect(page).not_to have_content(jersey.meta_title)
+ end
+ end
+
+ it 'uses product name in title when meta_title set to empty string' do
+ jersey.update_attributes meta_title: ''
+ click_link jersey.name
+ expect(page).to have_title('Ruby on Rails Baseball Jersey - ' + store_name)
+ end
+ end
+
+ context 'using Russian Rubles as a currency' do
+ before do
+ Spree::Config[:currency] = 'RUB'
+ end
+
+ let!(:product) do
+ product = Spree::Product.find_by(name: 'Ruby on Rails Ringer T-Shirt')
+ product.price = 19.99
+ product.tap(&:save)
+ end
+
+ # Regression tests for #2737
+ context 'uses руб as the currency symbol' do
+ it 'on products page' do
+ visit spree.root_path
+ within("#product_#{product.id}") do
+ within('.price') do
+ expect(page).to have_content('19.99 ₽')
+ end
+ end
+ end
+
+ it 'on product page' do
+ visit spree.product_path(product)
+ within('.price') do
+ expect(page).to have_content('19.99 ₽')
+ end
+ end
+
+ it 'when adding a product to the cart', js: true do
+ visit spree.product_path(product)
+ click_button 'Add To Cart'
+ click_link 'Home'
+ within('.cart-info') do
+ expect(page).to have_content('19.99 ₽')
+ end
+ end
+
+ it "when on the 'address' state of the cart", js: true do
+ visit spree.product_path(product)
+ click_button 'Add To Cart'
+ click_button 'Checkout'
+ fill_in 'order_email', with: 'test@example.com'
+ click_button 'Continue'
+ within('tr[data-hook=item_total]') do
+ expect(page).to have_content('19.99 ₽')
+ end
+ end
+ end
+ end
+
+ it 'is able to search for a product' do
+ fill_in 'keywords', with: 'shirt'
+ click_button 'Search'
+
+ expect(page.all('#products .product-list-item').size).to eq(1)
+ end
+
+ context 'a product with variants' do
+ let(:product) { Spree::Product.find_by(name: 'Ruby on Rails Baseball Jersey') }
+ let(:option_value) { create(:option_value) }
+ let!(:variant) { build(:variant, price: 5.59, product: product, option_values: []) }
+
+ before do
+ image = File.open(File.expand_path('../fixtures/thinking-cat.jpg', __dir__))
+ create_image(product, image)
+
+ product.option_types << option_value.option_type
+ variant.option_values << option_value
+ variant.save!
+ end
+
+ it 'is displayed' do
+ expect { click_link product.name }.not_to raise_error
+ end
+
+ it 'displays price of first variant listed', js: true do
+ click_link product.name
+ within('#product-price') do
+ expect(page).to have_content variant.price
+ expect(page).not_to have_content Spree.t(:out_of_stock)
+ end
+ end
+
+ it "doesn't display out of stock for master product" do
+ product.master.stock_items.update_all count_on_hand: 0, backorderable: false
+
+ click_link product.name
+ within('#product-price') do
+ expect(page).not_to have_content Spree.t(:out_of_stock)
+ end
+ end
+
+ it "doesn't display cart form if all variants (including master) are out of stock" do
+ product.variants_including_master.each { |v| v.stock_items.update_all count_on_hand: 0, backorderable: false }
+
+ click_link product.name
+ within('[data-hook=product_price]') do
+ expect(page).not_to have_content Spree.t(:add_to_cart)
+ end
+ end
+ end
+
+ context 'a product with variants, images only for the variants' do
+ let(:product) { Spree::Product.find_by(name: 'Ruby on Rails Baseball Jersey') }
+ let(:variant1) { create(:variant, product: product, price: 9.99) }
+ let(:variant2) { create(:variant, product: product, price: 10.99) }
+
+ before do
+ image = File.open(File.expand_path('../fixtures/thinking-cat.jpg', __dir__))
+ create_image(variant1, image)
+ end
+
+ it 'does not display no image available' do
+ visit spree.root_path
+ expect(page).to have_xpath("//img[contains(@src,'thinking-cat')]")
+ end
+ end
+
+ context 'an out of stock product without variants' do
+ let(:product) { Spree::Product.find_by(name: 'Ruby on Rails Tote') }
+
+ before do
+ product.master.stock_items.update_all count_on_hand: 0, backorderable: false
+ end
+
+ it 'does display out of stock for master product' do
+ click_link product.name
+ within('#product-price') do
+ expect(page).to have_content Spree.t(:out_of_stock)
+ end
+ end
+
+ it "doesn't display cart form if master is out of stock" do
+ click_link product.name
+ within('[data-hook=product_price]') do
+ expect(page).not_to have_content Spree.t(:add_to_cart)
+ end
+ end
+ end
+
+ context 'product with taxons' do
+ let(:product) { Spree::Product.find_by(name: 'Ruby on Rails Tote') }
+ let(:taxon) { product.taxons.first }
+
+ it 'displays breadcrumbs for the default taxon when none selected' do
+ click_link product.name
+ within('#breadcrumbs') do
+ expect(page).to have_content taxon.name
+ end
+ end
+
+ it 'displays selected taxon in breadcrumbs' do
+ taxon = Spree::Taxon.last
+ product.taxons << taxon
+ product.save!
+ visit '/t/' + taxon.to_param
+ click_link product.name
+ within('#breadcrumbs') do
+ expect(page).to have_content taxon.name
+ end
+ end
+ end
+
+ it 'is able to hide products without price' do
+ expect(page.all('#products .product-list-item').size).to eq(9)
+ Spree::Config.show_products_without_price = false
+ Spree::Config.currency = 'CAN'
+ visit spree.root_path
+ expect(page.all('#products .product-list-item').size).to eq(0)
+ end
+
+ it 'is able to display products priced under 10 dollars' do
+ within(:css, '#taxonomies') { click_link 'Ruby on Rails' }
+ check 'Price_Range_Under_$10.00'
+ within(:css, '#sidebar_products_search') { click_button 'Search' }
+ expect(page).to have_content('No products found')
+ end
+
+ it 'is able to display products priced between 15 and 18 dollars' do
+ within(:css, '#taxonomies') { click_link 'Ruby on Rails' }
+ check 'Price_Range_$15.00_-_$18.00'
+ within(:css, '#sidebar_products_search') { click_button 'Search' }
+
+ expect(page.all('#products .product-list-item').size).to eq(3)
+ tmp = page.all('#products .product-list-item a').map(&:text).flatten.compact
+ tmp.delete('')
+ expect(tmp.sort!).to eq(['Ruby on Rails Mug', 'Ruby on Rails Stein', 'Ruby on Rails Tote'])
+ end
+
+ it 'is able to display products priced between 15 and 18 dollars across multiple pages' do
+ Spree::Config.products_per_page = 2
+ within(:css, '#taxonomies') { click_link 'Ruby on Rails' }
+ check 'Price_Range_$15.00_-_$18.00'
+ within(:css, '#sidebar_products_search') { click_button 'Search' }
+
+ expect(page.all('#products .product-list-item').size).to eq(2)
+ products = page.all('#products .product-list-item span[itemprop=name]')
+ expect(products.count).to eq(2)
+
+ find('.pagination .next a').click
+ products = page.all('#products .product-list-item span[itemprop=name]')
+ expect(products.count).to eq(1)
+ end
+
+ it 'is able to display products priced 18 dollars and above' do
+ within(:css, '#taxonomies') { click_link 'Ruby on Rails' }
+ check 'Price_Range_$18.00_-_$20.00'
+ check 'Price_Range_$20.00_or_over'
+ within(:css, '#sidebar_products_search') { click_button 'Search' }
+
+ expect(page.all('#products .product-list-item').size).to eq(4)
+ tmp = page.all('#products .product-list-item a').map(&:text).flatten.compact
+ tmp.delete('')
+ expect(tmp.sort!).to eq(['Ruby on Rails Bag',
+ 'Ruby on Rails Baseball Jersey',
+ 'Ruby on Rails Jr. Spaghetti',
+ 'Ruby on Rails Ringer T-Shirt'])
+ end
+
+ it 'is able to put a product without a description in the cart', js: true do
+ product = FactoryBot.create(:base_product, description: nil, name: 'Sample', price: '19.99')
+ visit spree.product_path(product)
+ expect(page).to have_selector('form#add-to-cart-form')
+ expect(page).to have_selector('button#add-to-cart-button')
+ expect(page).to have_content 'This product has no description'
+ wait_for_condition do
+ expect(page.find('#add-to-cart-button').disabled?).to eq(false)
+ end
+ click_button 'add-to-cart-button'
+ wait_for_condition do
+ expect(page).to have_content(Spree.t(:shopping_cart))
+ end
+ expect(page).to have_content 'This product has no description'
+ end
+
+ it 'is not able to put a product without a current price in the cart' do
+ product = FactoryBot.create(:base_product, description: nil, name: 'Sample', price: '19.99')
+ Spree::Config.currency = 'CAN'
+ Spree::Config.show_products_without_price = true
+ visit spree.product_path(product)
+ expect(page).to have_content 'This product is not available in the selected currency.'
+ expect(page).not_to have_content 'add-to-cart-button'
+ end
+
+ it 'returns the correct title when displaying a single product' do
+ product = Spree::Product.find_by(name: 'Ruby on Rails Baseball Jersey')
+ click_link product.name
+
+ within('div#product-description') do
+ within('h1.product-title') do
+ expect(page).to have_content('Ruby on Rails Baseball Jersey')
+ end
+ end
+ end
+end
diff --git a/frontend/spec/features/taxons_spec.rb b/frontend/spec/features/taxons_spec.rb
new file mode 100644
index 00000000000..daabece39bf
--- /dev/null
+++ b/frontend/spec/features/taxons_spec.rb
@@ -0,0 +1,148 @@
+require 'spec_helper'
+
+describe 'viewing products', type: :feature, inaccessible: true do
+ let!(:taxonomy) { create(:taxonomy, name: 'Category') }
+ let!(:super_clothing) { taxonomy.root.children.create(name: 'Super Clothing') }
+ let!(:t_shirts) { super_clothing.children.create(name: 'T-Shirts') }
+ let(:metas) { { meta_description: 'Brand new Ruby on Rails TShirts', meta_title: 'Ruby On Rails TShirt', meta_keywords: 'ror, tshirt, ruby' } }
+ let(:store_name) { ((first_store = Spree::Store.first) && first_store.name).to_s }
+
+ before do
+ t_shirts.children.create(name: 'XXL') # xxl
+
+ product = create(:product, name: 'Superman T-Shirt')
+ product.taxons << t_shirts
+ end
+
+ # Regression test for #1796
+ it "can see a taxon's products, even if that taxon has child taxons" do
+ visit '/t/category/super-clothing/t-shirts'
+ expect(page).to have_content('Superman T-Shirt')
+ end
+
+ it 'does not show nested taxons with a search' do
+ visit '/t/category/super-clothing?keywords=shirt'
+ expect(page).to have_content('Superman T-Shirt')
+ expect(page).not_to have_selector("div[data-hook='taxon_children']")
+ end
+
+ describe 'breadcrumbs' do
+ before do
+ visit '/t/category/super-clothing/t-shirts'
+ end
+
+ it 'renders breadcrumbs' do
+ expect(page.find('#breadcrumbs')).to have_link('T-Shirts')
+ end
+ it 'marks last breadcrumb as active' do
+ expect(page.find('#breadcrumbs .active')).to have_link('T-Shirts')
+ end
+ end
+
+ describe 'meta tags and title' do
+ it 'displays metas' do
+ t_shirts.update_attributes metas
+ visit '/t/category/super-clothing/t-shirts'
+ expect(page).to have_meta(:description, 'Brand new Ruby on Rails TShirts')
+ expect(page).to have_meta(:keywords, 'ror, tshirt, ruby')
+ end
+
+ it 'display title if set' do
+ t_shirts.update_attributes metas
+ visit '/t/category/super-clothing/t-shirts'
+ expect(page).to have_title('Ruby On Rails TShirt')
+ end
+
+ it 'displays title from taxon root and taxon name' do
+ visit '/t/category/super-clothing/t-shirts'
+ expect(page).to have_title('Category - T-Shirts - ' + store_name)
+ end
+
+ # Regression test for #2814
+ it "doesn't use meta_title as heading on page" do
+ t_shirts.update_attributes metas
+ visit '/t/category/super-clothing/t-shirts'
+ within('h1.taxon-title') do
+ expect(page).to have_content(t_shirts.name)
+ end
+ end
+
+ it 'uses taxon name in title when meta_title set to empty string' do
+ t_shirts.update_attributes meta_title: ''
+ visit '/t/category/super-clothing/t-shirts'
+ expect(page).to have_title('Category - T-Shirts - ' + store_name)
+ end
+ end
+
+ context 'taxon pages' do
+ include_context 'custom products'
+ before do
+ visit spree.root_path
+ end
+
+ it 'is able to visit brand Ruby on Rails' do
+ within(:css, '#taxonomies') { click_link 'Ruby on Rails' }
+
+ expect(page.all('#products .product-list-item').size).to eq(7)
+ tmp = page.all('#products .product-list-item a').map(&:text).flatten.compact
+ tmp.delete('')
+ array = ['Ruby on Rails Bag',
+ 'Ruby on Rails Baseball Jersey',
+ 'Ruby on Rails Jr. Spaghetti',
+ 'Ruby on Rails Mug',
+ 'Ruby on Rails Ringer T-Shirt',
+ 'Ruby on Rails Stein',
+ 'Ruby on Rails Tote']
+ expect(tmp.sort!).to eq(array)
+ end
+
+ it 'is able to visit brand Ruby' do
+ within(:css, '#taxonomies') { click_link 'Ruby' }
+
+ expect(page.all('#products .product-list-item').size).to eq(1)
+ tmp = page.all('#products .product-list-item a').map(&:text).flatten.compact
+ tmp.delete('')
+ expect(tmp.sort!).to eq(['Ruby Baseball Jersey'])
+ end
+
+ it 'is able to visit brand Apache' do
+ within(:css, '#taxonomies') { click_link 'Apache' }
+
+ expect(page.all('#products .product-list-item').size).to eq(1)
+ tmp = page.all('#products .product-list-item a').map(&:text).flatten.compact
+ tmp.delete('')
+ expect(tmp.sort!).to eq(['Apache Baseball Jersey'])
+ end
+
+ it 'is able to visit category Clothing' do
+ click_link 'Clothing'
+
+ expect(page.all('#products .product-list-item').size).to eq(5)
+ tmp = page.all('#products .product-list-item a').map(&:text).flatten.compact
+ tmp.delete('')
+ expect(tmp.sort!).to eq(['Apache Baseball Jersey',
+ 'Ruby Baseball Jersey',
+ 'Ruby on Rails Baseball Jersey',
+ 'Ruby on Rails Jr. Spaghetti',
+ 'Ruby on Rails Ringer T-Shirt'])
+ end
+
+ it 'is able to visit category Mugs' do
+ click_link 'Mugs'
+
+ expect(page.all('#products .product-list-item').size).to eq(2)
+ tmp = page.all('#products .product-list-item a').map(&:text).flatten.compact
+ tmp.delete('')
+ expect(tmp.sort!).to eq(['Ruby on Rails Mug', 'Ruby on Rails Stein'])
+ end
+
+ it 'is able to visit category Bags' do
+ click_link 'Bags'
+
+ expect(page.all('#products .product-list-item').size).to eq(2)
+ tmp = page.all('#products .product-list-item a').map(&:text).flatten.compact
+ tmp.delete('')
+ expect(tmp.sort!).to eq(['Ruby on Rails Bag', 'Ruby on Rails Tote'])
+ end
+ end
+end
diff --git a/frontend/spec/features/template_rendering_spec.rb b/frontend/spec/features/template_rendering_spec.rb
new file mode 100644
index 00000000000..e4902106881
--- /dev/null
+++ b/frontend/spec/features/template_rendering_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'Template rendering', type: :feature do
+ after do
+ Capybara.ignore_hidden_elements = true
+ end
+
+ before do
+ Capybara.ignore_hidden_elements = false
+ end
+
+ it 'layout should have canonical tag referencing site url' do
+ Spree::Store.create!(code: 'spree', name: 'My Spree Store', url: 'spreestore.example.com', mail_from_address: 'test@example.com')
+
+ visit spree.root_path
+ expect(find('link[rel=canonical]')[:href]).to eql('http://spreestore.example.com/')
+ end
+end
diff --git a/frontend/spec/fixtures/thinking-cat.jpg b/frontend/spec/fixtures/thinking-cat.jpg
new file mode 100644
index 00000000000..7e8524d367b
Binary files /dev/null and b/frontend/spec/fixtures/thinking-cat.jpg differ
diff --git a/frontend/spec/helpers/frontend_helper_spec.rb b/frontend/spec/helpers/frontend_helper_spec.rb
new file mode 100644
index 00000000000..c8ec3aa14c2
--- /dev/null
+++ b/frontend/spec/helpers/frontend_helper_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+module Spree
+ describe FrontendHelper, type: :helper do
+ # Regression test for #2034
+ context 'flash_message' do
+ let(:flash) { { 'notice' => 'ok', 'foo' => 'foo', 'bar' => 'bar' } }
+
+ it 'outputs all flash content' do
+ flash_messages
+ html = Nokogiri::HTML(helper.output_buffer)
+ expect(html.css('.alert-notice').text).to eq('ok')
+ expect(html.css('.alert-foo').text).to eq('foo')
+ expect(html.css('.alert-bar').text).to eq('bar')
+ end
+
+ it 'outputs flash content except one key' do
+ flash_messages(ignore_types: :bar)
+ html = Nokogiri::HTML(helper.output_buffer)
+ expect(html.css('.alert-notice').text).to eq('ok')
+ expect(html.css('.alert-foo').text).to eq('foo')
+ expect(html.css('.alert-bar').text).to be_empty
+ end
+
+ it 'outputs flash content except some keys' do
+ flash_messages(ignore_types: [:foo, :bar])
+ html = Nokogiri::HTML(helper.output_buffer)
+ expect(html.css('.alert-notice').text).to eq('ok')
+ expect(html.css('.alert-foo').text).to be_empty
+ expect(html.css('.alert-bar').text).to be_empty
+ expect(helper.output_buffer).to eq('
ok
')
+ end
+ end
+
+ # Regression test for #2759
+ it 'nested_taxons_path works with a Taxon object' do
+ taxon = create(:taxon, name: 'iphone')
+ expect(spree.nested_taxons_path(taxon)).to eq("/t/#{taxon.parent.permalink}/#{taxon.name}")
+ end
+
+ context '#checkout_progress' do
+ before do
+ @order = create(:order, state: 'address')
+ end
+
+ it 'does not include numbers by default' do
+ output = checkout_progress
+ expect(output).not_to include('1.')
+ end
+
+ it 'has option to include numbers' do
+ output = checkout_progress(numbers: true)
+ expect(output).to include('1.')
+ end
+ end
+ end
+end
diff --git a/frontend/spec/helpers/taxons_helper_spec.rb b/frontend/spec/helpers/taxons_helper_spec.rb
new file mode 100644
index 00000000000..58a82ffb6ed
--- /dev/null
+++ b/frontend/spec/helpers/taxons_helper_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Spree::TaxonsHelper, type: :helper do
+ # Regression test for #4382
+ it '#taxon_preview' do
+ taxon = create(:taxon)
+ child_taxon = create(:taxon, parent: taxon)
+ product_1 = create(:product)
+ product_2 = create(:product)
+ product_3 = create(:product)
+ taxon.products << product_1
+ taxon.products << product_2
+ child_taxon.products << product_3
+
+ expect(taxon_preview(taxon.reload)).to eql([product_1, product_2, product_3])
+ end
+end
diff --git a/frontend/spec/requests/api_tokens_spec.rb b/frontend/spec/requests/api_tokens_spec.rb
new file mode 100644
index 00000000000..03013a4bceb
--- /dev/null
+++ b/frontend/spec/requests/api_tokens_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+require 'spree/api/testing_support/helpers'
+
+describe 'API Tokens Spec', type: :request do
+ include Spree::Api::TestingSupport::Helpers
+
+ shared_examples 'returns valid response' do
+ it 'with 200 HTTP status' do
+ expect(response.status).to eq(200)
+ end
+
+ it 'with all keys' do
+ expect(json_response).to have_key('order_token')
+ expect(json_response).to have_key('oauth_token')
+ end
+ end
+
+ context 'guest user' do
+ context 'with already created order' do
+ let(:order) { create(:order, user: nil, email: 'dummy@example.com') }
+
+ before do
+ allow_any_instance_of(Spree::StoreController).to receive_messages(current_order: order)
+ get '/api_tokens'
+ end
+
+ it 'returns order token' do
+ expect(response.status).to eq(200)
+ expect(json_response['order_token']).to eq(order.token)
+ expect(json_response['oauth_token']).to be_blank
+ end
+
+ it_behaves_like 'returns valid response'
+ end
+
+ context 'without order' do
+ before do
+ get '/api_tokens'
+ end
+
+ it 'returns blank tokens' do
+ expect(response.status).to eq(200)
+ expect(json_response['order_token']).to be_blank
+ expect(json_response['oauth_token']).to be_blank
+ end
+
+ it_behaves_like 'returns valid response'
+ end
+ end
+
+ context 'signed in user' do
+ let(:user) { create(:user) }
+
+ before do
+ allow_any_instance_of(Spree::StoreController).to receive_messages(try_spree_current_user: user)
+ end
+
+ context 'with already created order' do
+ let(:order) { create(:order, user: user) }
+
+ before do
+ allow_any_instance_of(Spree::StoreController).to receive_messages(current_order: order)
+ get '/api_tokens'
+ end
+
+ it 'returns order token and oauth token' do
+ expect(response.status).to eq(200)
+ expect(json_response['order_token']).to eq(order.token)
+ expect(json_response['oauth_token']).not_to be_blank
+ end
+
+ it_behaves_like 'returns valid response'
+ end
+
+ context 'without order' do
+ before do
+ get '/api_tokens'
+ end
+
+ it 'returns only oauth token' do
+ expect(response.status).to eq(200)
+ expect(json_response['order_token']).to be_blank
+ expect(json_response['oauth_token']).not_to be_blank
+ end
+
+ it_behaves_like 'returns valid response'
+ end
+ end
+end
diff --git a/frontend/spec/requests/ensure_cart_spec.rb b/frontend/spec/requests/ensure_cart_spec.rb
new file mode 100644
index 00000000000..b191b6e8868
--- /dev/null
+++ b/frontend/spec/requests/ensure_cart_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+require 'spree/api/testing_support/helpers'
+
+describe 'Ensure Cart Spec', type: :request do
+ include Spree::Api::TestingSupport::Helpers
+
+ let(:exec_post) { post '/ensure_cart' }
+
+ shared_examples 'returns current order' do
+ it 'with 200 HTTP status' do
+ expect(response.status).to eq(200)
+ end
+
+ it 'returns order' do
+ expect(json_response['token']).to eq(order.token)
+ expect(json_response['number']).to eq(order.number)
+ end
+ end
+
+ shared_examples 'creates new order' do
+ it 'and returns it' do
+ expect { exec_post }.to change { Spree::Order.count }.by(1)
+ expect(response.status).to eq(200)
+ order = Spree::Order.last
+ expect(json_response['token']).to eq(order.token)
+ expect(json_response['number']).to eq(order.number)
+ end
+ end
+
+ context 'guest user' do
+ context 'with already created order' do
+ let(:order) { create(:order, user: nil, email: 'dummy@example.com') }
+
+ before do
+ allow_any_instance_of(Spree::StoreController).to receive_messages(current_order: order)
+ exec_post
+ end
+
+ it_behaves_like 'returns current order'
+ end
+
+ context 'without order' do
+ it_behaves_like 'creates new order'
+ end
+ end
+
+ context 'signed in user' do
+ let(:user) { create(:user) }
+
+ context 'with already created order' do
+ let(:order) { create(:order, user: user, email: 'dummy@example.com') }
+
+ before do
+ allow_any_instance_of(Spree::StoreController).to receive_messages(current_order: order)
+ exec_post
+ end
+
+ it_behaves_like 'returns current order'
+ end
+
+ context 'without order' do
+ it_behaves_like 'creates new order'
+ end
+ end
+end
diff --git a/frontend/spec/spec_helper.rb b/frontend/spec/spec_helper.rb
new file mode 100644
index 00000000000..ec7e8fed0e6
--- /dev/null
+++ b/frontend/spec/spec_helper.rb
@@ -0,0 +1,121 @@
+if ENV['COVERAGE']
+ # Run Coverage report
+ require 'simplecov'
+ SimpleCov.start 'rails' do
+ add_group 'Libraries', 'lib/spree'
+
+ add_filter '/bin/'
+ add_filter '/db/'
+ add_filter '/script/'
+ add_filter '/spec/'
+
+ coverage_dir "#{ENV['COVERAGE_DIR']}/frontend" if ENV['COVERAGE_DIR']
+ end
+end
+
+# This file is copied to ~/spec when you run 'ruby script/generate rspec'
+# from the project root directory.
+ENV['RAILS_ENV'] ||= 'test'
+
+begin
+ require File.expand_path('../dummy/config/environment', __FILE__)
+rescue LoadError
+ puts 'Could not load dummy application. Please ensure you have run `bundle exec rake test_app`'
+ exit
+end
+
+require 'rspec/rails'
+require 'ffaker'
+
+# Requires supporting files with custom matchers and macros, etc,
+# in ./support/ and its subdirectories.
+Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
+
+require 'database_cleaner'
+require 'rspec/retry'
+
+require 'spree/testing_support/i18n' if ENV['CHECK_TRANSLATIONS']
+
+require 'spree/testing_support/authorization_helpers'
+require 'spree/testing_support/capybara_ext'
+require 'spree/testing_support/factories'
+require 'spree/testing_support/preferences'
+require 'spree/testing_support/controller_requests'
+require 'spree/testing_support/flash'
+require 'spree/testing_support/url_helpers'
+require 'spree/testing_support/order_walkthrough'
+require 'spree/testing_support/caching'
+require 'spree/testing_support/capybara_config'
+require 'spree/testing_support/image_helpers'
+
+RSpec.configure do |config|
+ config.color = true
+ config.default_formatter = 'doc'
+ config.fail_fast = ENV['FAIL_FAST'] || false
+ config.fixture_path = File.join(__dir__, 'fixtures')
+ config.infer_spec_type_from_file_location!
+ config.mock_with :rspec
+ config.raise_errors_for_deprecations!
+
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
+ # examples within a transaction, comment the following line or assign false
+ # instead of true.
+ config.use_transactional_fixtures = false
+
+ if ENV['WEBDRIVER'] == 'accessible'
+ config.around(:each, inaccessible: true) do |example|
+ Capybara::Accessible.skip_audit { example.run }
+ end
+ end
+
+ # Ensure DB is clean, so that transaction isolated specs see
+ # pristine state.
+ config.before(:suite) do
+ DatabaseCleaner.strategy = :truncation
+ DatabaseCleaner.clean
+ end
+
+ config.before do
+ WebMock.disable!
+ DatabaseCleaner.strategy = if RSpec.current_example.metadata[:js]
+ :truncation
+ else
+ :transaction
+ end
+ # TODO: Find out why open_transactions ever gets below 0
+ # See issue #3428
+ ApplicationRecord.connection.increment_open_transactions if ApplicationRecord.connection.open_transactions < 0
+ DatabaseCleaner.start
+ reset_spree_preferences
+ end
+
+ config.after do
+ DatabaseCleaner.clean
+ end
+
+ config.after(:each, type: :feature) do |example|
+ missing_translations = page.body.scan(/translation missing: #{I18n.locale}\.(.*?)[\s<\"&]/)
+ if missing_translations.any?
+ puts "Found missing translations: #{missing_translations.inspect}"
+ puts "In spec: #{example.location}"
+ end
+ end
+
+ config.include FactoryBot::Syntax::Methods
+
+ config.include Spree::TestingSupport::Preferences
+ config.include Spree::TestingSupport::UrlHelpers
+ config.include Spree::TestingSupport::ControllerRequests, type: :controller
+ config.include Spree::TestingSupport::Flash
+ config.include Spree::TestingSupport::ImageHelpers
+
+ config.order = :random
+ Kernel.srand config.seed
+
+ config.verbose_retry = true
+ config.display_try_failure_messages = true
+
+ config.around :each, type: :feature do |ex|
+ ex.run_with_retry retry: 3
+ end
+end
diff --git a/frontend/spec/support/add_to_cart.rb b/frontend/spec/support/add_to_cart.rb
new file mode 100644
index 00000000000..dc3fbd4eef6
--- /dev/null
+++ b/frontend/spec/support/add_to_cart.rb
@@ -0,0 +1,13 @@
+def add_to_cart(product_name)
+ visit spree.root_path
+ click_link product_name
+ expect(page).to have_selector('form#add-to-cart-form')
+ expect(page).to have_selector('button#add-to-cart-button')
+ wait_for_condition do
+ expect(page.find('#add-to-cart-button').disabled?).to eq(false)
+ end
+ click_button 'add-to-cart-button'
+ wait_for_condition do
+ expect(page).to have_content(Spree.t(:shopping_cart))
+ end
+end
diff --git a/frontend/spec/support/shared_contexts/checkout_setup.rb b/frontend/spec/support/shared_contexts/checkout_setup.rb
new file mode 100644
index 00000000000..b928e244f74
--- /dev/null
+++ b/frontend/spec/support/shared_contexts/checkout_setup.rb
@@ -0,0 +1,31 @@
+shared_context 'checkout setup' do
+ let!(:country) { create(:country, states_required: true) }
+ let!(:state) { create(:state, country: country) }
+ let!(:shipping_method) { create(:shipping_method) }
+ let!(:stock_location) { create(:stock_location) }
+ let!(:mug) { create(:product, name: 'RoR Mug') }
+ let!(:payment_method) { create(:check_payment_method) }
+ let!(:zone) { create(:zone) }
+ let!(:store) { create(:store) }
+end
+
+shared_context 'proceed to payment step' do
+ before do
+ add_to_cart('RoR Mug')
+ click_button 'Checkout'
+ fill_in 'order_email', with: 'spree@example.com'
+ fill_in 'First Name', with: 'John'
+ fill_in 'Last Name', with: 'Smith'
+ fill_in 'Street Address', with: '1 John Street'
+ fill_in 'City', with: 'City of John'
+ fill_in 'Zip', with: '01337'
+ select country.name, from: 'Country'
+ select state.name, from: 'order[bill_address_attributes][state_id]'
+ fill_in 'Phone', with: '555-555-5555'
+
+ # To shipping method screen
+ click_button 'Save and Continue'
+ # To payment screen
+ click_button 'Save and Continue'
+ end
+end
diff --git a/frontend/spec/support/shared_contexts/custom_products.rb b/frontend/spec/support/shared_contexts/custom_products.rb
new file mode 100644
index 00000000000..770f999f50d
--- /dev/null
+++ b/frontend/spec/support/shared_contexts/custom_products.rb
@@ -0,0 +1,25 @@
+shared_context 'custom products' do
+ before do
+ taxonomy = FactoryBot.create(:taxonomy, name: 'Categories')
+ root = taxonomy.root
+ clothing_taxon = FactoryBot.create(:taxon, name: 'Clothing', parent_id: root.id)
+ bags_taxon = FactoryBot.create(:taxon, name: 'Bags', parent_id: root.id)
+ mugs_taxon = FactoryBot.create(:taxon, name: 'Mugs', parent_id: root.id)
+
+ taxonomy = FactoryBot.create(:taxonomy, name: 'Brands')
+ root = taxonomy.root
+ apache_taxon = FactoryBot.create(:taxon, name: 'Apache', parent_id: root.id)
+ rails_taxon = FactoryBot.create(:taxon, name: 'Ruby on Rails', parent_id: root.id)
+ ruby_taxon = FactoryBot.create(:taxon, name: 'Ruby', parent_id: root.id)
+
+ FactoryBot.create(:custom_product, name: 'Ruby on Rails Ringer T-Shirt', price: '19.99', taxons: [rails_taxon, clothing_taxon])
+ FactoryBot.create(:custom_product, name: 'Ruby on Rails Mug', price: '15.99', taxons: [rails_taxon, mugs_taxon])
+ FactoryBot.create(:custom_product, name: 'Ruby on Rails Tote', price: '15.99', taxons: [rails_taxon, bags_taxon])
+ FactoryBot.create(:custom_product, name: 'Ruby on Rails Bag', price: '22.99', taxons: [rails_taxon, bags_taxon])
+ FactoryBot.create(:custom_product, name: 'Ruby on Rails Baseball Jersey', price: '19.99', taxons: [rails_taxon, clothing_taxon])
+ FactoryBot.create(:custom_product, name: 'Ruby on Rails Stein', price: '16.99', taxons: [rails_taxon, mugs_taxon])
+ FactoryBot.create(:custom_product, name: 'Ruby on Rails Jr. Spaghetti', price: '19.99', taxons: [rails_taxon, clothing_taxon])
+ FactoryBot.create(:custom_product, name: 'Ruby Baseball Jersey', price: '19.99', taxons: [ruby_taxon, clothing_taxon])
+ FactoryBot.create(:custom_product, name: 'Apache Baseball Jersey', price: '19.99', taxons: [apache_taxon, clothing_taxon])
+ end
+end
diff --git a/frontend/spec/support/shared_contexts/product_prototypes.rb b/frontend/spec/support/shared_contexts/product_prototypes.rb
new file mode 100644
index 00000000000..6dd159c8dd1
--- /dev/null
+++ b/frontend/spec/support/shared_contexts/product_prototypes.rb
@@ -0,0 +1,28 @@
+shared_context 'product prototype' do
+ def build_option_type_with_values(name, values)
+ ot = FactoryBot.create(:option_type, name: name)
+ values.each do |val|
+ ot.option_values.create(name: val.downcase, presentation: val)
+ end
+ ot
+ end
+
+ let(:product_attributes) do
+ # FactoryBot.attributes_for is un-deprecated!
+ # https://github.com/thoughtbot/factory_bot/issues/274#issuecomment-3592054
+ FactoryBot.attributes_for(:base_product)
+ end
+
+ let(:prototype) do
+ size = build_option_type_with_values('size', %w(Small Medium Large))
+ FactoryBot.create(:prototype, name: 'Size', option_types: [size])
+ end
+
+ let(:option_values_hash) do
+ hash = {}
+ prototype.option_types.each do |i|
+ hash[i.id.to_s] = i.option_value_ids
+ end
+ hash
+ end
+end
diff --git a/frontend/spec/views/spree/checkout/_summary_spec.rb b/frontend/spec/views/spree/checkout/_summary_spec.rb
new file mode 100644
index 00000000000..d661e8e173f
--- /dev/null
+++ b/frontend/spec/views/spree/checkout/_summary_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe 'spree/checkout/_summary.html.erb', type: :view do
+ # Regression spec for #4223
+ it 'does not use the @order instance variable' do
+ order = build(:order)
+ expect do
+ render partial: 'spree/checkout/summary', locals: { order: order }
+ end.not_to raise_error
+ end
+end
diff --git a/frontend/spree_frontend.gemspec b/frontend/spree_frontend.gemspec
new file mode 100644
index 00000000000..67f9ee48113
--- /dev/null
+++ b/frontend/spree_frontend.gemspec
@@ -0,0 +1,30 @@
+# encoding: UTF-8
+require_relative '../core/lib/spree/core/version.rb'
+
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'spree_frontend'
+ s.version = Spree.version
+ s.summary = 'Frontend e-commerce functionality for the Spree project.'
+ s.description = s.summary
+
+ s.required_ruby_version = '>= 2.3.3'
+
+ s.author = 'Sean Schofield'
+ s.email = 'sean@spreecommerce.com'
+ s.homepage = 'http://spreecommerce.org'
+ s.license = 'BSD-3-Clause'
+
+ s.files = `git ls-files`.split("\n").reject { |f| f.match(/^spec/) && !f.match(/^spec\/fixtures/) }
+ s.require_path = 'lib'
+ s.requirements << 'none'
+
+ s.add_dependency 'spree_api', s.version
+ s.add_dependency 'spree_core', s.version
+
+ s.add_dependency 'bootstrap-sass', '~> 3.4'
+ s.add_dependency 'canonical-rails', '~> 0.2.3'
+ s.add_dependency 'jquery-rails', '~> 4.3'
+
+ s.add_development_dependency 'capybara-accessible'
+end
diff --git a/frontend/vendor/assets/javascripts/accounting.min.js b/frontend/vendor/assets/javascripts/accounting.min.js
new file mode 100644
index 00000000000..8e09b868467
--- /dev/null
+++ b/frontend/vendor/assets/javascripts/accounting.min.js
@@ -0,0 +1,4 @@
+/*!
+ * accounting.js v0.4.2, copyright 2014 Open Exchange Rates, MIT license, http://openexchangerates.github.io/accounting.js
+ */
+(function(p,z){function q(a){return!!(""===a||a&&a.charCodeAt&&a.substr)}function m(a){return u?u(a):"[object Array]"===v.call(a)}function r(a){return"[object Object]"===v.call(a)}function s(a,b){var d,a=a||{},b=b||{};for(d in b)b.hasOwnProperty(d)&&null==a[d]&&(a[d]=b[d]);return a}function j(a,b,d){var c=[],e,h;if(!a)return c;if(w&&a.map===w)return a.map(b,d);for(e=0,h=a.length;ea?"-":"",g=parseInt(y(Math.abs(a||0),h),10)+"",l=3a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal))};c.formatColumn=function(a,b,d,i,e,h){if(!a)return[];var f=s(r(b)?b:{symbol:b,precision:d,thousand:i,decimal:e,format:h},c.settings.currency),g=x(f.format),l=g.pos.indexOf("%s")a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal));if(a.length>k)k=a.length;return a});return j(a,function(a){return q(a)&&a.length ] [ --svn ] [--db] [--hobo-src ] "
-USAGE = "USAGE: spree [ --edge ] "
-
-RAILSCART_REPO = "http://railscart.googlecode.com/svn/trunk/railscart/"
-
-
-### Nasty stuff needed for Windows :-( ###
-require 'rbconfig'
-
-if Config::CONFIG["arch"] =~ /win32/
- require "win32/registry"
- def system(command)
- win = Win32API.new("crtdll", "system", ['P'], 'L').Call(command)
- end
-end
-### end nasty stuff ###
-
-
-def command(*s)
- ok = system(s.join(' '))
- exit(1) unless ok
-end
-
-if ARGV.length == 0 || ARGV.include?("--help")
- puts USAGE
- exit 1
-end
-
-app_path = ARGV.pop
-plugins_dir = app_path + "/vendor/plugins"
-
-#user_model = "user"
-#hobo_svn = create_db = false
-
-edge = false
-
-until ARGV.empty?
- case ARGV.shift
- when "--edge"
- edge = true
- #when "--user-model"
- # arg = ARGV.shift
- # user_model = arg == "false" ? nil : arg
- #when "--svn"
- # hobo_svn = true
- #when "--db"
- # create_db = true
- #when "--hobo-src"
- # hobo_src = "../" + ARGV.shift
- else
- puts USAGE
- exit 1
- end
-end
-
-puts "\nGenerating Spree application...\n"
-
-# Create a new rails app by copying the starter app and renaming
-FileUtils.cp_r starter_app, app_path
-FileUtils.mkdir_p "#{app_path}/log"
-
-# setup database config
-#require 'erb'
-#File.open("#{app_path}/config/database.yml", 'w') {|f| f.write ERB.new(IO.read("configs/databases/mysql.yml"), nil, '-').result(binding)}
-
-# replace plugins with latest versions from SVN
-if edge
- FileUtils.remove_dir plugins_dir
- FileUtils.mkdir_p plugins_dir
- puts "\nGrabbing edge versions of required plugins from SVN ...\n"
-
- Dir.chdir(app_path) do
- gen = "ruby #{File.join('script', 'generate')}"
- plugin = "ruby #{File.join('script', 'plugin')}"
-
- puts "\nInstalling acts_as_list\n"
- command(plugin, "install http://svn.rubyonrails.org/rails/plugins/acts_as_list")
-
- puts "\nInstalling acts_as_tree\n"
- command(plugin, "install http://svn.rubyonrails.org/rails/plugins/acts_as_tree")
-
- puts "\nInstalling attachment_fu\n"
- command(plugin, "install http://svn.techno-weenie.net/projects/plugins/attachment_fu")
-
- puts "\nInstalling calendar_date_select\n"
- command(plugin, "install http://calendardateselect.googlecode.com/svn/tags/calendar_date_select")
-
- puts "\nInstalling engines\n"
- command(plugin, "install http://svn.rails-engines.org/engines/trunk")
-
- puts "\nInstalling in_place_editing\n"
- command(plugin, "install http://svn.rubyonrails.org/rails/plugins/in_place_editing")
-
- puts "\nInstalling paginating_find\n"
- command(plugin, "install http://svn.cardboardrocket.com/paginating_find")
-
- puts "\nInstalling railscart\n"
- command(plugin, "install http://railscart.googlecode.com/svn/railscart/trunk/")
-
- end
-
-end
-
-#Dir.chdir(app_path) do
- #gen = "ruby #{File.join('script', 'generate')}"
- #plugin = "ruby #{File.join('script', 'plugin')}"
-
- #FileUtils.touch("public/stylesheets/application.css")
-
- #puts "\nInstalling classic_pagination\n"
- #command(plugin, "install svn://errtheblog.com/svn/plugins/classic_pagination")
-
- #for now we're going to force install from svn
- #puts "\nInstalling Railscart plugin via svn checkout...\n"
- #command("svn co #{RAILSCART_REPO} vendor/plugins/railscart")
-
- #if hobo_svn
- # puts "\nInstalling Hobo plugin via svn checkout...\n"
- # command("svn co #{HOBO_REPO} vendor/plugins/hobo")
- #else
- # puts "\nInstalling Hobo plugin...\n"
- # FileUtils.cp_r hobo_src, "vendor/plugins/hobo"
- #end
-
- #puts "\nInitialising Hobo...\n"
- #command(gen, "hobo --add-routes")
-
- #puts "\nInstalling Hobo Rapid and default theme...\n"
- #command("#{gen} hobo_rapid --import-tags")
-
- #if user_model
- # puts "\nCreating #{user_model} model and controller...\n"
- # command("#{gen} hobo_user_model #{user_model}")
- # command("#{gen} hobo_user_controller #{user_model}")
- #end
-
- #puts "\nCreating standard pages...\n"
- #command("#{gen} hobo_front_controller front --delete-index --add-routes")
-
- #if create_db
- # puts "\nCreating databases"
- # command("rake db:create:all")
- #end
-#end
-
-puts "Finished."
-
diff --git a/gem-refactor/config/hoe.rb b/gem-refactor/config/hoe.rb
deleted file mode 100644
index f134e7851bb..00000000000
--- a/gem-refactor/config/hoe.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-require 'spree/version'
-
-AUTHOR = 'Sean Schofield' # can also be an array of Authors
-EMAIL = "sean.schofield@gmail.com"
-DESCRIPTION = "Complete commerce solution for Ruby on Rails"
-GEM_NAME = 'spree' # what ppl will type to install your gem
-RUBYFORGE_PROJECT = 'spree' # The unix name for your project
-HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
-DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
-
-@config_file = "~/.rubyforge/user-config.yml"
-@config = nil
-RUBYFORGE_USERNAME = "schof"
-def rubyforge_username
- unless @config
- begin
- @config = YAML.load(File.read(File.expand_path(@config_file)))
- rescue
- puts <<-EOS
-ERROR: No rubyforge config file found: #{@config_file}
-Run 'rubyforge setup' to prepare your env for access to Rubyforge
- - See http://newgem.rubyforge.org/rubyforge.html for more details
- EOS
- exit
- end
- end
- RUBYFORGE_USERNAME.replace @config["username"]
-end
-
-
-REV = nil
-# UNCOMMENT IF REQUIRED:
-# REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
-VERS = Spree::VERSION::STRING + (REV ? ".#{REV}" : "")
-RDOC_OPTS = ['--quiet', '--title', 'spree documentation',
- "--opname", "index.html",
- "--line-numbers",
- "--main", "README",
- "--inline-source"]
-
-class Hoe
- def extra_deps
- @extra_deps.reject! { |x| Array(x).first == 'hoe' }
- @extra_deps
- end
-end
-
-# Generate all the Rake tasks
-# Run 'rake -T' to see list of generated tasks (from gem root directory)
-hoe = Hoe.new(GEM_NAME, VERS) do |p|
- p.author = AUTHOR
- p.description = DESCRIPTION
- p.email = EMAIL
- p.summary = DESCRIPTION
- p.url = HOMEPATH
- p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
- p.test_globs = ["test/**/test_*.rb"]
- p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
-
- # == Optional
- p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
-
- p.extra_deps = [
- ['rails', '>= 2.0.0'],
- ['activemerchant', '>= 1.2.1'],
- ['mocha', '>= 0.5.6'],
- ['has_many_polymorphs', '>= 2.12'],
- ['mini_magick', ">=1.2.3"],
- ['rspec', '=1.1.3'] #rspec version needs to exactly match what is required by the rspec plugin
- ]
- #p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
-
- #p.spec_extras = {} # A hash of extra values to set in the gemspec.
-
-end
-
-CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
-PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
-hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
-hoe.rsync_args = '-av --delete --ignore-errors'
\ No newline at end of file
diff --git a/gem-refactor/config/requirements.rb b/gem-refactor/config/requirements.rb
deleted file mode 100644
index 9292b696aba..00000000000
--- a/gem-refactor/config/requirements.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'fileutils'
-include FileUtils
-
-require 'rubygems'
-%w[rake hoe newgem rubigen].each do |req_gem|
- begin
- require req_gem
- rescue LoadError
- puts "This Rakefile requires the '#{req_gem}' RubyGem."
- puts "Installation: gem install #{req_gem} -y"
- exit
- end
-end
-
-$:.unshift(File.join(File.dirname(__FILE__), %w[.. lib]))
diff --git a/gem-refactor/lib/spree/version.rb b/gem-refactor/lib/spree/version.rb
deleted file mode 100644
index 80bccc13da4..00000000000
--- a/gem-refactor/lib/spree/version.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Spree #:nodoc:
- module VERSION #:nodoc:
- MAJOR = 0
- MINOR = 0
- TINY = 9
-
- STRING = [MAJOR, MINOR, TINY].join('.')
- end
-end
diff --git a/gem-refactor/script/destroy b/gem-refactor/script/destroy
deleted file mode 100755
index 5fa7e10e71d..00000000000
--- a/gem-refactor/script/destroy
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env ruby
-APP_ROOT = File.join(File.dirname(__FILE__), '..')
-
-begin
- require 'rubigen'
-rescue LoadError
- require 'rubygems'
- require 'rubigen'
-end
-require 'rubigen/scripts/destroy'
-
-ARGV.shift if ['--help', '-h'].include?(ARGV[0])
-RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
-RubiGen::Scripts::Destroy.new.run(ARGV)
diff --git a/gem-refactor/script/generate b/gem-refactor/script/generate
deleted file mode 100755
index 230a1868569..00000000000
--- a/gem-refactor/script/generate
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env ruby
-APP_ROOT = File.join(File.dirname(__FILE__), '..')
-
-begin
- require 'rubigen'
-rescue LoadError
- require 'rubygems'
- require 'rubigen'
-end
-require 'rubigen/scripts/generate'
-
-ARGV.shift if ['--help', '-h'].include?(ARGV[0])
-RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
-RubiGen::Scripts::Generate.new.run(ARGV)
diff --git a/gem-refactor/script/txt2html b/gem-refactor/script/txt2html
deleted file mode 100755
index 58e9ef82c4b..00000000000
--- a/gem-refactor/script/txt2html
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env ruby
-
-require 'rubygems'
-begin
- require 'newgem'
-rescue LoadError
- puts "\n\nGenerating the website requires the newgem RubyGem"
- puts "Install: gem install newgem\n\n"
- exit(1)
-end
-require 'redcloth'
-require 'syntax/convertors/html'
-require 'erb'
-require File.dirname(__FILE__) + '/../lib/spree/version.rb'
-
-version = Spree::VERSION::STRING
-download = 'http://rubyforge.org/projects/spree'
-
-class Fixnum
- def ordinal
- # teens
- return 'th' if (10..19).include?(self % 100)
- # others
- case self % 10
- when 1: return 'st'
- when 2: return 'nd'
- when 3: return 'rd'
- else return 'th'
- end
- end
-end
-
-class Time
- def pretty
- return "#{mday}#{mday.ordinal} #{strftime('%B')} #{year}"
- end
-end
-
-def convert_syntax(syntax, source)
- return Syntax::Convertors::HTML.for_syntax(syntax).convert(source).gsub(%r!^
+ )
+ }
+}
diff --git a/guides/src/components/helpers/Status.js b/guides/src/components/helpers/Status.js
new file mode 100644
index 00000000000..b768a17a6ce
--- /dev/null
+++ b/guides/src/components/helpers/Status.js
@@ -0,0 +1,27 @@
+// --- Dependencies
+import * as React from 'react'
+import PropTypes from 'prop-types'
+
+// --- Utils
+import STATUS from '../../data/status'
+
+/**
+ * Component
+ */
+
+const Status = ({ code }) => (
+
+
+ Status:
+
+
+ {STATUS[code]}
+
+
+)
+
+Status.propTypes = {
+ code: PropTypes.oneOf(Object.keys(STATUS))
+}
+
+export default Status
diff --git a/guides/src/content/api/addresses.md b/guides/src/content/api/addresses.md
new file mode 100644
index 00000000000..86747d89f8a
--- /dev/null
+++ b/guides/src/content/api/addresses.md
@@ -0,0 +1,59 @@
+---
+title: Address
+description: Use the Spree Commerce storefront API to access Address data.
+---
+
+## Show
+
+Retrieve details about a particular address:
+
+```text
+GET /api/v1/orders/1/addresses/1
+```
+
+Order addresses through the API will only be visible to admins and the users who own particular orders related to that addresses.
+If a user attempts to access an order address that does not belong to him, he
+will be met with an authorization error.
+
+Users may pass in the order's token in order to be authorized to view an order address:
+
+```text
+GET /api/v1/orders/1/addresses/1?order_token=abcdef123456
+```
+
+The `order_token` parameter will work for authorizing any action for an order address within Spree's API.
+
+### Response
+
+
+
+
+## Update
+
+To update an address, make a request like this:
+
+```text
+PUT /api/v1/orders/1/addresses/1?address[firstname]=Ryan
+```
+
+This request will update the `firstname` field for an address to the value of \"Ryan\"
+
+### Response
+
+
+
+
+Valid address fields are:
+
+* firstname
+* lastname
+* company
+* address1
+* address2
+* city
+* zipcode
+* phone
+* alternative_phone
+* country_id
+* state_id
+* state_name
diff --git a/guides/src/content/api/checkouts.md b/guides/src/content/api/checkouts.md
new file mode 100644
index 00000000000..06debc5e5cc
--- /dev/null
+++ b/guides/src/content/api/checkouts.md
@@ -0,0 +1,415 @@
+---
+title: Checkouts
+description: Use the Spree Commerce storefront API to access Checkout data.
+---
+
+# Checkouts API
+
+## Introduction
+
+The checkout API functionality can be used to advance an existing order's state.
+Sending a `PUT` request to `/api/v1/checkouts/:number` will advance an order's
+state or, failing that, report any errors.
+
+The following sections will walk through creating a new order and advancing an order from its `cart` state to its `complete` state.
+
+## Creating a blank order
+
+To create a new, empty order, make this request:
+
+```
+POST /api/v1/orders.json
+```
+
+### Response
+
+
+
+
+Any time you update the order or move a checkout step you'll get
+a response similar as above along with the new associated objects. e.g. addresses,
+payments, shipments.
+
+## Add line items to an order
+
+Pass line item attributes like this:
+
+```json
+{
+ "line_item": {
+ "variant_id": 1,
+ "quantity": 5
+ }
+}
+```
+
+to this api endpoint:
+
+```
+POST /api/v1/orders/:number/line_items.json
+```
+
+
+
+
+## Updating an order
+
+To update an order you must be authenticated as the order's user, and perform a request like this:
+
+```
+PUT /api/v1/orders/:number.json
+```
+
+If you know the order's token, then you can also update the order:
+
+```
+PUT /api/v1/orders/:number.json?order_token=abcdef123456
+```
+
+Requests performed as a non-admin or non-authorized user will be met with a 401 response from this action.
+
+## Address
+
+To transition an order to its next step, make a request like this:
+
+```
+PUT /api/v1/checkouts/:number/next.json
+```
+
+If the request is successful you'll get a 200 response using the same order
+template shown when creating the order with the state updated. See example of
+failed response below.
+
+### Failed Response
+
+
+
+
+## Delivery
+
+To advance to the next state, `delivery`, the order will first need both a shipping and billing address.
+
+In order to update the addresses, make this request with the necessary parameters:
+
+```
+PUT /api/v1/checkouts/:number.json
+```
+
+As an example, here are the required address attributes and how they should be formatted:
+
+```json
+{
+ "order": {
+ "bill_address_attributes": {
+ "firstname": "John",
+ "lastname": "Doe",
+ "address1": "233 36th Ave Ne",
+ "city": "St Petersburg",
+ "phone": "3014445002",
+ "zipcode": "33704-1535",
+ "state_id": 3534,
+ "country_id": 232
+ },
+ "ship_address_attributes": {
+ "firstname": "John",
+ "lastname": "Doe",
+ "address1": "233 36th Ave Ne",
+ "city": "St Petersburg",
+ "phone": "3014445002",
+ "zipcode": "33704-1535",
+ "state_id": 3534,
+ "country_id": 232
+ }
+ }
+}
+```
+
+### Response
+
+Once valid address information has been submitted, the shipments and shipping rates
+available for this order will be returned inside a `shipments` key inside the order,
+as seen below:
+
+
+```json
+{
+ ...
+ "shipments": [
+ {
+ "id": 1,
+ "tracking": null,
+ "number": "H71216494427",
+ "cost": "5.0",
+ "shipped_at": null,
+ "state": "pending",
+ "shipping_rates": [
+ {
+ "id": 1,
+ "name": "UPS Ground (USD)",
+ "cost": "5.0",
+ "selected": true,
+ "shipping_method_id": 1,
+ "shipping_method_code": null,
+ "display_cost": "$5.00"
+ },
+ {
+ "id": 2,
+ "name": "UPS Two Day (USD)",
+ "cost": "10.0",
+ "selected": false,
+ "shipping_method_id": 2,
+ "shipping_method_code": null,
+ "display_cost": "$10.00"
+ },
+ {
+ "id": 3,
+ "name": "UPS One Day (USD)",
+ "cost": "15.0",
+ "selected": false,
+ "shipping_method_id": 3,
+ "shipping_method_code": null,
+ "display_cost": "$15.00"
+ }
+ ],
+ "selected_shipping_rate": {
+ "id": 1,
+ "name": "UPS Ground (USD)",
+ "cost": "5.0",
+ "selected": true,
+ "shipping_method_id": 1,
+ "shipping_method_code": null,
+ "display_cost": "$5.00"
+ },
+ "shipping_methods": [
+ {
+ "id": 1,
+ "code": null,
+ "name": "UPS Ground (USD)",
+ "zones": [
+ {
+ "id": 2,
+ "name": "North America",
+ "description": "USA + Canada"
+ }
+ ],
+ "shipping_categories": [
+ {
+ "id": 1,
+ "name": "Default"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "code": null,
+ "name": "UPS Two Day (USD)",
+ "zones": [
+ {
+ "id": 2,
+ "name": "North America",
+ "description": "USA + Canada"
+ }
+ ],
+ "shipping_categories": [
+ {
+ "id": 1,
+ "name": "Default"
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "code": null,
+ "name": "UPS One Day (USD)",
+ "zones": [
+ {
+ "id": 2,
+ "name": "North America",
+ "description": "USA + Canada"
+ }
+ ],
+ "shipping_categories": [
+ {
+ "id": 1,
+ "name": "Default"
+ }
+ ]
+ }
+ ],
+ "manifest": [
+ {
+ "variant_id": 1,
+ "quantity": 5,
+ "states": {
+ "on_hand": 5
+ }
+ }
+ ],
+ "adjustments": [],
+ "order_id": "R608623713",
+ "stock_location_name": "default"
+ }
+ ],
+ ...
+```
+
+## Payment
+
+To advance to the next state, `payment`, you will need to select a shipping rate
+for each shipment for the order. These were returned when transitioning to the
+`delivery` step. If you want to see them again, make the following request:
+
+```
+GET /api/v1/orders/:number.json
+```
+
+Spree will select a shipping rate by default so you can advance to the `payment`
+state by making this request:
+
+```
+PUT /api/v1/checkouts/:number/next.json
+```
+
+If the order doesn't have an assigned shipping rate, or you want to choose a different
+shipping rate make the following request to select one and advance the order's state:
+
+```
+PUT /api/v1/checkouts/:number.json
+```
+
+With parameters such as these:
+
+```json
+{
+ "order": {
+ "shipments_attributes": {
+ "0": {
+ "selected_shipping_rate_id": 1,
+ "id": 1
+ }
+ }
+ }
+}
+```
+
+
+Please ensure you select a shipping rate for each shipment in the order. In the request above, the `selected_shipping_rate_id` should be the id of the shipping rate you want to use and the `id` should be the id of the shipment you are choosing this shipping rate for.
+
+
+## Confirm
+
+To advance to the next state, `confirm`, the order will need to have a payment.
+You can create a payment by passing in parameters such as this:
+
+```json
+{
+ "order": {
+ "payments_attributes": [{
+ "payment_method_id": "1"
+ }]
+ },
+ "payment_source": {
+ "1": {
+ "number": "4111111111111111",
+ "month": "1",
+ "year": "2017",
+ "verification_value": "123",
+ "name": "John Smith"
+ }
+ }
+```
+
+
+ The numbered key in the `payment_source` hash directly corresponds to the
+`payment_method_id` attribute within the `payment_attributes` key.
+
+
+You can also use an existing card for the order by submitting the credit card
+id. See an example request:
+
+```json
+{
+ "order": {
+ "existing_card": "1"
+ }
+}
+```
+
+_Please note that for 2-2-stable checkout api the request body to submit a payment
+via api/checkouts is slight different. See example:_
+
+```json
+{
+ "order": {
+ "payments_attributes": {
+ "payment_method_id": "1"
+ },
+ "payment_source": {
+ "1": {
+ "number": "4111111111111111",
+ "month": "1",
+ "year": "2017",
+ "verification_value": "123",
+ "name": "John Smith"
+ }
+ }
+ }
+}
+```
+
+If the order already has a payment, you can advance it to the `confirm` state by making this request:
+
+```
+PUT /api/v1/checkouts/:number.json
+```
+
+For more information on payments, view the [payments documentation](payments).
+
+### Response
+
+
+```json
+{
+ ...
+ "state": "confirm",
+ ...
+ "payments": [
+ {
+ "id": 3,
+ "source_type": "Spree::CreditCard",
+ "source_id": 2,
+ "amount": "65.37",
+ "display_amount": "$65.37",
+ "payment_method_id": 1,
+ "state": "checkout",
+ "avs_response": null,
+ "created_at": "2014-07-06T19:55:08.308Z",
+ "updated_at": "2014-07-06T19:55:08.308Z",
+ "number": "PNTS91GF",
+ "payment_method": {
+ "id": 1,
+ "name": "Credit Card"
+ },
+ "source": {
+ "id": 2,
+ "month": "1",
+ "year": "2017",
+ "cc_type": null,
+ "last_digits": "1111",
+ "name": "John Smith"
+ }
+ }
+ ],
+ ...
+}
+```
+
+## Complete
+
+Now the order is ready to be advanced to the final state, `complete`. To accomplish this, make this request:
+
+```
+PUT /api/v1/checkouts/:number.json
+```
+
+You should get a 200 response with all the order info.
diff --git a/guides/src/content/api/countries.md b/guides/src/content/api/countries.md
new file mode 100644
index 00000000000..684ea0f2834
--- /dev/null
+++ b/guides/src/content/api/countries.md
@@ -0,0 +1,71 @@
+---
+title: Countries
+description: Use the Spree Commerce storefront API to access Country data.
+---
+
+## Index
+
+Retrieve a list of all countries by making this request:
+
+```
+GET /api/v1/countries
+```
+
+Countries are paginated and can be iterated through by passing along a `page` parameter:
+
+```
+GET /api/v1/countries?page=2
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a particular country, make a request like this:
+
+```
+GET /api/v1/countries?q[name_cont]=united
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```
+GET /api/v1/countries?q[s]=name%20desc
+```
+
+## Show
+
+Retrieve details about a particular country:
+
+```
+GET /api/v1/countries/1
+```
+
+### Response
+
+
+
diff --git a/guides/src/content/api/index.md b/guides/src/content/api/index.md
new file mode 100644
index 00000000000..06c8068071e
--- /dev/null
+++ b/guides/src/content/api/index.md
@@ -0,0 +1,11 @@
+---
+title: API
+---
+
+## Spree API Guide
+
+This site covers the inner working of Spree\'s RESTful API. It assumes a basic understanding of the principles of REST.
+
+The REST API is designed to give developers a convenient way to access data contained within Spree. With a standard read/write interface to store data, it is now very simple to write third party applications (eg. iPhone) that can talk to your Spree store. It is also possible to build sophisticated middleware applications that can serve as a bridge between Spree and a warehouse or inventory system.
+
+For a comprehensive list of API functions, start browsing the resources in the above diagram.
diff --git a/guides/src/content/api/line_items.md b/guides/src/content/api/line_items.md
new file mode 100644
index 00000000000..f98eb379b7e
--- /dev/null
+++ b/guides/src/content/api/line_items.md
@@ -0,0 +1,42 @@
+---
+title: Line Items
+description: Use the Spree Commerce storefront API to access LineItem data.
+---
+
+# Line Items API
+
+## Create
+
+To create a new line item, make a request like this:
+
+ POST /api/v1/orders/R1234567/line_items?line_item[variant_id]=1&line_item[quantity]=1
+
+This will create a new line item representing a single item for the variant with the id of 1.
+
+### Response
+
+
+
+
+## Update
+
+To update the information for a line item, make a request like this:
+
+ PUT /api/v1/orders/R1234567/line_items/1?line_item[variant_id]=1&line_item[quantity]=1
+
+This request will update the line item with the ID of 1 for the order, updating the line item's `variant_id` to 1, and its `quantity` 1.
+
+### Response
+
+
+
+
+## Delete
+
+To delete a line item, make a request like this:
+
+ DELETE /api/v1/orders/R1234567/line_items/1
+
+### Response
+
+
diff --git a/guides/src/content/api/option_types.md b/guides/src/content/api/option_types.md
new file mode 100644
index 00000000000..b19697a8169
--- /dev/null
+++ b/guides/src/content/api/option_types.md
@@ -0,0 +1,165 @@
+---
+title: Option Types
+description: Use the Spree Commerce storefront API to access OptionType data.
+---
+
+## Index
+
+Retrieve a list of option types by making this request:
+
+``` text
+GET /api/v1/option_types
+```
+
+### Parameters
+
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a specific option type, make a request like this:
+
+```text
+GET /api/v1/option_types?q[name_cont]=color
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```text
+GET /api/v1/option_types?q[s]=name%20asc
+```
+
+## Show
+
+Retrieve details about a particular option type:
+
+```text
+GET /api/v1/option_types/1
+```
+
+### Response
+
+
+
+
+## New
+
+You can learn about the potential attributes (required and non-required) for a option type by making this request:
+
+```text
+GET /api/v1/option_types/new
+```
+
+### Response
+
+
+```json
+{
+ "attributes": [
+ "id", "name", "presentation", "position"
+ ],
+ "required_attributes": [
+ "name", "presentation"
+ ]
+}
+```
+
+## Create
+
+
+
+To create a new option type through the API, make this request with the necessary parameters:
+
+```text
+POST /api/v1/option_types
+```
+
+For instance, a request to create a new option type called "tshirt-category" with a presentation value of "Category" would look like this:
+
+```text
+POST api/v1/option_types/?option_type[name]=tshirt-category&option_type[presentation]=Category
+```
+
+### Successful Response
+
+
+
+### Failed Response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {
+ "name": ["can't be blank"],
+ "presentation": ["can't be blank"]
+ }
+}
+```
+
+## Update
+
+
+
+To update a option type's details, make this request with the necessary parameters:
+
+```text
+PUT /api/v1/option_types/1
+```
+
+For instance, to update a option types's name, send it through like this:
+
+```text
+PUT /api/v1/option_types/3?option_type[name]=t-category
+```
+
+### Successful Response
+
+
+
+### Failed Response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {
+ "name": ["can't be blank"],
+ "presentation": ["can't be blank"]
+ }
+}
+```
+
+## Delete
+
+
+
+To delete a option type, make this request:
+
+```text
+DELETE /api/v1/option_types/1
+```
+
+This request removes a option type from database.
+
+
diff --git a/guides/src/content/api/option_values.md b/guides/src/content/api/option_values.md
new file mode 100644
index 00000000000..c3213e9bde9
--- /dev/null
+++ b/guides/src/content/api/option_values.md
@@ -0,0 +1,169 @@
+---
+title: Option Values
+description: Use the Spree Commerce storefront API to access OptionValue data.
+---
+
+## Index
+
+Retrieve a list of option values by making this request:
+
+``` text
+GET /api/v1/option_values
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a specific option value, make a request like this:
+
+```text
+GET /api/v1/option_values?q[name_cont]=red
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```text
+GET /api/v1/option_values?q[s]=name%20asc
+```
+
+## Show
+
+Retrieve details about a particular option value:
+
+```text
+GET /api/v1/option_values/1
+```
+
+### Response
+
+
+
+
+## New
+
+You can learn about the potential attributes (required and non-required) for a option value by making this request:
+
+```text
+GET /api/v1/option_values/new
+```
+
+### Response
+
+
+```json
+{
+ "attributes": [
+ "id", "name", "presentation", "option_type_name", "option_type_id",
+ "option_type_presentation"
+ ],
+ "required_attributes": [
+ "name", "presentation"
+ ]
+}
+```
+
+## Create
+
+
+
+To create a new option value through the API, make this request with the necessary parameters:
+
+```text
+POST /api/v1/option_values
+```
+
+For instance, a request to create a new option value called "sports" with a presentation value of "Sports" would look like this:
+
+```text
+POST /api/v1/option_values?option_value[name]=sports&option_value[presentation]=Sports
+```
+
+### Successful Response
+
+
+
+### Failed Response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {
+ "name": ["can't be blank"],
+ "presentation": ["can't be blank"]
+ }
+}
+```
+
+## Update
+
+
+
+To update an option value's details, make this request with the necessary parameters:
+
+```text
+PUT /api/v1/option_values/1
+```
+
+For instance, to update an option value's name, send it through like this:
+
+```text
+PUT /api/v1/option_values/1?option_value[name]=sport&option_value[presentation]=Sport
+```
+
+### Successful Response
+
+
+
+### Failed Response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {
+ "name": ["can't be blank"],
+ "presentation": ["can't be blank"]
+ }
+}
+```
+
+
+## Delete
+
+
+
+To delete an option value, make this request:
+
+```text
+DELETE /api/v1/option_values/1
+```
+
+This request removes an option value from database.
+
+
diff --git a/guides/src/content/api/orders.md b/guides/src/content/api/orders.md
new file mode 100644
index 00000000000..efe229421f8
--- /dev/null
+++ b/guides/src/content/api/orders.md
@@ -0,0 +1,167 @@
+---
+title: Orders
+description: Use the Spree Commerce storefront API to access Order data.
+---
+
+## Index
+
+
+
+Retrieve a list of orders by making this request:
+
+```text
+GET /api/v1/orders
+```
+
+Orders are paginated and can be iterated through by passing along a `page` parameter:
+
+```text
+GET /api/v1/orders?page=2
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+
+
+To search for a particular order, make a request like this:
+
+```text
+GET /api/v1/orders?q[email_cont]=bob
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `email_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```text
+GET /api/v1/orders?q[s]=number%20desc
+```
+
+It is also possible to sort results using an associated object's field.
+
+```text
+GET /api/v1/orders?q[s]=user_name%20asc
+```
+
+## Show
+
+To view the details for a single order, make a request using that order\'s number:
+
+```text
+GET /api/v1/orders/R123456789
+```
+
+Orders through the API will only be visible to admins and the users who own
+them. If a user attempts to access an order that does not belong to them, they
+will be met with an authorization error.
+
+Users may pass in the order's token in order to be authorized to view an order:
+
+```text
+GET /api/v1/orders/R123456789?order_token=abcdef123456
+```
+
+The `order_token` parameter will work for authorizing any action for an order within Spree's API.
+
+### Successful Response
+
+
+
+
+### Not Found Response
+
+
+
+### Authorization Failure
+
+
+
+## Show (delivery)
+
+When an order is in the "delivery" state, additional shipments information will be returned in the API:
+
+
+
+## Create
+
+To create a new order through the API, make this request:
+
+```text
+POST /api/v1/orders
+```
+
+If you wish to create an order with a line item matching to a variant whose ID is \"1\" and quantity is 5, make this request:
+
+```text
+POST /api/v1/orders
+```
+
+```json
+{
+ "order": {
+ "line_items": [
+ { "variant_id": 1, "quantity": 5 }
+ ]
+ }
+}
+```
+
+### Successful response
+
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {
+ "name": ["can't be blank"],
+ "price": ["can't be blank"]
+ }
+}
+```
+
+## Update Address
+
+To add address information to an order, please see the [checkout transitions](checkouts#checkout-transitions) section of the Checkouts guide.
+
+## Empty
+
+To empty an order\'s cart, make this request:
+
+```text
+PUT /api/v1/orders/R1234567/empty
+```
+
+All line items will be removed from the cart and the order\'s information will
+be cleared. Inventory that was previously depleted by this order will be
+repleted.
diff --git a/guides/src/content/api/payments.md b/guides/src/content/api/payments.md
new file mode 100644
index 00000000000..ea0c4af37fe
--- /dev/null
+++ b/guides/src/content/api/payments.md
@@ -0,0 +1,213 @@
+---
+title: Payments
+description: Use the Spree Commerce storefront API to access Payment data.
+---
+
+# Payments API
+
+## Index
+
+To see details about an order's payments, make this request:
+
+ GET /api/v1/orders/R1234567/payments
+
+Payments are paginated and can be iterated through by passing along a `page` parameter:
+
+ GET /api/v1/orders/R1234567/payments?page=2
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a particular payment, make a request like this:
+
+ GET /api/v1/orders/R1234567/payments?q[response_code_cont]=123
+
+The searching API is provided through the Ransack gem which Spree depends on. The `response_code_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+ GET /api/v1/payments?q[s]=state%20desc
+
+## New
+
+In order to create a new payment, you will need to know about the available payment methods and attributes. To find these out, make this request:
+
+ GET /api/v1/orders/R1234567/payments/new
+
+### Response
+
+
+```json
+{
+ "attributes": ["id", "source_type", "source_id", "amount",
+ "display_amount", "payment_method_id", "state", "avs_response",
+ "created_at", "updated_at", "number"],
+ "payment_methods": [Spree::Resources::PAYMENT_METHOD]
+}
+```
+
+## Create
+
+To create a new payment, make a request like this:
+
+ POST /api/v1/orders/R1234567/payments?payment[payment_method_id]=1&payment[amount]=10
+
+### Response
+
+
+
+
+## Show
+
+To get information for a particular payment, make a request like this:
+
+ GET /api/v1/orders/R1234567/payments/1
+
+### Response
+
+
+
+
+## Authorize
+
+To authorize a payment, make a request like this:
+
+ PUT /api/v1/orders/R1234567/payments/1/authorize
+
+### Response
+
+
+
+
+### Failed Response
+
+
+```json
+{
+ "error": "There was a problem with the payment gateway: [text]"
+}
+```
+
+## Capture
+
+
+ Capturing a payment is typically done shortly after authorizing the payment. If you are auto-capturing payments, you may be able to use the purchase endpoint instead.
+
+
+To capture a payment, make a request like this:
+
+ PUT /api/v1/orders/R1234567/payments/1/capture
+
+### Response
+
+
+
+
+### Failed Response
+
+
+```json
+{
+ "error": "There was a problem with the payment gateway: [text]"
+}
+```
+
+## Purchase
+
+
+ Purchasing a payment is typically done only if you are not authorizing payments before-hand. If you are authorizing payments, then use the authorize and capture endpoints instead.
+
+
+To make a purchase with a payment, make a request like this:
+
+ PUT /api/v1/orders/R1234567/payments/1/purchase
+
+### Response
+
+
+
+
+### Failed Response
+
+
+```json
+{
+ "error": "There was a problem with the payment gateway: [text]"
+}
+```
+
+## Void
+
+To void a payment, make a request like this:
+
+ PUT /api/v1/orders/R1234567/payments/1/void
+
+### Response
+
+
+
+
+### Failed Response
+
+
+```json
+{
+ "error": "There was a problem with the payment gateway: [text]"
+}
+```
+
+## Credit
+
+To credit a payment, make a request like this:
+
+ PUT /api/v1/orders/R1234567/payments/1/credit?amount=10
+
+If the payment is over the payment's credit allowed limit, a "Credit Over Limit" response will be returned.
+
+### Response
+
+
+
+
+### Failed Response
+
+
+```json
+{
+ "error": "There was a problem with the payment gateway: [text]"
+}
+```
+
+### Credit Over Limit Response
+
+
+```json
+{
+ "error": "This payment can only be credited up to [amount]. Please specify an amount less than or equal to this number."
+}
+```
diff --git a/guides/src/content/api/product_images.md b/guides/src/content/api/product_images.md
new file mode 100644
index 00000000000..3eec6a38a2d
--- /dev/null
+++ b/guides/src/content/api/product_images.md
@@ -0,0 +1,121 @@
+---
+title: Product Images
+description: Use the Spree Commerce storefront API to access Product Images data.
+---
+
+## Index
+
+List product images visible to the authenticated user. If the user is an admin, they are able to see all images.
+
+You may make a request using product\'s permalink or id attribute.
+
+Note that the API will attempt a permalink lookup before an ID lookup.
+
+```text
+GET /api/v1/products/a-product/images
+```
+
+### Response
+
+
+
+
+## Show
+
+```text
+GET /api/v1/products/a-product/images/1
+```
+
+### Successful Response
+
+
+
+
+### Not Found Response
+
+
+
+## New
+
+You can learn about the potential attributes (required and non-required) for a product's image by making this request:
+
+```text
+GET /api/v1/products/a-product/images/new
+```
+
+### Response
+
+
+```json
+{
+ "attributes": [
+ "id", "position", "attachment_content_type", "attachment_file_name", "type",
+ "attachment_updated_at", "attachment_width", "attachment_height", "alt"
+ ],
+ "required_attributes": []
+}
+```
+
+## Create
+
+
+
+To upload a new image through the API, make this request with the necessary parameters:
+
+```text
+POST /api/v1/products/a-product/images
+```
+
+For instance, a request using cURL will look like this:
+
+```bash
+curl -i -X POST \
+ -H "X-Spree-Token: USER_TOKEN" \
+ -H "Content-Type: multipart/form-data" \
+ -F "image[attachment]=@/absolute/path/to/image.jpg" \
+ -F "type=image/jpeg" \
+ http://localhost:3000/api/v1/products/a-product/images
+```
+
+### Successful response
+
+
+
+## Update
+
+
+
+To update an image, make this request with the necessary parameters:
+
+```text
+PUT /api/v1/products/a-product/images/1
+```
+
+A cURL request to update a product image would look like this:
+
+```bash
+curl -i -X PUT \
+ -H "X-Spree-Token: USER_TOKEN" \
+ -H "Content-Type: multipart/form-data" \
+ -F "image[attachment]=@/new/path/to/image.jpg" \
+ -F "type=image/jpeg" \
+ http://localhost:3000/api/v1/products/a-product/images/1
+```
+
+### Successful response
+
+
+
+## Delete
+
+
+
+To delete a product image, make this request:
+
+```text
+DELETE /api/v1/products/a-product/images/1
+```
+
+This request will remove the record from the database.
+
+
diff --git a/guides/src/content/api/product_properties.md b/guides/src/content/api/product_properties.md
new file mode 100644
index 00000000000..ce6a0de793f
--- /dev/null
+++ b/guides/src/content/api/product_properties.md
@@ -0,0 +1,113 @@
+---
+title: Product Properties
+description: Use the Spree Commerce storefront API to access ProductProperty data.
+---
+
+
+ Requests to this API will only succeed if the user making them has access to the underlying products. If the user is not an admin and the product is not available yet, users will receive a 404 response from this API.
+
+
+## List
+
+Retrieve a list of all product properties for a product by making this request:
+
+ GET /api/v1/products/1/product_properties
+
+Product properties are paginated and can be iterated through by passing along a `page` parameter:
+
+ GET /api/v1/products/1/product_properties?page=2
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a particular product property, make a request like this:
+
+ GET /api/v1/products/1/product_properties?q[property_name_cont]=bag
+
+The searching API is provided through the Ransack gem which Spree depends on. The `property_name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+ GET /api/v1/products/1/product_properties?q[s]=property_name%20desc
+
+## Show
+
+To get information about a single product property, make a request like this:
+
+ GET /api/v1/products/1/product_properties/1
+
+Or you can use a property's name:
+
+ GET /api/v1/products/1/product_properties/size
+
+### Response
+
+
+
+
+## Create
+
+
+
+To create a new product property, make a request like this:
+
+ POST /api/v1/products/1/product_properties?product_property[property_name]=size&product_property[value]=10
+
+If a property with that name does not already exist, then it will automatically be created.
+
+### Response
+
+
+
+
+## Update
+
+
+
+To update an existing product property, make a request like this:
+
+ PUT /api/v1/products/1/product_properties/size?product_property[value]=10
+
+You may also use a property's id if you know it:
+
+ PUT /api/v1/products/1/product_properties/1?product_property[value]=10
+
+### Response
+
+
+
+
+## Delete
+
+
+
+To delete a product property, make a request like this:
+
+ DELETE /api/v1/products/1/product_properties/size
+
+
diff --git a/guides/src/content/api/products.md b/guides/src/content/api/products.md
new file mode 100644
index 00000000000..ecb9a3669d5
--- /dev/null
+++ b/guides/src/content/api/products.md
@@ -0,0 +1,204 @@
+---
+title: Products
+description: Use the Spree Commerce storefront API to access Product data.
+---
+
+## Index
+
+List products visible to the authenticated user. If the user is not an admin, they will only be able to see products which have an `available_on` date in the past. If the user is an admin, they are able to see all products.
+
+```text
+GET /api/v1/products
+```
+
+Products are paginated and can be iterated through by passing along a `page` parameter:
+
+```text
+GET /api/v1/products?page=2
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a particular product, make a request like this:
+
+```text
+GET /api/v1/products?q[name_cont]=Spree
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```text
+GET /api/v1/products?q[s]=sku%20asc
+```
+
+It is also possible to sort results using an associated object's field.
+
+```text
+GET /api/v1/products?q[s]=shipping_category_name%20asc
+```
+
+## Show
+
+To view the details for a single product, make a request using that product\'s permalink:
+
+```text
+GET /api/v1/products/a-product
+```
+
+You may also query by the product\'s id attribute:
+
+```text
+GET /api/v1/products/1
+```
+
+Note that the API will attempt a permalink lookup before an ID lookup.
+
+### Successful Response
+
+
+
+
+### Not Found Response
+
+
+
+## New
+
+You can learn about the potential attributes (required and non-required) for a product by making this request:
+
+```text
+GET /api/v1/products/new
+```
+
+### Response
+
+
+```json
+{
+ "attributes": [
+ "id", "name", "description", "price", "display_price", "available_on",
+ "slug", "meta_description", "meta_keywords", "shipping_category_id",
+ "taxon_ids", "total_on_hand"
+ ],
+ "required_attributes": ["name", "shipping_category", "price"]
+}
+```
+
+## Create
+
+
+
+To create a new product through the API, make this request with the necessary parameters:
+
+```text
+POST /api/v1/products
+```
+
+For instance, a request to create a new product called \"Headphones\" with a price of $100 would look like this:
+
+```text
+POST /api/v1/products?product[name]=Headphones&product[price]=100&product[shipping_category_id]=1
+```
+
+### Successful response
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {
+ "name": ["can't be blank"],
+ "price": ["can't be blank"],
+ "shipping_category_id": ["can't be blank"]
+ }
+}
+```
+
+## Update
+
+
+
+To update a product\'s details, make this request with the necessary parameters:
+
+```text
+PUT /api/v1/products/a-product
+```
+
+For instance, to update a product\'s name, send it through like this:
+
+```text
+PUT /api/v1/products/a-product?product[name]=Headphones
+```
+
+### Successful response
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {
+ "name":: ["can't be blank"],
+ "price": ["can't be blank"],
+ "shipping_category_id": ["can't be blank"]
+ }
+}
+```
+
+## Delete
+
+
+
+To delete a product, make this request:
+
+```text
+DELETE /api/v1/products/a-product
+```
+
+This request, much like a typical product \"deletion\" through the admin interface, will not actually remove the record from the database. It simply sets the `deleted_at` field to the current time on the product, as well as all of that product\'s variants.
+
+
diff --git a/guides/src/content/api/return_authorizations.md b/guides/src/content/api/return_authorizations.md
new file mode 100644
index 00000000000..ff61126e7c3
--- /dev/null
+++ b/guides/src/content/api/return_authorizations.md
@@ -0,0 +1,123 @@
+---
+title: Return Authorizations
+description: Use the Spree Commerce storefront API to access ReturnAuthorization data.
+---
+
+# Return Authorizations API
+
+
+
+## Index
+
+To list all return authorizations for an order, make a request like this:
+
+ GET /api/v1/orders/R1234567/return_authorizations
+
+Return authorizations are paginated and can be iterated through by passing along a `page` parameter:
+
+ GET /api/v1/orders/R1234567/return_authorizations?page=2
+
+### Parameters
+
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a particular return authorization, make a request like this:
+
+ GET /api/v1/orders/R1234567/return_authorizations?q[memo_cont]=damage
+
+The searching API is provided through the Ransack gem which Spree depends on. The `memo_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+ GET /api/v1/orders/R1234567/return_authorizations?q[s]=amount%20asc
+
+### Response
+
+
+
+
+## Show
+
+To get information for a single return authorization, make a request like this:
+
+ GET /api/v1/orders/R1234567/return_authorizations/1
+
+### Response
+
+
+
+
+## Create
+
+
+
+To create a return authorization, make a request like this:
+
+ POST /api/v1/orders/R1234567/return_authorizations
+
+For instance, if you want to create a return authorization with a number, make
+above request with following parameters:
+
+```json
+{
+ "order_id": "R1234567",
+ "return_authorization": {
+ "stock_location_id": 1,
+ "return_authorization_reason_id": 2
+ }
+}
+```
+
+### Response
+
+
+
+
+## Update
+
+
+
+To update a return authorization, make a request like this:
+
+ PUT /api/v1/orders/R1234567/return_authorizations/1
+
+For instance, to update a return authorization's number, make this request:
+
+ PUT /api/v1/orders/R1234567/return_authorizations/1?return_authorization[memo]=Broken
+
+### Response
+
+
+
+
+## Delete
+
+
+
+To delete a return authorization, make a request like this:
+
+ DELETE /api/v1/orders/R1234567/return_authorizations/1
+
+### Response
+
+
diff --git a/guides/src/content/api/shipments.md b/guides/src/content/api/shipments.md
new file mode 100644
index 00000000000..f8424fa3b5c
--- /dev/null
+++ b/guides/src/content/api/shipments.md
@@ -0,0 +1,157 @@
+---
+title: Shipments
+description: Use the Spree Commerce storefront API to access Shipment data.
+---
+
+# Shipments API
+
+## Mine
+
+Retrieve a list of the current user's shipments by making this request:
+
+```text
+GET /api/v1/shipments/mine
+```
+
+Shipments are paginated and can be iterated through by passing along a `page` parameter:
+
+```text
+GET /api/v1/shipments/mine?page=2
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Create
+
+
+
+The following attributes are required when creating a shipment:
+
+- order_id
+- stock_location_id
+- variant_id
+
+To create a shipment, make a request like this:
+
+```text
+POST /api/v1/shipments?shipment[order_id]=R123456789
+```
+
+The `order_id` is the number of the order to create a shipment for and is provided as part of the URL string as shown above. The shipment will be created at the selected stock location and include the variant selected.
+
+Assuming in this instance that you want to create a shipment with a stock_location_id of `1` and a variant_id of `10` for order `R1234567`, send through the parameters like this:
+
+```json
+{
+ "order_id": "R1234567",
+ "stock_location_id": "1",
+ "variant_id": "10"
+}
+```
+
+### Response
+
+
+
+
+## Update
+
+
+
+To update shipment information, make a request like this:
+
+```text
+PUT /api/v1/shipments/H123456789?shipment[tracking]=TRK9000
+```
+
+To update order ship method inspect order/shipments/shipping_rates for available shipping_rate_id values and use following api call:
+
+ PUT /api/v1/shipments/H123456789?shipment[selected_shipping_rate_id]=1
+
+### Response
+
+
+
+
+## Ready
+
+
+
+To mark a shipment as ready, make a request like this:
+
+ PUT /api/v1/shipments/H123456789/ready
+
+You may choose to update shipment attributes with this request as well:
+
+ PUT /api/v1/shipments/H123456789/ready?shipment[number]=1234567
+
+### Response
+
+
+
+
+## Ship
+
+
+
+To mark a shipment as shipped, make a request like this:
+
+ PUT /api/v1/shipments/H123456789/ship
+
+You may choose to update shipment attributes with this request as well:
+
+ PUT /api/v1/shipments/H123456789/ship?shipment[tracking]=1234567
+
+### Response
+
+
+
+
+## Add Variant
+
+
+
+To add a variant to a shipment, make a request like this:
+
+ PUT /api/v1/shipments/H123456789/add
+
+```json
+{
+ "order_id": 123456,
+ "stock_location_id": 1,
+ "variant_id": 10
+}
+```
+
+### Response
+
+
+
+
+## Remove Variant
+
+
+
+To remove a variant from a shipment, make a request like this:
+
+ PUT /api/v1/shipments/H123456789/remove?variant_id=1&quantity=1
+
+### Response
+
+
+
diff --git a/guides/src/content/api/states.md b/guides/src/content/api/states.md
new file mode 100644
index 00000000000..cc20bd8defd
--- /dev/null
+++ b/guides/src/content/api/states.md
@@ -0,0 +1,50 @@
+---
+title: States
+description: Use the Spree Commerce storefront API to access State data.
+---
+
+## Index
+
+To get a list of states within Spree, make a request like this:
+
+```text
+GET /api/v1/states
+```
+
+States are paginated and can be iterated through by passing along a `page`
+parameter:
+
+```text
+GET /api/v1/states?page=2
+```
+
+As well as a `per_page` parameter to control how many results will be returned:
+
+```text
+GET /api/v1/states?per_page=100
+```
+
+You can scope the states by country by passing along a `country_id` parameter
+too:
+
+```text
+GET /api/v1/states?country_id=1
+```
+
+### Response
+
+
+
+
+## Show
+
+To find out about a single state, make a request like this:
+
+```text
+GET /api/v1/states/1
+```
+
+### Response
+
+
+
diff --git a/guides/src/content/api/stock_items.md b/guides/src/content/api/stock_items.md
new file mode 100644
index 00000000000..6ba4f0cb3ec
--- /dev/null
+++ b/guides/src/content/api/stock_items.md
@@ -0,0 +1,181 @@
+---
+title: Stock Items
+description: Use the Spree Commerce storefront API to access StockItem data.
+---
+
+## Index
+
+
+
+To return a paginated list of all stock items for a stock location, make this request, passing the stock location id you wish to see stock items for:
+
+```text
+GET /api/v1/stock_locations/1/stock_items
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+
+
+To search for a particular stock item, make a request like this:
+
+```text
+GET /api/v1/stock_locations/1/stock_items?q[variant_id_eq]=10
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `variant_id_eq` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```text
+GET /api/v1/stock_locations/1/stock_items?q[s]=variant_id%20asc
+```
+
+## Show
+
+
+
+To view the details for a single stock item, make a request using that stock item's id, along with its `stock_location_id`:
+
+```text
+GET /api/v1/stock_locations/1/stock_items/2
+```
+
+### Successful Response
+
+
+
+
+### Not Found Response
+
+
+
+## Create
+
+
+
+To create a new stock item for a stock location, make this request with the necessary parameters:
+
+```text
+POST /api/v1/stock_locations/1/stock_items
+```
+
+For instance, a request to create a new stock item with a count_on_hand of 10 and a variant_id of 1 would look like this::
+
+```json
+{
+ "stock_item": {
+ "count_on_hand": 10,
+ "variant_id": "1",
+ "backorderable": true
+ }
+}
+```
+
+### Successful response
+
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {}
+}
+```
+
+## Update
+
+
+
+Note that using this endpoint, count_on_hand IS APPENDED to its current value.
+
+Sending a request with a negative count_on_hand WILL SUBSTRACT the current value.
+
+To force a value for count_on_hand, include force: true in your request, this will replace the current
+value as it's stored in the database.
+
+To update a stock item's details, make this request with the necessary parameters.
+
+```text
+PUT /api/v1/stock_locations/1/stock_items/2
+```
+
+For instance, to update a stock item's count_on_hand, send it through like this:
+
+```json
+{
+ "stock_item": {
+ "count_on_hand": 30
+ }
+}
+```
+
+Or alternatively with the force attribute to replace the current count_on_hand with a new value:
+
+```json
+{
+ "stock_item": {
+ "count_on_hand": 30,
+ "force": true,
+ }
+}
+```
+
+### Successful response
+
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {}
+}
+```
+
+## Delete
+
+
+
+To delete a stock item, make this request:
+
+```text
+DELETE /api/v1/stock_locations/1/stock_items/2
+```
+
+### Response
+
+
diff --git a/guides/src/content/api/stock_locations.md b/guides/src/content/api/stock_locations.md
new file mode 100644
index 00000000000..668d5f01d59
--- /dev/null
+++ b/guides/src/content/api/stock_locations.md
@@ -0,0 +1,137 @@
+---
+title: Stock Locations
+description: Use the Spree Commerce storefront API to access StockLocation data.
+---
+
+## Index
+
+
+
+To get a list of stock locations, make this request:
+
+```text
+GET /api/v1/stock_locations
+```
+
+Stock locations are paginated and can be iterated through by passing along a `page` parameter:
+
+```text
+GET /api/v1/stock_locations?page=2
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+
+
+To search for a particular stock location, make a request like this:
+
+```text
+GET /api/v1/stock_locations?q[name_cont]=default
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+## Show
+
+
+
+To get information for a single stock location, make this request:
+
+```text
+GET /api/v1/stock_locations/1
+```
+
+### Response
+
+
+
+
+## Create
+
+
+
+To create a stock location, make a request like this:
+
+```text
+POST /api/v1/stock_locations
+```
+
+Assuming in this instance that you want to create a stock location with a name of `East Coast`, send through the parameters like this:
+
+```json
+{
+ "stock_location": {
+ "name": "East Coast"
+ }
+}
+```
+
+### Response
+
+
+
+
+## Update
+
+
+
+To update a stock location, make a request like this:
+
+```text
+PUT /api/v1/stock_locations/1
+```
+
+To update stock location information, use parameters like this:
+
+```json
+{
+ "stock_location": {
+ "name": "North Pole"
+ }
+}
+```
+
+### Response
+
+
+
+
+## Delete
+
+
+
+To delete a stock location, make a request like this:
+
+```text
+DELETE /api/v1/stock_locations/1
+```
+
+This request will also delete any related `stock item` records.
+
+### Response
+
+
diff --git a/guides/src/content/api/stock_movements.md b/guides/src/content/api/stock_movements.md
new file mode 100644
index 00000000000..1d944f3cecb
--- /dev/null
+++ b/guides/src/content/api/stock_movements.md
@@ -0,0 +1,149 @@
+---
+title: Stock Movements
+description: Use the Spree Commerce storefront API to access StockMovement data.
+---
+
+## Index
+
+
+
+To return a paginated list of all stock movements for a stock location, make this request, passing the stock location id you wish to see stock items for:
+
+```text
+GET /api/v1/stock_locations/1/stock_movements
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+
+
+To search for a particular stock movement, make a request like this:
+
+```text
+GET /api/v1/stock_locations/1/stock_movements?q[quantity_eq]=10
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `quantity_eq` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```text
+GET /api/v1/stock_locations/1/stock_movements?q[s]=quantity%20asc
+```
+
+## Show
+
+
+
+To view the details for a single stock movement, make a request using that stock movement's id, along with its `stock_location_id`:
+
+```text
+GET /api/v1/stock_locations/1/stock_movements/1
+```
+
+### Successful Response
+
+
+
+
+### Not Found Response
+
+
+
+## Create
+
+
+
+To create a new stock movement for a stock location, make this request with the necessary parameters:
+
+```text
+POST /api/v1/stock_locations/1/stock_movements
+```
+
+For instance, a request to create a new stock movement with a quantity of 10, the action set to received, and a stock_item_id of 1 would look like this::
+
+```json
+{
+ "stock_movement": {
+ "quantity": 10,
+ "stock_item_id": "1",
+ "action": "received"
+ }
+}
+```
+
+### Successful response
+
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {}
+}
+```
+
+## Update
+
+
+
+To update a stock movement's details, make this request with the necessary parameters:
+
+```text
+PUT /api/v1/stock_locations/1/stock_movements/1
+```
+
+For instance, to update a stock movement's quantity, send it through like this:
+
+```json
+{
+ "stock_movement": {
+ "quantity": 30,
+ }
+}
+```
+
+### Successful response
+
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {}
+}
+```
diff --git a/guides/src/content/api/summary.md b/guides/src/content/api/summary.md
new file mode 100644
index 00000000000..440f74470ad
--- /dev/null
+++ b/guides/src/content/api/summary.md
@@ -0,0 +1,93 @@
+---
+title: Summary
+---
+
+## Overview
+
+Spree currently supports RESTful access to the resources listed in the sidebar
+on the right »
+
+This API was built using the great [Rabl](https://github.com/nesquena/rabl) gem.
+Please consult its documentation if you wish to understand how the templates use
+it to return data.
+
+This API conforms to a set of [rules](#rules).
+
+### JSON Data
+
+Developers communicate with the Spree API using the [JSON](http://www.json.org) data format. Requests for data are communicated in the standard manner using the HTTP protocol.
+
+### Making an API Call
+
+You will need an authentication token to access the API. These keys can be generated on the user edit screen within the admin interface. To make a request to the API, pass a `X-Spree-Token` header along with the request:
+
+```bash
+$ curl --header "X-Spree-Token: YOUR_KEY_HERE" http://example.com/api/v1/products.json
+```
+
+
+Alternatively, you may also pass through the token as a parameter in the request if a header just won't suit your purposes (i.e. JavaScript console debugging).
+
+```bash
+$ curl http://example.com/api/v1/products.json?token=YOUR_KEY_HERE
+```
+
+The token allows the request to assume the same level of permissions as the actual user to whom the token belongs.
+
+### Error Messages
+
+You may encounter the follow error messages when using the API.
+
+#### Not Found
+
+
+
+#### Authorization Failure
+
+
+
+#### Invalid API Key
+
+
+```json
+{ "error": "Invalid API key ([key]) specified." }
+```
+
+#### No API Key specified
+
+
+
+## Rules
+
+The following are some simple rules that all Spree API endpoints comply with.
+
+1. All successful requests for the API will return a status of 200.
+2. Successful create and update requests will result in a status of 201 and 200 respectively.
+3. Both create and update requests will return Spree\'s representation of the data upon success.
+4. If a create or update request fails, a status code of 422 will be returned, with a hash containing an \"error\" key, and an \"errors\" key. The errors value will contain all ActiveRecord validation errors encountered when saving this record.
+5. Delete requests will return status of 200, and no content.
+6. Requests that list collections, such as `/api/v1/products` are paginated and
+ will return 25 records per page. You can speficy your own `per_page` value.
+7. Requests that list collections can be paginated through by passing a `page`
+ parameter, `page=1` being the first page.
+8. If a resource can not be found, the API will return a status of 404.
+9. Unauthorized requests will be met with a 401 response.
+
+## Customizing Responses
+
+If you wish to customize the responses from the API, you can do this in one of
+two ways: overriding the template, or providing a custom template.
+
+### Overriding template
+
+Overriding a template for the API should be done if you want to provide a custom
+response for an API endpoint. Template loading in Rails will attempt to look up
+a template within your application's view paths first. If it isn't available
+there, then it will fallback to looking within the other engine's view paths,
+eventually finding its way to the API engine.
+
+You can use this to your advantage and define a view template within your
+application that exists at the same path as a template within the API engine.
+For instance, if you place a template in your application at
+`app/views/spree/api/v1/products/show.v1.rabl`, it will take precedence over the
+template within the API engine.
diff --git a/guides/src/content/api/taxonomies.md b/guides/src/content/api/taxonomies.md
new file mode 100644
index 00000000000..578bb38a239
--- /dev/null
+++ b/guides/src/content/api/taxonomies.md
@@ -0,0 +1,231 @@
+---
+title: Taxonomies
+description: Use the Spree Commerce storefront API to access Taxonomy data.
+---
+
+## Index
+
+To get a list of all the taxonomies, including their root nodes and the
+immediate children for the root node, make a request like this:
+
+```text
+GET /api/v1/taxonomies
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a particular taxonomy, make a request like this:
+
+```text
+GET /api/v1/taxonomies?q[name_cont]=brand
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```text
+GET /api/v1/taxonomies?q[s]=name%20asc
+```
+
+It is also possible to sort results using an associated object's field.
+
+```text
+GET /api/v1/taxonomies?q[s]=root_name%20desc
+```
+
+## Show
+
+To get information for a single taxonomy, including its root node and the immediate children of the root node, make a request like this:
+
+```text
+GET /api/v1/taxonomies/1
+```
+
+### Response
+
+
+
+
+## Create
+
+
+
+To create a taxonomy, make a request like this:
+
+```text
+POST /api/v1/taxonomies
+```
+
+For instance, if you want to create a taxonomy with the name \"Brands\", make
+this request:
+
+```text
+POST /api/v1/taxonomies?taxonomy[name]=Brand
+```
+
+If you\'re creating a taxonomy without a root taxon, a root taxon will automatically be
+created for you with the same name as the taxon.
+
+### Response
+
+
+
+
+## Update
+
+
+
+To update a taxonomy, make a request like this:
+
+```text
+PUT /api/v1/taxonomies/1
+```
+
+For instance, to update a taxonomy\'s name, make this request:
+
+```text
+PUT /api/v1/taxonomies/1?taxonomy[name]=Brand
+```
+
+### Response
+
+
+
+
+## Delete
+
+
+
+To delete a taxonomy, make a request like this:
+
+```text
+DELETE /api/v1/taxonomies/1
+```
+
+### Response
+
+
+
+## List taxons
+
+To get a list for all taxons underneath the root taxon for a taxonomy (and their children) for a taxonomy, make this request:
+
+ GET /api/v1/taxonomies/1/taxons
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## A single taxon
+
+To see information about a taxon and its immediate children, make a request
+like this:
+
+ GET /api/v1/taxonomies/1/taxons/1
+
+### Response
+
+
+
+
+## Taxon Create
+
+
+
+To create a taxon, make a request like this:
+
+ POST /api/v1/taxonomies/1/taxons
+
+To create a new taxon with the name "Brands", make this request:
+
+ POST /api/v1/taxonomies/1/taxons?taxon[name]=Brands
+
+### Response
+
+
+
+
+## Taxon Update
+
+
+
+To update a taxon, make a request like this:
+
+ PUT /api/v1/taxonomies/1/taxons/1
+
+For example, to update the taxon's name to "Brand", make this request:
+
+ PUT /api/v1/taxonomies/1/taxons/1?taxon[name]=Brand
+
+### Response
+
+
+
+
+## Taxon Delete
+
+
+
+To delete a taxon, make a request like this:
+
+ DELETE /api/v1/taxonomies/1/taxons/1
+
+
+ This will cause all child taxons to be deleted as well.
+
+
+### Response
+
+
diff --git a/guides/src/content/api/users.md b/guides/src/content/api/users.md
new file mode 100644
index 00000000000..5462db0cae2
--- /dev/null
+++ b/guides/src/content/api/users.md
@@ -0,0 +1,143 @@
+---
+title: Users
+description: Use the Spree Commerce storefront API to access User data.
+---
+
+List users visible to the authenticated user. If the user is not an admin,
+they will only be able to see their own user, unless they have custom
+permissions to see other users. If the user is an admin then they can see all
+users.
+
+```text
+GET /api/v1/users
+```
+
+Users are paginated and can be iterated through by passing along a `page`
+parameter:
+
+```text
+GET /api/v1/users?page=2
+```
+
+### Response
+
+
+
+
+## A single user
+
+To view the details for a single user, make a request using that user\'s
+id:
+
+```text
+GET /api/v1/users/1
+```
+
+### Successful Response
+
+
+
+
+### Not Found Response
+
+
+
+## Pre-creation of a user
+
+You can learn about the potential attributes (required and non-required) for a
+user by making this request:
+
+```text
+GET /api/v1/users/new
+```
+
+### Response
+
+
+```json
+{
+ "attributes": ["", ""],
+ "required_attributes": []
+}
+```
+
+## Creating a new new
+
+
+
+To create a new user through the API, make this request with the necessary
+parameters:
+
+```text
+POST /api/v1/users
+```
+
+For instance, a request to create a new user with the email
+\"spree@example.com\" and password \"password\" would look like this:
+
+```text
+POST /api/v1/users?user[email]=spree@example.com&user[password]=password
+```
+
+### Successful response
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {
+ "email": ["can't be blank"]
+ }
+}
+```
+
+## Updating a user
+
+
+
+To update a user\'s details, make this request with the necessary parameters:
+
+```text
+PUT /api/v1/users/1
+```
+
+For instance, to update a user\'s password, send it through like this:
+
+```text
+PUT /api/v1/users/1?user[password]=password
+```
+
+### Successful response
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {
+ "email": ["can't be blank"]
+ }
+}
+```
+
+## Deleting a user
+
+
+
+To delete a user, make this request:
+
+```text
+DELETE /api/v1/users/1
+```
+
+### Response
+
+
+
diff --git a/guides/src/content/api/variants.md b/guides/src/content/api/variants.md
new file mode 100644
index 00000000000..15167654745
--- /dev/null
+++ b/guides/src/content/api/variants.md
@@ -0,0 +1,208 @@
+---
+title: Variants
+description: Use the Spree Commerce storefront API to access Variant data.
+---
+
+## Index
+
+To return a paginated list of all variants within the store, make this request:
+
+```text
+GET /api/v1/variants
+```
+
+You can limit this to showing the variants for a particular product by passing through a product's slug:
+
+```text
+GET /api/v1/products/ruby-on-rails-tote/variants
+```
+
+or
+
+```text
+GET /api/v1/variants?product_id=ruby-on-rails-tote
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a particular variant, make a request like this:
+
+```text
+GET /api/v1/variants?q[sku_cont]=foo
+```
+
+You can limit this to showing the variants for a particular product by passing through a product id:
+
+```text
+GET /api/v1/products/ruby-on-rails-tote/variants?q[sku_cont]=foo
+```
+
+or
+
+```text
+GET /api/v1/variants?product_id=ruby-on-rails-tote&q[sku_cont]=foo
+```
+
+
+The searching API is provided through the Ransack gem which Spree depends on. The `sku_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```text
+GET /api/v1/variants?q[s]=price%20asc
+```
+
+It is also possible to sort results using an associated object's field.
+
+```text
+GET /api/v1/variants?q[s]=product_name%20asc
+```
+
+## Show
+
+To view the details for a single variant, make a request using that variant\'s id, along with the product's permalink as its `product_id`:
+
+```text
+GET /api/v1/products/ruby-on-rails-tote/variants/1
+```
+
+Or:
+
+```text
+GET /api/v1/variants/1?product_id=ruby-on-rails-tote
+```
+
+### Successful Response
+
+
+
+
+### Not Found Response
+
+
+
+## New
+
+You can learn about the potential attributes (required and non-required) for a variant by making this request:
+
+```text
+GET /api/v1/products/ruby-on-rails-tote/variants/new
+```
+
+### Response
+
+
+```json
+{
+ "attributes": [
+ "id", "name", "sku", "price", "weight", "height",
+ "width", "depth", "is_master", "slug", "description", "track_inventory"
+ ],
+ "required_attributes": []
+}
+```
+
+## Create
+
+
+
+To create a new variant for a product, make this request with the necessary parameters:
+
+```text
+POST /api/v1/products/ruby-on-rails-tote/variants
+```
+
+For instance, a request to create a new variant with a SKU of 12345 and a price of 19.99 would look like this::
+
+```text
+POST /api/v1/products/ruby-on-rails-tote/variants/?variant[sku]=12345&variant[price]=19.99&variant[option_value_ids][]=1
+```
+
+### Successful response
+
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {}
+}
+```
+
+## Update
+
+
+
+To update a variant\'s details, make this request with the necessary parameters:
+
+```text
+PUT /api/v1/products/ruby-on-rails-tote/variants/2
+```
+
+For instance, to update a variant\'s SKU, send it through like this:
+
+```text
+PUT /api/v1/products/ruby-on-rails-tote/variants/2?variant[sku]=12345
+```
+
+### Successful response
+
+
+
+
+### Failed response
+
+
+```json
+{
+ "error": "Invalid resource. Please fix errors and try again.",
+ "errors": {}
+}
+```
+
+## Delete
+
+
+
+To delete a variant, make this request:
+
+```text
+DELETE /api/v1/products/ruby-on-rails-tote/variants/2
+```
+
+This request, much like a typical variant \"deletion\" through the admin interface, will not actually remove the record from the database. It simply sets the `deleted_at` field to the current time on the variant.
+
+
diff --git a/guides/src/content/api/zones.md b/guides/src/content/api/zones.md
new file mode 100644
index 00000000000..0d176b0d1d3
--- /dev/null
+++ b/guides/src/content/api/zones.md
@@ -0,0 +1,152 @@
+---
+title: Zones
+description: Use the Spree Commerce storefront API to access Zone data.
+---
+
+## Index
+
+To get a list of zones, make this request:
+
+```text
+GET /api/v1/zones
+```
+
+Zones are paginated and can be iterated through by passing along a `page` parameter:
+
+```text
+GET /api/v1/zones?page=2
+```
+
+### Parameters
+
+
+
+### Response
+
+
+
+
+## Search
+
+To search for a particular zone, make a request like this:
+
+```text
+GET /api/v1/zones?q[name_cont]=north
+```
+
+The searching API is provided through the Ransack gem which Spree depends on. The `name_cont` here is called a predicate, and you can learn more about them by reading about [Predicates on the Ransack wiki](https://github.com/ernie/ransack/wiki/Basic-Searching).
+
+The search results are paginated.
+
+### Response
+
+
+
+
+### Sorting results
+
+Results can be returned in a specific order by specifying which field to sort by when making a request.
+
+```text
+GET /api/v1/zones?q[s]=name%20desc
+```
+
+## Show
+
+To get information for a single zone, make this request:
+
+```text
+GET /api/v1/zones/1
+```
+
+### Response
+
+
+
+
+## Create
+
+
+
+To create a zone, make a request like this:
+
+```text
+POST /api/v1/zones
+```
+
+Assuming in this instance that you want to create a zone containing
+a zone member which is a `Spree::Country` record with the `id` attribute of 1, send through the parameters like this:
+
+```json
+{
+ "zone": {
+ "name": "North Pole",
+ "zone_members": [
+ {
+ "zoneable_type": "Spree::Country",
+ "zoneable_id": 1
+ }
+ ]
+ }
+}
+```
+
+### Response
+
+
+
+
+## Update
+
+
+
+To update a zone, make a request like this:
+
+```text
+PUT /api/v1/zones/1
+```
+
+To update zone and zone member information, use parameters like this:
+
+```json
+{
+ "zone": {
+ "name": "North Pole",
+ "zone_members": [
+ {
+ "zoneable_type": "Spree::Country",
+ "zoneable_id": 1
+ }
+ ]
+ }
+}
+```
+
+### Response
+
+
+
+
+## Delete
+
+
+
+To delete a zone, make a request like this:
+
+```text
+DELETE /api/v1/zones/1
+```
+
+This request will also delete any related `zone_member` records.
+
+### Response
+
+
diff --git a/guides/src/content/developer/advanced/developer_tips.md b/guides/src/content/developer/advanced/developer_tips.md
new file mode 100644
index 00000000000..09dd9ddb847
--- /dev/null
+++ b/guides/src/content/developer/advanced/developer_tips.md
@@ -0,0 +1,114 @@
+---
+title: 'Developer Tips and Tricks'
+section: advanced
+order: 2
+---
+
+## Overview
+
+This guide presents accumulated wisdom from person-years of Spree use.
+
+## Upgrade Considerations
+
+### The important commands
+
+`spree -update` was removed in favor of Bundler.
+
+Before updating, you will want to ensure the installed spree gem is
+up-to-date by modifying `Gemfile` to match the new spree version and
+run `bundle update`.
+
+Thanks to Rails 3.1 Mountable Engine, the update process is
+"non-destructive" than in previous versions of Spree. The core files are encapsulated
+separately from sandbox, thus upgrading to newer files will not override nor replace
+sandbox's customized files.
+
+This makes it easier to see when and how some file has changed – which
+is often useful if you need to update a customized version.
+
+### Dos and Don'ts
+
+
+Try to avoid modifying `config/boot.rb` and
+`config/environment.rb`: use [initializers](#initializers) instead.
+
+
+### Tracking changes for overridden code
+
+Be aware that core changes might have an impact on the components you
+have overridden in your project.
+You might need to patch your local copies, or ensure that such copies
+interact correctly with changed code (e.g. using appropriate ids in HTML to allow the JavaScript to
+work).
+
+If you can help us generalize the core code so that your preferred
+effect is achieved by altering a few parameters, this will be more useful than duplicating several
+files. Ideas and suggestions are always welcome.
+
+### Initializers
+
+Initializers are run during startup, and are the recommended way to
+execute certain settings. You can put initializers in extensions, thus have a way to execute
+extension-specific configurations.
+
+See the [extensions guide](extensions_tutorial.html#extension-initializers) for
+more information.
+
+## Debugging techniques
+
+### Use tests!
+
+Use `rake spec` and `rake test` to test basic functioning after you've
+made changes.
+
+### Analyzing crashes on a non-local machine
+
+If you're testing on a server, whether in production or development
+mode, the following code in one
+of your `FOO_extension.rb` files might save some time. It triggers
+local behavior for users who have
+an admin role. One useful consequence is that uncaught exceptions will
+show the detailed error page
+instead of `404.html`, so you don't have to hunt through the server
+logs.
+
+```ruby
+Spree::BaseController.class_eval do
+ def local_request?
+ ENV["RAILS_ENV"] !="production" || current_user.present? &&
+ current_user.has_role?(:admin)
+ end
+end
+```
+
+## Managing large projects
+
+### To fork or not to fork…
+
+Suppose there's a few details of Spree that you want to override due to
+personal or client preference,
+but which aren't the usual things that you'd override (like views) - so
+something like tweaks to the models or controllers.
+
+You could hide these away in your site extension, but they could get
+mixed up with your real site customizations. You could also fork Spree and run your site on this
+forked version, but this can also be a headache to get right. There's also the hassle of tracking
+changes to `spree/master` and pulling them into your project at the right time.
+
+So here's a compromise: have an extra extension, say `spree-tweaks`, to
+contain your small collection of modified files, which is loaded first in the extension order. The
+benefits are:
+
+- it's clear what you are overriding, and easier to check against core
+ changes
+- you can base your project on an official gem release or a
+ `spree/master` commit stage
+- such tweaks can become part of your client site project and be
+ managed with SCM etc.
+
+If you find yourself wanting extensive changes to core, this technique
+probably won't work so well.
+But then again, if this is the case, then you probably want to look
+seriously at splitting some
+code off into stand-alone extensions and then see whether any of the
+other code should be contributed to the core.
diff --git a/guides/src/content/developer/advanced/gateway_specific_information.md b/guides/src/content/developer/advanced/gateway_specific_information.md
new file mode 100644
index 00000000000..ae63589625e
--- /dev/null
+++ b/guides/src/content/developer/advanced/gateway_specific_information.md
@@ -0,0 +1,39 @@
+---
+title: Gateway Specific Information
+section: advanced
+order: 1
+---
+
+## Gateway Specific Information
+
+### AuthorizeNet and AuthorizeNet CIM
+
+The transaction with Authoriet.Net comes in two flavors: One, you pass the 16-digit credit card to authorize.net and obtain a transaction key uniquely identifying that transaction. You can't charge that customer's credit card again. Two, you can use the Customer Information Manager ("CIM") to save a user's credit card attached to their account. AuthorizeNetCIM accomplishes this by obtaining two tokens: one uniquely identifying the user and another uniquely identifying credit card.
+
+In short, if you want to build a feature where a user can "Save this card for later" you need to use the CIM method when the user has selected to save their credit card and the non-CIM method when the user hasn't.
+
+For your dev, QA, and staging environments we recommend you set up a Authorize.Net sandbox environment, which is available for free to all developers at [http://developer.authorize.net](http://developer.authorize.net).
+
+Please note that in the Spree Admin > Configuration > Payment Methods screen, you will create payment methods for each of the AuthorizeNet and AuthorizeNetCIM objects (probably both).
+
+For both you real business's Authorize.net account and your sandbox, you will generate a "API Login id" and a "Transaction key." In the Spree Payment methods interface, plug the API Login Id into the field marked "LOGIN" and the Transaction Key into the field marked "PASSWORD". (Your Spree::Gateway::AuthorizeNet and Spree::Gateway::AuthorizeNetCIM object can use the same API login id & transaction keys.)
+
+WARNING: If you reset your Authorize.net password (via the web interface), these credentials will be expired and regenerated automatically. However, the old set of credentials will continue to work for a period of 24 hours before they cease working.
+
+Here on the edit Payment Method screen you will also see settings for both "test mode" and a setting for "server." Do not confuse the two settings.
+
+In both the AuthorizeNet and AuthorizeNetCIM objects, the "server" setting must be set to either 'live' (for your real-live authorize.net Production environemnt tied to your business's bank account) or 'test' (for a sandbox account you created on http://developer.authorize.net)
+
+Note that it is additionally possible to set "Test Mode" on _either_ your Live or Sandbox (test) environments. There are four possible use cases for live server/test server and test mode on/off:
+
+Test server with test mode On - Probably redundant, you generally will never need this configuration. If you do have a need for this, you can use a real credit card number or one of the fake numbers (like these [test credit card numbers](https://community.developer.authorize.net/t5/Integration-and-Testing/Test-Credit-Card-Numbers/td-p/7653)) to test for a successful result.
+
+Test server with test mode Off - Probably how you want your dev, QA, and staging servers set up. You can use a real credit card or any of the Authorize.net FAKE (aka "test") credit card numbers to get a successful transaction.
+
+Live server with test mode On - You're going to do this the _very first time_ you ever set up you new Spree store. When Test Mode is ON and your store is live (the first day you "go live"), you're going to put in a real credit card into the gateway and you will simulate a tranasction on your website. Your credit card will not actually be charged for this transaction, although Authorize.net will return a successful response to your Spree store to simulate what it would be like for a real customer. This will be the final step of verifying that your Authorize.net gateway is working before switching Test Mode to "Off" (thus making yout store ready for business)
+
+Live server with test mode Off - This is how your live, production website operates when the store is in business.
+
+Separated Auth & Capture Warning: If you have a separated authorize event from capture event (in Authorize.net terms this is called an "auth" event and "prior auth capture" event), the Sandbox won't return a transaction ID correctly if Test mode is set to ON. Instead, it will simply return a "0". This will mean that any subsequent "prior auth capture" event will fail because the capture relies on the previously-generated transaction ID. So essentially, if you have a store with a separated auth and capture do not turn Test mode to "On".
+
+(If you have test mode "Off" the Sandbox environment Authorize.net will generate transaction IDs that begin with the number 2)
diff --git a/guides/src/content/developer/advanced/seo.md b/guides/src/content/developer/advanced/seo.md
new file mode 100644
index 00000000000..dc2d24eec4b
--- /dev/null
+++ b/guides/src/content/developer/advanced/seo.md
@@ -0,0 +1,113 @@
+---
+title: 'Seo Considerations'
+section: advanced
+order: 0
+---
+
+## Overview
+
+Search Engine Optimization is an important area to address when
+implementing and developing an ecommerce solution to ensure competitive
+search engine performance. The following guide outlines current Spree
+Search Engine Optimization features and future optimization development
+possibilities.
+
+## Existing Search Engine Optimization Features
+
+Chapter 1 contains a description of the work that has been completed to
+address common search engine optimization issues.
+
+### Relevant, Meaningful URLs
+
+The helper method `seo_url(taxon)` yields SEO friendly URLs such as [demo.spreecommerce.org/products/xm-direct2-interface-adapter](http://demo.spreecommerce.org/products/xm-direct2-interface-adapter) and [demo.spreecommerce.org/t/categories/headphones](http://demo.spreecommerce.org/t/categories/headphones).
+Each controller is configured to serve the content using these keyword-relevant, meaningful URLs.
+
+### On Page Keyword Targeting
+
+Several enhancements have been made to improve on-page keyword targeting. The admin interface provides the ability to manage meta descriptions and meta keywords at the product level. Additionally, H1 tags are used throughout the site for product and taxonomy names. The ease of extension development and layout changes allows you to target keywords throughout the site.
+
+Starting with Spree 2.0, Taxons also have `meta_keywords` and `meta_description` on them. (You can configure these in the Admin > Configuration > Taxonomies). If you want to add keywords and description to another kind of object in Spree, you can do so simply by adding those two fields (`meta_keywords` and `meta_description`) onto the object in question. The Spree controller must instantiate an instance variable of the same class name as the controller (so, for example, `@taxon` for the TaxonsController) for this to work. Check out the `meta_data` method on spree_core/app/helpers/spree/base_helper.rb for details on how that works.
+
+### Clean Content
+
+Spree 2.4 and earlier uses Skeleton and Spree 3.0 uses Bootstrap. Both are a responsive CSS framework that allows clean HTML that also responds well to any screen size. Having clean HTML with minimal inline JavaScript and CSS is considered to be a factor in search engine optimization.
+
+### On Site Performance Optimization
+
+Spree has been configured to serve one CSS and one JavaScript file on
+every page (excluding extension inclusions). Minimizing HTTP requests is
+considered an important factor in search engine optimization as the
+server performance is an important influence in the search engine crawl
+behavior for a site.
+
+### Google Analytics integration
+
+Google Analytics has been integrated into the Spree core and can be
+managed from the "Analytics Trackers" section of the admin. Google
+Analytics is not included on your store if this preference is not set.
+The Google Analytics setup includes e-commerce conversion tracking.
+
+## Gotchas, Known Issues, and Further Considerations
+
+### Known Duplicate Content Issues
+
+In the Spree demo, it is a known issue that
+[demo.spreecommerce.org](http://demo.spreecommerce.org/) contains
+duplicate content to
+[demo.spreecommerce.org/products](http://demo.spreecommerce.org/products).
+Duplicate content can be a detriment to search engine performance as
+external links are divided among duplicate content pages. As a result,
+duplicate content pages may not only not be excluded from the main
+search engine index, but pages may also rank poorly in comparison to
+other sites where all external links go to one non-duplicated page.
+
+If you change your home page this won't be an issue for you. Alternatively, you can have your [demo.spreecommerce.org/products](http://demo.spreecommerce.org/products) page redirect to your home page to eliminate this problem.
+
+### Integration of Content Management System or Content
+
+There has been quite a bit of interest in development of [CMS
+integration into
+Spree](https://groups.google.com/forum/#!searchin/spree-user/cms). Having
+good content is an important part of search engine optimization, as it
+not only can improve on page keyword targeting, but it also can improve
+the popularity of a site which can in turn improve search engine
+optimization.
+
+### Tool Integration
+
+In addition to integration of Google Analytics, several other tools can
+be implemented for SEO purposes such as Bing Webmaster Tools, Google
+Webmaster Tools and Quantcast. Social media optimization tools such as
+Pinterest, Reddit, Digg, Delicious, Facebook, Google+ and Twitter may
+also be integrated to improve social networking site performance.
+
+Many of these can be implemented with minimal changes to your Spree store.
+
+### Spree SEO Extensions
+
+The following list shows extensions that can improve search engine
+performance. Refer to the GitHub README for developer notes.
+
+- [Spree Sitemap Generator](https://github.com/spree-contrib/spree_sitemap)
+- [Static Content Management](https://github.com/spree-contrib/spree_static_content)
+- [Product Reviews](https://github.com/spree-contrib/spree_reviews)
+
+(for stores older than Spree 1.0, check out [Spree Sitemap Generation](https://github.com/romul/spree_dynamic_sitemaps))
+
+## Planned Search Engine Optimization Features (TODO)
+
+Although several common search engine optimization issues have been
+addressed, we are always looking for the new best practices in SEO.
+Contributions to address issues will be very welcome. Visit the
+[contributing to spree section](contributing.html) to learn
+more about contributing.
+
+Here are some of the specific planned ideas we have for the future of Spree:
+
+- Make the `alt` field from spree_assets output as the alt attribute in the image tag
+
+## In Conclusion
+
+Spree cannot control factors such as external links, quality of external
+links, server performance and capabilities. These areas should not be
+ignored in implementation of search engine optimization efforts.
diff --git a/guides/src/content/developer/core/addresses.md b/guides/src/content/developer/core/addresses.md
new file mode 100644
index 00000000000..2e04877f025
--- /dev/null
+++ b/guides/src/content/developer/core/addresses.md
@@ -0,0 +1,41 @@
+---
+title: "Addresses"
+section: core
+---
+
+## Address
+
+The `Address` model in the `spree` gem is used to track address information, mainly for orders. Address information can also be tied to the `Spree::User` objects which come from the `spree_auth_devise` extension.
+
+Addresses have the following attributes:
+
+* `firstname`: The first name for the person at this address.
+* `lastname`: The last name for the person at this address.
+* `address1`: The address's first line.
+* `address2`: The address's second line.
+* `city`: The city where the address is.
+* `zipcode`: The postal code.
+* `phone`: The phone number.
+* `state_name`: The name for the state.
+* `alternative_phone`: The alternative phone number.
+* `company`: A company name.
+
+Addresses can also link to countries and states. An address must always link to a `Spree::Country` object. It can optionally link to a `Spree::State` object, but only in the cases where the related country has no states listed. In that case, the state information is still required, and is kept within the `state_name` field on the address record. An easy way to get the state information for the address is to call `state_text` on that object.
+
+## Zones
+
+When an order's address is linked to a country or a state, that can ultimately affect different features of the order, including shipping availability and taxation. The way these effects work is through zones.
+
+A zone is comprised of many different "zone members", which can either be a set of countries or a set of states.
+
+Every order has a "tax zone", which indicates if a user should or shouldn't be taxed when placing an order. For more information, please see the [Taxation](taxation) guide.
+
+In addition to tax zones, orders also have shipping methods. These are provided to the user based on their address information, and once selected lock in how an order is going to be shipped to that user. For more information, please see the [Shipments](shipments) guide.
+
+## Countries
+
+Countries within Spree are used as a container for states. Countries can be zone members, and also link to an address. The difference between one country and another on an address record can determine which tax rates and shipping methods are used for the order.
+
+## States
+
+States within Spree are used to scope address data slightly more than country. States are useful for tax purposes, as different states in a country may impose different tax rates on different products. In addition to this, different states may cause different tax rates and shipping methods to be used for an order, similar to how countries affect it also.
diff --git a/guides/src/content/developer/core/adjustments.md b/guides/src/content/developer/core/adjustments.md
new file mode 100644
index 00000000000..04582d79dcc
--- /dev/null
+++ b/guides/src/content/developer/core/adjustments.md
@@ -0,0 +1,146 @@
+---
+title: "Adjustments"
+section: core
+---
+
+## Overview
+
+An `Adjustment` object tracks an adjustment to the price of an [Order](orders), an order's [Line Item](orders#line-items), or an order's [Shipments](shipments) within a Spree Commerce storefront.
+
+Adjustments can be either positive or negative. Adjustments with a positive value are sometimes referred to as "charges" while adjustments with a negative value are sometimes referred to as "credits." These are just terms of convenience since there is only one `Spree::Adjustment` model in a storefront which handles this by allowing either positive or negative values.
+
+Adjustments can either be considered included or additional. An "included" adjustment is an adjustment to the price of an item which is included in that price of an item. A good example of this is a GST/VAT tax. An "additional" adjustment is an adjustment to the price of the item on top of the original item price. A good example of that would be how sales tax is handled in countries like the United States.
+
+Adjustments have the following attributes:
+
+* `amount` The dollar amount of the adjustment.
+* `label`: The label for the adjustment to indicate what the adjustment is for.
+* `eligible`: Indicates if the adjustment is eligible for the thing it's adjusting.
+* `mandatory`: Indicates if this adjustment is mandatory; i.e that this adjustment *must* be applied regardless of its eligibility rules.
+* `state`: Can either be `open` or `closed`. Once an adjustment is closed, it will not be automatically updated.
+* `included`: Whether or not this adjustment affects the final price of the item it is applied to. Used only for tax adjustments which may themselves be included in the price.
+
+Along with these attributes, an adjustment links to three polymorphic objects:
+
+* A source
+* An adjustable
+
+The *source* is the source of the adjustment. Typically a `Spree::TaxRate` object or a `Spree::PromotionAction` object.
+
+The *adjustable* is the object being adjusted, which is either an order, line item or shipment.
+
+Adjustments can come from one of two locations within Spree's core:
+
+* Tax Rates
+* Promotions
+
+An adjustment's `label` attribute can be used as a good indicator of where the adjustment is coming from.
+
+## Adjustment Scopes
+
+There are some helper methods to return the different types of adjustments:
+
+```ruby
+scope :shipping, -> { where(adjustable_type: 'Spree::Shipment') }
+scope :is_included, -> { where(included: true) }
+scope :additional, -> { where(included: false) }
+```
+
+* `open`: All open adjustments.
+* `tax`: All adjustments which have a source that is a `Spree::TaxRate` object
+* `price`: All adjustments which adjust a `Spree::LineItem` object.
+* `shipping`: All adjustments which adjust a `Spree::Shipment` object.
+* `promotion`: All adjustments where the source is a `Spree::PromotionAction` object.
+* `optional`: All adjustments which are not `mandatory`.
+* `return_authorization`: All adjustments where the source is a `Spree::ReturnAuthorization`.
+* `eligible`: Adjustments which have been determined to be `eligible` for their adjustable. Useful for determining which adjustments are applying to the adjustable.
+* `charge`: Adjustments which *increase* the price of their adjustable.
+* `credit`: Adjustments which *decrease* the price of their adjustable.
+* `included`: Adjustments which are included in the object's price. Typically tax adjustments.
+* `additional`: Adjustments which modify the object's price. The default for all adjustments.
+
+These scopes can be called on either the `Spree::Adjustment` class itself, or on an `adjustments` association. For example, calling any one of these three is
+valid:
+
+```ruby
+Spree::Adjustment.eligible
+order.adjustments.eligible
+line_item.adjustments.eligible
+shipment.adjustments.eligible
+```
+
+## Adjustment Associations
+
+As of Spree 2.2, you are able to retrieve the specific adjustments of an Order, a Line Item or a Shipment.
+
+An order itself, much like line items and shipments, can have its own individual modifications. For instance, an order with over $100 of line items may have 10% off. To retrieve these adjustments on the order, call the `adjustments` association:
+
+```ruby
+order.adjustments
+```
+
+If you want to retrieve all the adjustments for all the line items, shipments and the order itself, call the `all_adjustments` method:
+
+```ruby
+order.all_adjustments
+```
+
+If you want to grab just the line item adjustments, call `line_item_adjustments`:
+
+```ruby
+order.line_item_adjustments
+```
+
+Simiarly, if you want to grab the adjustments applied to shipments, call `shipment_adjustments`:
+
+```ruby
+order.shipment_adjustments
+```
+
+## Extending Adjustments
+
+### Creating a New Adjuster
+
+To create a new adjuster for Spree, create a new ruby object that inherits from `Spree::Adjustable::Adjuster::Base` and implements an `update` method:
+
+```ruby
+module Spree
+ module Adjustable
+ module Adjuster
+ class MyAdjuster < Spree::Adjustable::Adjuster::Base
+ def update
+ ...
+ #your ruby magic
+ ...
+ update_totals(some_total, my_other_total)
+ end
+
+ private
+
+ # Note to persist your totals you need to update @totals
+ # This is shown in a separate method for readability
+ def update_totals(some_total, my_other_total)
+ # if you want to keep track of your total,
+ # you will need the column defined
+ @totals[:total_you_want_to_track] += some_total
+ @totals[:taxable_adjustment_total] += some_total
+ @totals[:non_taxable_adjustment_total] += my_other_total
+ end
+ end
+ end
+ end
+end
+```
+
+Next you need to add the class to spree `Rails.application.config.spree.adjusters` so it is included whenever adjustments are updated (Promotion and Tax are included by default):
+
+```ruby
+# NOTE: it is advisable that that Tax be implemented last so Tax is calculated correctly
+app.config.spree.adjusters = [
+ Spree::Adjustable::Adjuster::MyAdjuster,
+ Spree::Adjustable::Adjuster::Promotion,
+ Spree::Adjustable::Adjuster::Tax
+ ]
+```
+
+That's it! Your custom adjuster is ready to go.
diff --git a/guides/src/content/developer/core/calculators.md b/guides/src/content/developer/core/calculators.md
new file mode 100644
index 00000000000..7587cea7550
--- /dev/null
+++ b/guides/src/content/developer/core/calculators.md
@@ -0,0 +1,249 @@
+---
+title: 'Calculators'
+section: core
+---
+
+## Overview
+
+Spree makes extensive use of the `Spree::Calculator` model and there are several subclasses provided to deal with various types of calculations (flat rate, percentage discount, sales tax, VAT, etc.) All calculators extend the `Spree::Calculator` class and must provide the following methods:
+
+```ruby
+def self.description
+ # Human readable description of the calculator
+end
+
+def compute(object=nil)
+ # Returns the value after performing the required calculation
+end
+```
+
+Calculators link to a `calculable` object, which are typically one of `Spree::ShippingMethod`, `Spree::TaxRate`, or `Spree::Promotion::Actions::CreateAdjustment`. These three classes use the `Spree::Core::CalculatedAdjustment` module described below to provide an easy way to calculate adjustments for their objects.
+
+## Available Calculators
+
+The following are descriptions of the currently available calculators in Spree. If you would like to add your own, please see the [Creating a New Calculator](#creating-a-new-calculator) section.
+
+### Default Tax
+
+For information about this calculator, please read the [Taxation](taxation) guide.
+
+### Flat Percent Per Item Total
+
+This calculator has one preference: `flat_percent` and can be set like this:
+
+```ruby
+calculator.preferred_flat_percent = 10
+```
+
+This calculator takes an order and calculates an amount using this calculation:
+
+```ruby
+[item total] x [flat percentage]
+```
+
+For example, if an order had an item total of $31 and the calculator was configured to have a flat percent amount of 10, the discount would be$3.10, because $31 x 10% =$3.10.
+
+### Flat Rate
+
+This calculator can be used to provide a flat rate discount.
+
+This calculator has two preferences: `amount` and `currency`. These can be set like this:
+
+```ruby
+calculator.preferred_amount = 10
+calculator.currency = "USD"
+```
+
+The currency for this calculator is used to check to see if a shipping method is available for an order. If an order's currency does not match the shipping method's currency, then that shipping method will not be displayed on the frontend.
+
+This calculator can take any object and will return simply the preferred amount.
+
+### Flexi Rate
+
+This calculator is typically used for promotional discounts when you want a specific discount for the first product, and then subsequent discounts for other products, up to a certain amount.
+
+This calculator takes three preferences:
+
+- `first_item`: The discounted price of the first item(s).
+- `additional_item`: The discounted price of subsequent items.
+- `max_items`: The maximum number of items this discount applies to.
+
+The calculator computes based on this:
+
+ [first item discount] + (([items_count*] - 1) x [additional item discount])
+
+- up to the `max_items`
+
+Thus, if you have ten items in your shopping cart, your `first_item` preference is set to \$10, your `additional_items` preference is set to $5, and your `max_items` preference is set to 4, the total discount would be$25:
+
+- \$10 for the first item
+- $5 for each of the 3 subsequent items:$5 \* 3 = \$15
+- \$0 for the remaining 6 items
+
+### Free Shipping
+
+This calculator will take an object, and then work out the shipping total for that object. Useful for when you want to apply free shipping to an order.
+
+$$
+This is a little confusing and vague. Need to investigate more and explain better. Also, might this be obsolete with the new split shipments functionality?
+$$
+
+### Per Item
+
+The Per Item calculator computes a value for every item within an order. This is useful for providing a discount for a specific product, without it affecting others.
+
+This calculator takes two preferences:
+
+- `amount`: The amount per item to calculate.
+- `currency`: The currency for this calculator.
+
+This calculator depends on its `calculable` responding to a `promotion` method, which should return a `Spree::Promotion` (or similar) object. This object should then return a list of rules, which should respond to a `products` method. This is used to return a result of matching products.
+
+The list of matching products is compared against the line items for the order being calculated. If any of the matching products are included in the order, they are eligible for this calculator. The calculation is this:
+
+[matching product quantity] x [amount]
+
+Every matching product within an order will add to the calculator's total. For example, assuming the calculator has an `amount` of 5 and there's an order with the following line items:
+
+- Product A: \$15.00 x 2 (within matching products)
+- Product B: \$10.00 x 1 (within matching products)
+- Product C: \$20.00 x 4 (excluded from matching products)
+
+The calculation would be:
+
+ = (2 x 5) + (1 x 5)
+ = 10 + 5
+
+meaning the calculator will compute an amount of 15.
+
+### Percent Per Item
+
+The Percent Per Item calculator works in a near-identical fashion to the [Per Item Calculator](#per-item), with the exception that rather than providing a flat-rate per item, it is a percentage.
+
+Assuming a calculator amount of 10% and an order such as this:
+
+- Product A: \$15.00 x 2 (within matching products)
+- Product B: \$10.00 x 1 (within matching products)
+- Product C: \$20.00 x 4 (excluded from matching products)
+
+The calculation would be:
+
+ = ($15 x 2 x 10%) + ($10 x 10%)
+ = ($30 x 10%) + ($10 x 10%)
+ = $3 + $1
+
+The calculator will calculate a discount of \$4.
+
+### Price Sack
+
+The Price Sack calculator is useful for when you want to provide a discount for an order which is over a certain price. The calculator has four preferences:
+
+- `minimal_amount`: The minimum amount for the line items total to trigger the calculator.
+- `discount_amount`: The amount to discount from the order if the line items total is equal to or greater than the `minimal_amount`.
+- `normal_amount`: The amount to discount from the order if the line items total is less than the `minimal_amount`.
+- `currency`: The currency for this calculator. Defaults to the currency you have set for your store with `Spree::Config[:currency]`
+
+Suppose you have a Price Sack calculator with a `minimal_amount` preference of \$50, a `normal_amount` preference of $2, and a `discount_amount` of$5. An order with a line items total of $60 would result in a discount of$5 for the whole order. An order of $20 would result in a discount of$2.
+
+## Creating a New Calculator
+
+To create a new calculator for Spree, you need to do two things. The first is to inherit from the `Spree::Calculator` class and define `description` and `compute` methods on that class:
+
+```ruby
+class CustomCalculator < Spree::Calculator
+ def self.description
+ # Human readable description of the calculator
+ end
+
+ def compute(object=nil)
+ # Returns the value after performing the required calculation
+ end
+end
+```
+
+If you are creating a new calculator for shipping methods, please be aware that you need to inherit from `Spree::ShippingCalculator` instead, and define a `compute_package` method:
+
+```ruby
+class CustomCalculator < Spree::ShippingCalculator
+ def self.description
+ # Human readable description of the calculator
+ end
+
+ def compute_package(package)
+ # Returns the value after performing the required calculation
+ end
+end
+```
+
+The second thing is to register this calculator as a tax, shipping, or promotion adjustment calculator by calling code like this at the end of `config/initializers/spree.rb` inside your application (`config` variable defined for brevity):
+
+```ruby
+config = Rails.application.config
+config.spree.calculators.tax_rates << CustomCalculator
+config.spree.calculators.shipping_methods << CustomCalculator
+config.spree.calculators.promotion_actions_create_adjustments << CustomCalculator
+```
+
+For example if your calculator is placed in `app/models/spree/calculator/shipping/my_own_calculator.rb` you should call:
+
+```ruby
+config = Rails.application.config
+config.spree.calculators.shipping_methods << Spree::Calculator::Shipping::MyOwnCalculator
+```
+
+### Determining Availability
+
+By default, all shipping method calculators are available at all times. If you wish to make this dependent on something from the order, you can re-define the `available?` method inside your calculator:
+
+```ruby
+class CustomCalculator < Spree::Calculator
+ def available?(object)
+ object.currency == "USD"
+ end
+end
+```
+
+## Calculated Adjustments
+
+If you wish to use Spree's calculator functionality for your own application, you can include the `Spree::Core::CalculatedAdjustments` module into a model of your choosing.
+
+```ruby
+class Plan < ActiveRecord::Base
+ include Spree::Core::CalculatedAdjustments
+end
+```
+
+To have calculators available for this class, you will need to register them:
+
+```ruby
+config.spree.calculators.plans << CustomCalculator
+```
+
+Then you can access these calculators by calling this method:
+
+```ruby
+Plan.calculators
+```
+
+Using this method, you can then display the calculators as you please. Each object for this new class will need to have a calculator associated so that adjustments can be calculated on them.
+
+This module provides a `has_one` association to a `calculator` object, as well as some convenience helpers for creating and updating adjustments for objects. Assuming that an object has a calculator associated with it first, creating an adjustment is simple:
+
+```ruby
+plan.create_adjustment("#{plan.name}", , )
+```
+
+To update this adjustment:
+
+```ruby
+plan.update_adjustment(, )
+```
+
+To work out what the calculator would compute an amount to be:
+
+```ruby
+plan.compute_amount()
+```
+
+`create_adjustment`, `update_adjustment` and `compute_amount` will call `compute` on the `Calculator` object. This `calculable` amount is whatever object your
+`CustomCalculator` class supports.
diff --git a/guides/src/content/developer/core/inventory.md b/guides/src/content/developer/core/inventory.md
new file mode 100644
index 00000000000..ff82dd365e9
--- /dev/null
+++ b/guides/src/content/developer/core/inventory.md
@@ -0,0 +1,60 @@
+---
+title: "Inventory"
+section: core
+---
+
+## Overview
+
+Spree uses a hybrid approach for tracking inventory: On-hand inventory is stored as a count on a variant `StockItem`. This gives good performance for stores with large inventories. Back-ordered, sold, or shipped products are stored as individual `InventoryUnit` objects so they can have relevant information attached to them.
+
+What if you don't need to track inventory? We have come up with a design that basically shields users of simple stores from much of this complexity. Simply set `Spree::Config[:track_inventory_levels]` to `false` and you never have to worry about it.
+
+New products created in the system can be given a starting "on hand" inventory level. You can subsequently set new inventory levels and the correct things will happen, e.g. adding new on-hand inventory to an out-of-stock product that has some backorders will first fill the backorders then update the product with the remaining inventory count.
+
+As of Spree 2.0, there is a new Stock Management system in place that allows for fine-grained control over inventory for products and variants.
+
+## Stock Management
+
+### Stock Locations
+
+Stock Locations are the locations where your inventory is shipped from. Each `StockLocation` has many `stock_items` and `stock_movements`.
+
+Stock Locations are created in the admin interface (Configuration → Stock Locations). Note that a `StockItem` will be added to the newly created `StockLocation` for each variant in your application.
+
+### Stock Items
+
+Stock Items represent the inventory at a stock location for a specific variant. Stock item count on hand can be increased or decreased by creating stock movements.
+
+***
+Note: Stock items are created automatically for each stock location you have. You don't need to manage these manually.
+***
+
+!!!
+**Count On Hand** is no longer an attribute on variants. It has been moved to stock items, as those are now used for inventory management.
+!!!
+
+### Stock Movements
+
+
+
+Stock movements allow you to manage the inventory of a stock item for a stock location. Stock movements are created in the admin interface by first navigating to the product you want to manage. Then, follow the "Stock Management" link in the sidebar.
+
+As shown in the image above, you can increase or decrease the count on hand available for a variant at a stock location. To increase the count on hand, make a stock movement with a positive quantity. To decrease the count on hand, make a stock movement with a negative quantity.
+
+### Stock Transfers
+
+
+
+Stock transfers allow you to move inventory in bulk from one stock location to another stock location. Transfers are created in the admin interface by first navigating to the Configuration page. Then, follow the "Stock Transfers" link.
+
+
+
+As shown in the image above, you can move stock from one location to a different location. This is done by selecting a source location, a destination location, and one or more variants. You are also able to set the quantity for each variant individually.
+
+If you check "Receive Stock" while creating a new transfer, your stock transfer will only have a destination stock location.
+
+## Return Authorizations
+
+After an order is shipped, administrators can approve the return of some part (maybe all) of an order via the "Return Authorizations" tab in the single order console. To create a new return authorization, you should indicate which part of the order is being returned, what the reason for the return is, and what the resulting credit should be. The sale price of the product is shown for reference, but you can choose any value you like.
+
+After the authorization is created, you can return later to its edit page and click on the 'Received' button to register the return of goods. This will create a credit adjustment on the order, which you can apply (i.e. refund) to the order's credit card via the payments screen. Spree will log the events in the order's history.
\ No newline at end of file
diff --git a/guides/src/content/developer/core/orders.md b/guides/src/content/developer/core/orders.md
new file mode 100644
index 00000000000..323905f5ebc
--- /dev/null
+++ b/guides/src/content/developer/core/orders.md
@@ -0,0 +1,117 @@
+---
+title: "Orders"
+section: core
+---
+
+## Overview
+
+The `Order` model is one of the key models in Spree. It provides a central place around which to collect information about a customer order - including line items, adjustments, payments, addresses, return authorizations, and shipments.
+
+Orders have the following attributes:
+
+* `number`: The unique identifier for this order. It begins with the letter R and ends in a 9-digit number. This number is shown to the users, and can be used to find the order by calling `Spree::Order.find_by(number: number)`.
+* `item_total`: The sum of all the line items for this order.
+* `adjustment_total`: The sum of all adjustments on this order.
+* `total`: The result of the sum of the `item_total` and the `adjustment_total`.
+* `payment_total`: The total value of all finalized payments.
+* `shipment_total`: The total value of all shipments' costs.
+* `additional_tax_total`: The sum of all shipments' and line items' `additional_tax`.
+* `included_tax_total`: The sum of all shipments' and line items' `included_tax`.
+* `promo_total`: The sum of all shipments', line items' and promotions' `promo_total`.
+* `state`: The current state of the order. To read more about the states an order goes through, read [The Order State Machine](#the-order-state-machine) section of this guide.
+* `email`: The email address for the user who placed this order. Stored in case this order is for a guest user.
+* `user_id`: The ID for the corresponding user record for this order. Stored only if the order is placed by a signed-in user.
+* `completed_at`: The timestamp of when the order was completed.
+* `bill_address_id`: The ID for the related `Address` object with billing address information.
+* `ship_address_id`: The ID for the related `Address` object with shipping address information.
+* `shipping_method_id`: The ID for the related `ShippingMethod` object.
+* `created_by_id`: The ID of object that created this order.
+* `shipment_state`: The current shipment state of the order. For possible states, please see the [Shipments guide](shipments).
+* `payment_state`: The current payment state of the order. For possible states, please see the [Payments guide](payments).
+* `special_instructions`: Any special instructions for the store to do with this order. Will only appear if `Spree::Config[:shipping_instructions]` is set to `true`.
+* `currency`: The currency for this order. Determined by the `Spree::Config[:currency]` value that was set at the time of order.
+* `last_ip_address`: The last IP address used to update this order in the frontend.
+* `channel`: The channel specified when importing orders from other stores. e.g. amazon.
+* `item_count`: The total value of line items' quantity.
+* `approver_id`: The ID of user that approved this order.
+* `confirmation_delivered`: Boolean value indicating that confirmation email was delivered.
+* `token`: The token stored corresponding to token stored in cookies.
+* `canceler_id`: The ID of user that canceled this order.
+* `store_id`: The ID of `Store` in which this order was created.
+
+
+Some methods you may find useful:
+
+* `outstanding_balance`: The outstanding balance for the order, calculated by taking the `total` and subtracting `payment_total`.
+* `display_item_total`: A "pretty" version of `item_total`. If `item_total` was `10.0`, `display_item_total` would be `$10.00`.
+* `display_adjustment_total`: Same as above, except for `adjustment_total`.
+* `display_total`: Same as above, except for `total`.
+* `display_outstanding_balance`: Same as above, except for `outstanding_balance`.
+
+## The Order State Machine
+
+Orders flow through a state machine, beginning at a `cart` state and ending up at a `complete` state. The intermediary states can be configured using the [Checkout Flow API](checkout).
+
+The default states are as follows:
+
+* `cart`
+* `address`
+* `delivery`
+* `payment`
+* `confirm`
+* `complete`
+
+The `payment` state will only be triggered if `payment_required?` returns `true`.
+
+The `confirm` state will only be triggered if `confirmation_required?` returns `true`.
+
+The `complete` state can only be reached in one of two ways:
+
+1. No payment is required on the order.
+2. Payment is required on the order, and at least the order total has been received as payment.
+
+Assuming that an order meets the criteria for the next state, you will be able to transition it to the next state by calling `next` on that object. If this returns `false`, then the order does *not* meet the criteria. To work out why it cannot transition, check the result of an `errors` method call.
+
+## Line Items
+
+Line items are used to keep track of items within the context of an order. These records provide a link between orders, and [Variants](products#variants).
+
+When a variant is added to an order, the price of that item is tracked along with the line item to preserve that data. If the variant's price were to change, then the line item would still have a record of the price at the time of ordering.
+
+* Inventory tracking notes
+
+$$$
+Update this section after Chris+Brian have done their thing.
+$$$
+
+## Addresses
+
+An order can link to two `Address` objects. The shipping address indicates where the order's product(s) should be shipped to. This address is used to determine which shipping methods are available for an order.
+
+The billing address indicates where the user who's paying for the order is located. This can alter the tax rate for the order, which in turn can change how much the final order total can be.
+
+For more information about addresses, please read the [Addresses](addresses) guide.
+
+## Adjustments
+
+Adjustments are used to affect an order's final cost, either by decreasing it ([Promotions](promotions)) or by increasing it ([Shipping](shipments), [Taxes](taxation)).
+
+For more information about adjustments, please see the [Adjustments](adjustments) guide.
+
+## Payments
+
+Payment records are used to track payment information about an order. For more information, please read the [Payments](payments) guide.
+
+## Return Authorizations
+
+$$$
+document return authorizations.
+$$$
+
+## Updating an Order
+
+If you change any aspect of an `Order` object within code and you wish to update the order's totals -- including associated adjustments and shipments -- call the `update_with_updater!` method on that object, which calls out to the `OrderUpdater` class.
+
+For example, if you create or modify an existing payment for the order which would change the order's `payment_state` to a different value, calling `update_with_updater!` will cause the `payment_state` to be recalculated for that order.
+
+Another example is if a `LineItem` within the order had its price changed. Calling `update_with_updater!` will cause the totals for the order to be updated, the adjustments for the order to be recalculated, and then a final total to be established.
diff --git a/guides/src/content/developer/core/payments.md b/guides/src/content/developer/core/payments.md
new file mode 100644
index 00000000000..de9a101c304
--- /dev/null
+++ b/guides/src/content/developer/core/payments.md
@@ -0,0 +1,190 @@
+---
+title: "Payments"
+section: core
+---
+
+## Overview
+
+Spree has a highly flexible payments model which allows multiple payment methods to be available during checkout. The logic for processing payments is decoupled from orders, making it easy to define custom payment methods with their own processing logic.
+
+Payment methods typically represent a payment gateway. Gateways will process card payments, and may also include non-gateway methods of payment such as Check, which is provided in Spree by default.
+
+The `Payment` model in Spree tracks payments against [Orders](orders). Payments relate to a `source` which indicates how the payment was made, and a `PaymentMethod`, indicating the processor used for this payment.
+
+When a payment is created, it is given a unique, 8-character identifier. This is used when sending the payment details to the payment processor. Without this identifier, some payment gateways mistakenly reported duplicate payments.
+
+A payment can go through many different states, as illustrated below.
+
+
+
+An explanation of the different states:
+
+* `checkout`: Checkout has not been completed
+* `processing`: The payment is being processed (temporary – intended to prevent double submission)
+* `pending`: The payment has been processed but is not yet complete (ex. authorized but not captured)
+* `failed`: The payment was rejected (ex. credit card was declined)
+* `void`: The payment should not be counted against the order
+* `completed`: The payment is completed. Only payments in this state count against the order total
+
+The state transition for these is handled by the processing code within Spree; however, you are able to call the event methods yourself to reach these states. The event methods are:
+
+* `started_processing`
+* `failure`
+* `pend`
+* `complete`
+* `void`
+
+## Payment Methods
+
+Payment methods represent the different options a customer has for making a payment. Most sites will accept credit card payments through a payment gateway, but there are other options. Spree also comes with built-in support for a Check payment, which can be used to represent any offline payment. There are also third-party extensions that provide support for some other interesting options such as [spree_braintree_vzero](https://github.com/spree-contrib/spree_braintree_vzero) for Braintree & PayPal payment methods.
+
+A `PaymentMethod` can have the following attributes:
+
+* `type`: The subclass of `Spree::PaymentMethod` this payment method represents. Uses rails single table inheritance feature.
+* `name`: The visible name for this payment method
+* `description`: The description for this payment method
+* `active`: Whether or not this payment method is active. Set it `false` to hide it in frontend.
+* `display_on`: Determines where the payment method can be visible. Values can be `front` for frontend, `back` for backend or `both` for both.
+
+### Payment Method Visibility
+
+The appearance of the payment methods on the frontend and backend depend on several criteria used by the `PaymentMethod.available` method. The code is this:
+
+```ruby
+def self.available(display_on = 'both')
+ all.select do |p|
+ p.active &&
+ (p.display_on == display_on.to_s || p.display_on.blank?)
+ end
+end
+```
+
+If a payment method meets these criteria, then it will be available.
+
+### Auto-Capturing
+
+By default, a payment method's `auto_capture?` method depends on the `Spree::Config[:auto_capture]` preference. If you have set this preference to `true`, but don't want a payment method to be auto-capturable like other payment methods in your system, you can override the `auto_capture?` method in your
+`PaymentMethod` subclass:
+
+```ruby
+class FancyPaymentMethod < Spree::PaymentMethod
+ def auto_capture?
+ false
+ end
+end
+```
+
+The result of this method determines if a payment will be automatically captured (true) or only authorized (false) during the processing of the payment.
+
+## Payment Processing
+
+Payment processing in Spree supports many different gateways, but also attempts to comply with the API provided by the [active_merchant](https://github.com/shopify/active_merchant) gem where possible.
+
+### Gateway Options
+
+For every gateway action, a list of gateway options are passed through.
+
+* `email` and `customer`: The email address related to the order
+* `ip`: The last IP address for the order
+* `order_id`: The Order's `number` attribute, plus the `identifier` for each payment, generated when the payment is first created
+* `shipping`: The total shipping cost for the order, in cents
+* `tax`: The total tax cost for the order, in cents
+* `subtotal`: The item total for the order, in cents
+* `currency`: The 3-character currency code for the order
+* `discount`: The promotional discount applied to the order
+* `billing_address`: A hash containing billing address information
+* `shipping_address`: A hash containing shipping address information
+
+The billing address and shipping address data is as follows:
+
+* `name`: The combined `first_name` and `last_name` from the address
+* `address1`: The first line of the address information
+* `address2`: The second line of address information
+* `city`: The city of the address
+* `state`: An abbreviated version of the state name or, failing that, the state name itself, from the related `State` object. If that fails, the `state_name` attribute from the address.
+* `country`: The ISO name for the country. For example, United States of America is "US", Australia is "AU".
+* `phone`: The phone number associated with the address
+
+### Credit Card Data
+
+Spree stores only the type, expiration date, name and last four digits for the card on your server. This data can then be used to present to the user so that they can verify that the correct card is being used. All credit card data sent through forms is sent through immediately to the gateways, and is not stored for any period of time.
+
+### Processing Walkthrough
+
+When an order is completed in spree, each `Payment` object associated with the order has the `process!` method called on it (unless `payment_required?` for the order returns `false`), in order to attempt to automatically fulfill the payment required for the order. If the payment method requires a source, and the payment has a source associated with it, then Spree will attempt to process the payment. Otherwise, the payment will need to be processed manually.
+
+If the `PaymentMethod` object is configured to auto-capture payments, then the `Payment#purchase!` method will be called, which will call `PaymentMethod#purchase` like this:
+
+```ruby
+payment_method.purchase(, , )
+```
+
+If the payment is *not* configured to auto-capture payments, the `Payment#authorize!` method will be called, with the same arguments as the `purchase` method above:
+
+```ruby
+payment_method.authorize(, , )
+```
+
+How the payment is actually put through depends on the `PaymentMethod` sub-class' implementation of the `purchase` and `authorize` methods.
+
+The returned object from both the `purchase` and `authorize` methods on the payment method objects must be an `ActiveMerchant::Billing::Response` object. This response object is then stored (in YAML) in the `spree_log_entries` table. Log entries can be retrieved with a call to the `log_entries` association on any `Payment` object.
+
+If the `purchase!` route is taken and is successful, the payment is marked as `completed`. If it fails, it is marked as `failed`. If the `authorize` method is successful, the payment is transitioned to the `pending` state so that it can be manually captured later by calling the `capture!` method. If it is unsuccessful, it is also transitioned to the `failed` state.
+
+***
+Once a payment has been saved, it also updates the order. This may trigger the `payment_state` to change, which would reflect the current payment state of the order. The possible states are:
+
+* `balance_due`: Indicates that payment is required for this order
+* `failed`: Indicates that the last payment for the order failed
+* `credit_owed`: This order has been paid for in excess of its total
+* `paid`: This order has been paid for in full.
+***
+
+!!!
+You may want to keep tabs on the number of orders with a `payment_state` of `failed`. A sudden increase in the number of such orders could indicate a problem with your credit card gateway and most likely indicates a serious problem affecting customer satisfaction. You should check the latest `log_entries` for the most recent payments in the store if this is happening.
+!!!
+
+### Log Entries
+
+Responses from payment gateways within Spree are typically `ActiveMerchant::Billing::Response` objects. When Spree handles a response from a payment gateway, it will serialize the object as YAML and store it in the database as a log entry for a payment. These responses can be useful for debugging why a payment has failed.
+
+You can get a list of these log entries by calling the `log_entries` on any `Spree::Payment` object. To get the `Active::Merchant::Billing::Response` out of these `Spree::LogEntry` objects, call the `details` method.
+
+## Supported Gateways
+
+Access to a number of payment gateways is handled with the usage of the [spree_gateway](https://github.com/spree/spree_gateway) extension. This extension currently supports the following gateways:
+
+* Authorize.Net
+* Balanced
+* Beanstram
+* Braintree
+* eWAY
+* LinkPoint
+* Moneris
+* PayPal
+* Sage Pay
+* Samurai
+* Skrill
+* Stripe
+* USA ePay
+* WorldPay
+
+With the `spree_gateway` gem included in your application's `Gemfile`, these gateways will be selectable in the admin backend for payment methods.
+
+***
+These are just some of the gateways which are supported by the Active Merchant gem. You can see a [list of all the Active Merchant gateways on that project's GitHub page](https://github.com/Shopify/active_merchant#supported-direct-payment-gateways).
+
+In order to implement a new gateway in the spree_gateway project, please refer to the other gateways within `app/models/spree/gateway` inside that project.
+***
+
+## Adding your custom gateway
+
+In order to make your custom gateway show up on backend list of available payment methods
+you need to add it to spree config list of payment methods first. That can be achieved
+by adding the following code in your spree.rb for example:
+
+```ruby
+Rails.application.config.spree.payment_methods << YourCustomGateway
+```
+
+[spree_braintree_vzero](https://github.com/spree-contrib/spree_braintree_vzero) is a good example of a standalone custom gateways.
diff --git a/guides/src/content/developer/core/preferences.md b/guides/src/content/developer/core/preferences.md
new file mode 100644
index 00000000000..4a76cc91b3c
--- /dev/null
+++ b/guides/src/content/developer/core/preferences.md
@@ -0,0 +1,439 @@
+---
+title: "Preferences"
+section: core
+---
+
+## Overview
+
+Spree Preferences support general application configuration and preferences per model instance. Spree comes with preferences for your store like `logo` and `currency`. Additional preferences can be added by your application or included extensions.
+
+To implement preferences for a model, simply add a new column called `preferences`. This is an example migration for the `spree_products` table:
+
+```ruby
+class AddPreferencesColumnToSpreeProducts < ActiveRecord::Migration[4.2]
+ def up
+ add_column :spree_products, :preferences, :text
+ end
+
+ def down
+ remove_column :spree_products, :preferences
+ end
+end
+```
+
+This will work because `Spree::Product` is a subclass of `Spree::Base`. If found, the `preferences` attribute gets serialized into a `Hash` and merged with the default values.
+
+As another example, you might want to add preferences for users to manage their notification settings. Just make sure your `User` model inherits from `Spree::Base` then add the `preferences` column. You'll then be able to define preferences for `User`s without adding extra columns to the database table.
+
+> If you're using `spree_auth_devise`, note that the provided `Spree::User` doesn't inherit from `Spree::Base`.
+
+Extensions may add to the Spree General Settings or create their own namespaced preferences.
+
+The first several sections of this guide describe preferences in a very general way. If you're just interested in making modifications to the existing preferences, you can skip ahead to the [Configuring Spree Preferences section](#configuring-spree-preferences). If you would like a more in-depth understanding of the underlying concepts used by the preference system, please read on.
+
+### Motivation
+
+Preferences for models within an application are very common. Although the rule of thumb is to keep the number of preferences available to a minimum, sometimes it's necessary if you want users to have optional preferences like disabling e-mail notifications.
+
+Both use cases are handled by Spree Preferences. They are easy to define, provide quick cached reads, persist across restarts and do not require additional columns to be added to your models' tables.
+
+## General Settings
+
+Spree comes with many application-wide preferences. They are defined in `core/app/models/spree/app_configuration.rb` and made available to your code through `Spree::Config`, e.g., `Spree::Config.logo`.
+
+A limited set of the general settings are available in the admin interface of your store (`/admin/general_settings`).
+
+You can add additional preferences under the `spree/app_configuration` namespace or create your own subclass of `Preferences::Configuration`.
+
+```ruby
+# These will be saved with key: spree/app_configuration/hot_salsa
+Spree::AppConfiguration.class_eval do
+ preference :hot_salsa, :boolean
+ preference :dark_chocolate, :boolean, default: true
+ preference :color, :string
+ preference :favorite_number
+ preference :language, :string, default: 'English'
+end
+
+# Spree::Config is an instance of Spree::AppConfiguration
+Spree::Config.hot_salsa = false
+
+# Create your own class
+# These will be saved with key: kona/store_configuration/hot_coffee
+Kona::StoreConfiguration < Preferences::Configuration
+ preference :hot_coffee, :boolean
+ preference :color, :string, default: 'black'
+end
+
+KONA::STORE_CONFIG = Kona::StoreConfiguration.new
+puts KONA::STORE_CONFIG.hot_coffee
+```
+
+## Defining Preferences
+
+You can define preferences for a model within the model itself:
+
+```ruby
+class User < ActiveRecord::Base
+ preference :hot_salsa, :boolean
+ preference :dark_chocolate, :boolean, default: true
+ preference :color, :string
+ preference :favorite_number, :integer
+ preference :language, :string, default: "English"
+end
+```
+
+In the above model, five preferences have been defined:
+
+* `hot_salsa`
+* `dark_chocolate`
+* `color`
+* `favorite_number`
+* `language`
+
+For each preference, a data type is provided. The types available are:
+
+* `boolean`
+* `string`
+* `password`
+* `integer`
+* `text`
+* `array`
+* `hash`
+
+An optional default value may be defined which will be used unless a value has been set for that specific instance.
+
+## Accessing Preferences
+
+Once preferences have been defined for a model, they can be accessed either using the shortcut methods that are generated for each preference or the generic methods that are not specific to a particular preference.
+
+### Shortcut Methods
+
+There are several shortcut methods that are generated. They are shown below.
+
+Query methods:
+
+```ruby
+user.prefers_hot_salsa? # => false
+user.prefers_dark_chocolate? # => false
+```
+
+Reader methods:
+
+```ruby
+user.preferred_color # => nil
+user.preferred_language # => "English"
+```
+
+Writer methods:
+
+```ruby
+user.prefers_hot_salsa = false # => false
+user.preferred_language = "English" # => "English"
+```
+
+Check if a preference is available:
+
+```ruby
+user.has_preference? :hot_salsa
+```
+
+### Generic Methods
+
+Each shortcut method is essentially a wrapper for the various generic methods shown below:
+
+Query method:
+
+```ruby
+user.prefers?(:hot_salsa) # => false
+user.prefers?(:dark_chocolate) # => false
+```
+
+Reader methods:
+
+```ruby
+user.preferred(:color) # => nil
+user.preferred(:language) # => "English"
+```
+
+```ruby
+user.get_preference :color
+user.get_preference :language
+```
+
+Writer method:
+
+```ruby
+user.set_preference(:hot_salsa, false) # => false
+user.set_preference(:language, "English") # => "English"
+```
+
+### Accessing All Preferences
+
+You can get a hash of all stored preferences by accessing the `preferences` helper:
+
+```ruby
+user.preferences # => {"language"=>"English", "color"=>nil}
+```
+
+This hash will contain the value for every preference that has been defined for the model instance, whether the value is the default or one that has been previously stored.
+
+### Default and Type
+
+You can access the default value for a preference:
+
+```ruby
+user.preferred_color_default # => 'blue'
+```
+
+Types are used to generate forms or display the preference. You can also get the type defined for a preference:
+
+```ruby
+user.preferred_color_type # => :string
+```
+
+## Configuring Spree Preferences
+
+Up until now we've been discussing the general preference system that was adapted to Spree. This has given you a general idea of what types of preference features are theoretically supported. Now, let's start to look specifically at how Spree is using these preferences for configuration.
+
+### Reading the Current Preferences
+
+At the heart of Spree preferences lies the `Spree::Config` constant. This object provides general access to the configuration settings anywhere in the application.
+
+These settings can be accessed from initializers, models, controllers, views, etc.
+
+The `Spree::Config` constant returns an instance of `Spree::AppConfiguration` which is where the default values for all of the general Spree preferences are defined.
+
+You can access these preferences directly in code. To see this in action, just fire up `rails console` and try the following:
+
+```ruby
+>> Spree::Config.admin_interface_logo
+=> "logo/spree_50.png"
+>> Spree::Config.admin_products_per_page
+=> 10
+```
+
+The above examples show the default configuration values for these preferences. The defaults themselves are coded within the `Spree::AppConfiguration` class.
+
+```ruby
+class Spree::AppConfiguration < Configuration
+ #... snip ...
+ preference :allow_guest_checkout, :boolean, default: true
+ #... snip ...
+end
+```
+
+If you are using the default preferences without any modifications, then nothing will be stored in the database. If you set a value for the preference it will save it to `spree_preferences` or in our `preferences` column. It will use a memory cached version to maintain performance.
+
+### Overriding the Default Preferences
+
+The default Spree preferences in `Spree::AppConfiguration` can be changed using the `set` method of the `Spree::Config` module. For example to set the number of products shown on the products listing in the admin interface we could do the following:
+
+```ruby
+>> Spree::Config.admin_products_per_page = 20
+=> 20
+>> Spree::Config.admin_products_per_page
+=> 20
+```
+
+Here we are changing a preference to something other than the default as specified in `Spree::AppConfiguration`. In this case the preference system will persist the new value in the `spree_preferences` table.
+
+### Configuration Through the Spree Initializer
+
+During the Spree installation process, an initializer file is created within your application's source code. The initializer is found under `config/initializers/spree.rb`:
+
+```ruby
+Spree.config do |config|
+ # Example:
+ # Uncomment to override the default logo location.
+ # config.logo = 'logo/my_store.png'
+end
+```
+
+The `Spree.config` block acts as a shortcut to setting `Spree::Config` multiple times. If you have multiple default preferences you would like to override within your code you may override them here. Using the initializer for setting the defaults is a nice shortcut, and helps keep your preferences organized in a standard location.
+
+For example if you would like to change the logo location and if you want to tax using the shipping address you can accomplish this by doing the following:
+
+```ruby
+Spree.config do |config|
+ config.logo = 'logo/my_store.png'
+ config.tax_using_ship_address = true
+end
+```
+
+***
+Initializing preferences in `config/initializers/spree.rb` will overwrite any changes that were made through the admin user interface when you restart.
+***
+
+### Configuration Through the Admin Interface
+
+The Spree admin interface has several different screens where various settings can be configured. For instance, the `admin/general_settings` URL in your Spree application can be used to configure the values for the site name and the site URL. This is basically equivalent to calling `Spree::Config.set(currency: "CDN", currency_thousands_separator: " ")` directly in your Ruby code.
+
+## Site-Wide Preferences
+
+You can define preferences that are site-wide and don't apply to a specific instance of a model by creating a configuration file that inherits from `Spree::Preferences::Configuration`.
+
+```ruby
+class Spree::MyApplicationConfiguration < Spree::Preferences::Configuration
+ preference :theme, :string, default: "Default"
+ preference :show_splash_page, :boolean
+ preference :number_of_articles, :integer
+end
+```
+
+In the above configuration file, three preferences have been defined:
+
+* theme
+* show_splash_page
+* number_of_articles
+
+It is recommended to create the configuration file in the `lib/` directory.
+
+***
+Extensions can also define site-wide preferences. For more information on using preferences like this with extensions, check out the [Extensions Tutorial](extensions_tutorial).
+***
+
+### Configuring Site-Wide Preferences
+
+The recommended way to configure site-wide preferences is through an initializer. Let's take a look at configuring the preferences defined in the previous configuration example.
+
+```ruby
+module Spree
+ MyApp::Config = Spree::MyApplicationConfiguration.new
+end
+
+MyApp::Config[:theme] = "blue_theme"
+MyApp::Config[:show_spash_page] = true
+MyApp::Config[:number_of_articles] = 5
+```
+
+The `MyApp` name used here is an example and should be replaced with your actual application's name, found in `config/application.rb`.
+
+The above example will configure the preferences we defined earlier. Take note of the second line. In order to set and get preferences using `MyApp::Config`, we must first instantiate the configuration object.
+
+## Spree Configuration Options
+
+This section lists all of the configuration options for the current version of Spree.
+
+`address_requires_state`
+
+Will determine if the state field should appear on the checkout page. Defaults to `true`.
+
+`admin_interface_logo`
+
+The path to the logo to display on the admin interface. Can be different from `Spree::Config[:logo]`. Defaults to `logo/spree_50.png`.
+
+`admin_products_per_page`
+
+How many products to display on the products listing in the admin interface. Defaults to 30.
+
+`allow_checkout_on_gateway_error`
+
+Continues the checkout process even if the payment gateway error failed. Defaults to `false`.
+
+`alternative_shipping_phone`
+
+Determines if an alternative phone number should be present for the shipping address on the checkout page. Defaults to `false`.
+
+`always_put_site_name_in_title`
+
+Determines if the site name (`current_store.site_name`) should be placed into the title. Defaults to `true`.
+
+`attachment_default_url`
+
+Tells `Paperclip` the form of the URL to use for attachments which are missing.
+
+`attachment_path`
+
+Tells `Paperclip` the path at which to store images.
+
+`attachment_styles`
+
+A JSON hash of different styles that are supported by attachments. Defaults to:
+
+```json
+{
+ "mini":"48x48>",
+ "small":"100x100>",
+ "product":"240x240>",
+ "large":"600x600>"
+}
+```
+
+`attachment_default_style`
+
+A key from the list of styles from `Spree::Config[:attachment_styles]` that is the default style for images. Defaults to the the `product` style.
+
+`auto_capture`
+
+Depending on whether or not Spree is configured to "auto capture" the credit card, either a purchase or an authorize operation will be performed on the card (via the current credit card gateway). Defaults to `false`.
+
+`checkout_zone`
+
+Limits the checkout to countries from a specific zone, by name. Defaults to `nil`.
+
+`company`
+
+Determines whether or not a field for "Company" displays on the checkout pages for shipping and billing addresses. Defaults to `false`.
+
+`currency`
+
+The three-letter currency code for the currency that prices will be displayed in. Defaults to "USD".
+
+`default_country_id`
+
+The default country's id. Defaults to 214, as this is the id for the United States within the seed data.
+
+`layout`
+
+The path to the layout of your application, relative to the `app/views` directory. Defaults to `spree/layouts/spree_application`. To make Spree use your application's layout rather than Spree's default, use this:
+
+```ruby
+Spree.config do |config|
+ config.layout = "application"
+end
+```
+
+`logo`
+
+The logo to display on your frontend. Defaults to `logo/spree_50.png`.
+
+`max_level_in_taxons_menu`
+
+The number of levels to descend when viewing a taxon menu. Defaults to `1`.
+
+`admin_orders_per_page`
+
+The number of orders to display on the orders listing in the admin backend. Defaults to `30`.
+
+`prices_inc_tax`
+
+Determines if prices are labelled as including tax or not. Defaults to `false`.
+
+`shipment_inc_vat`
+
+Determines if shipments should include VAT calculations. Defaults to `false`.
+
+`shipping_instructions`
+
+Determines if shipping instructions are requested or not when checking out. Defaults to `false`.
+
+`show_descendents`
+
+Determines if taxon descendants are shown when showing taxons. Defaults to `true`.
+
+`show_only_complete_orders_by_default`
+
+Determines if, on the admin listing screen, only completed orders should be shown. Defaults to `true`.
+
+`show_variant_full_price`
+
+Determines if the variant's full price or price difference from a product should be displayed on the product's show page. Defaults to `false`.
+
+`tax_using_ship_address`
+
+Determines if tax information should be based on shipping address, rather than the billing address. Defaults to `true`.
+
+`track_inventory_levels`
+
+Determines if inventory levels should be tracked when products are purchased at checkout. This option causes new `InventoryUnit` objects to be created when a product is bought. Defaults to `true`.
diff --git a/guides/src/content/developer/core/products.md b/guides/src/content/developer/core/products.md
new file mode 100644
index 00000000000..e0c5034e8ac
--- /dev/null
+++ b/guides/src/content/developer/core/products.md
@@ -0,0 +1,140 @@
+---
+title: "Products"
+section: core
+---
+
+## Overview
+
+`Product` records track unique products within your store. These differ from [Variants](#variants), which track the unique variations of a product. For instance, a product that's a T-shirt would have variants denoting its different colors. Together, Products and Variants describe what is for sale.
+
+Products have the following attributes:
+
+* `name`: short name for the product
+* `description`: The most elegant, poetic turn of phrase for describing your product's benefits and features to your site visitors
+* `permalink`: An SEO slug based on the product name that is placed into the URL for the product
+* `available_on`: The first date the product becomes available for sale online in your shop. If you don't set the `available_on` attribute, the product will not appear among your store's products for sale.
+* `deleted_at`: The date the product is no longer available for sale in the store
+* `meta_description`: A description targeted at search engines for search engine optimization (SEO)
+* `meta_keywords`: Several words and short phrases separated by commas, also targeted at search engines
+
+To understand how variants come to be, you must first understand option types and option values.
+
+## Option Types and Option Values
+
+Option types denote the different options for a variant. A typical option type would be a size, with that option type's values being something such as "Small", "Medium" and "Large". Another typical option type could be a color, such as "Red", "Green", or "Blue".
+
+A product can be assigned many option types, but must be assigned at least one if you wish to create variants for that product.
+
+## Variants
+
+`Variant` records track the individual variants of a `Product`. Variants are of two types: master variants and normal variants.
+
+Variant records can track some individual properties regarding a variant, such as height, width, depth, and cost price. These properties are unique to each variant, and so are different from [Product Properties](#product-properties), which apply to all variants of that product.
+
+### Master Variants
+
+Every single product has a master variant, which tracks basic information such as a count on hand, a price and a SKU. Whenever a product is created, a master variant for that product will be created too.
+
+Master variants are automatically created along with a product and exist for the sole purpose of having a consistent API when associating variants and [line items](orders#line-items). If there were no master variant, then line items would need to track a polymorphic association which would either be a product or a variant.
+
+By having a master variant, the code within Spree to track is simplified.
+
+### Normal Variants
+
+Variants which are not the master variant are unique based on [option type and option value](#option_type) combinations. For instance, you may be selling a product which is a Baseball Jersey, which comes in the sizes "Small", "Medium" and "Large", as well as in the colors of "Red", "Green" and "Blue". For this combination of sizes and colors, you would be able to create 9 unique variants:
+
+* Small, Red
+* Small, Green
+* Small, Blue
+* Medium, Red
+* Medium, Green
+* Medium, Blue
+* Large, Red
+* Large, Green
+* Large, Blue
+
+## Images
+
+Images link to a product through its master variant. The sub-variants for the product may also have their own unique images to differentiate them in the frontend.
+
+Spree automatically handles creation and storage of several size versions of each image (via the Paperclip plugin). The default styles are as follows:
+
+```ruby
+styles: {
+ mini: '48x48>',
+ small: '100x100>',
+ product: '240x240>',
+ large: '600x600>'
+}
+```
+
+These sizes can be changed by altering the value of `Spree::Image.attachment_definitions[:attachment][:styles]`. Once `Spree::Image.attachment_definitions[:attachment][:styles]` has been changed, you *must* regenerate the paperclip thumbnails by running this command:
+
+```bash
+$ bundle exec rake paperclip:refresh:thumbnails CLASS=Spree::Image
+```
+
+If you want to change the image that is displayed when a product has no image, simply set `Spree::Image.attachment_definitions[:attachment][:default_url]` with a path to the image that you want to use like this: `/assets/images/missing_:style.png`. These image styles must match the keys within `Spree::Config[:attachment_styles]`. That means that ideally, you'd have four images of different sizes: `missing_mini.png`, `missing_small.png`, `missing_product.png`, and `missing_large.png`.
+
+## Product Properties
+
+Product properties track individual attributes for a product which don't apply to all products. These are typically additional information about the item. For instance, a T-Shirt may have properties representing information about the kind of material used, as well as the type of fit the shirt is.
+
+A `Property` should not be confused with an [`OptionType`](#option_type), which is used when defining [Variants](#variants) for a product.
+
+You can retrieve the value for a property on a `Product` object by calling the `property` method on it and passing through that property's name:
+
+```bash
+$ product.property("material")
+=> "100% Cotton"
+```
+
+You can set a property on a product by calling the `set_property` method:
+
+```ruby
+product.set_property("material", "100% cotton")
+```
+
+If this property doesn't already exist, a new `Property` instance with this name will be created.
+
+## Multi-Currency Support
+
+`Price` objects track a price for a particular currency and variant combination. For instance, a [Variant](#variants) may be available for $15 (15 USD) and €7 (7 Euro).
+
+This presence or lack of a price for a variant in a particular currency will determine if that variant is visible in the frontend. If no variants of a product have a particular price value for the site's current currency, that product will not be visible in the frontend.
+
+You may see what price a product would be in the current currency (`Spree::Config[:currency]`) by calling the `price` method on that instance:
+
+```bash
+$ product.price
+=> "15.99"
+```
+
+To find a list of currencies that this product is available in, call `prices` to get a list of related `Price` objects:
+
+```bash
+$ product.prices
+=> [# Promotions tab where you can set up new Promotions, edit rules & actions, etc.
+
+Promotions can be activated in three different ways:
+
+* When a user adds a product to their cart
+* When a user enters a coupon code during the checkout process
+* When a user visits a page within the Spree store
+
+Promotions for these individual ways are activated through their corresponding `PromotionHandler` class, once they've been checked for eligibility.
+
+Promotions relate to two other main components: `actions` and `rules`. When a promotion is activated, the actions for the promotion are performed, passing in the payload from the `fire_event` call that triggered the activator becoming active. Rules are used to determine if a promotion meets certain criteria in order to be applicable. (In Spree 2.1 and prior, you need to explicitly associate a Promotion to an event like spree.order.contents_changed, spree.order.contents_changed, spree.checkout.coupon_code_added, etc. As of Spree 2.2, this is no longer necessary and the event_name column has been dropped.)
+
+In some special cases where a promotion has a `code` or a `path` configured for it, the promotion will only be activated if the payload's code or path match the promotion's. The `code` attribute is used for promotion codes, where a user must enter a code to receive the promotion, and the `path` attribute is used to apply a promotion once a user has visited a specific path.
+
+!!!
+Path-based promotions will only work when the `Spree::PromotionHandler::Page` class is used, as in `Spree::ContentController` from `spree_frontend`.
+!!!
+
+A promotion may also have a `usage_limit` attribute set, which restricts how many times the promotion can be used.
+
+## Actions
+
+There are four actions that come with spree:
+
+* An order-level adjustment
+* An item-level adjustment
+* Create line items
+* Free shipping
+
+### Creating an Adjustment
+
+When a `CreateAdjustment` action is undertaken, an adjustment is automatically applied to the order, unless the promotion has already applied an adjustment to the order.
+
+Once the adjustment has been applied to the order, its eligibility is re-checked every time the order is saved, by way of the `Promotion#eligible?` method, which uses `Promotion#eligible_rules` to determine if the promotion is still eligible based on its rules. For how this process works, please see the [rules section](#rules) below.
+
+An adjustment to an order from a promotion depends on the calculators. For more information about calculators, please see the [Calculators guide](calculators).
+
+### Creating an item adjustment
+
+When a `CreateItemAdjustments` action is undertaken, an adjustment is automatically applied to each item within the order, unless the action has already been performed on that line item
+
+The eligibility of the item for this promotion is re-checked whenever the item is updated. Its eligibility is based off the rules of the promotion.
+
+An adjustment to an order from a promotion depends on the calculators. For more information about calculators, please see the [Calculators guide](calculators).
+
+!!!
+The Spree::Promotion::Actions::CreateItemAdjustments object has a specific bloat issue in Spree 2.2 and will not scale on larger stores. Spree 2.3 fixes the root cause of the problem. For this reason, you may want to upgrade to Spree 2.3 before using this promotion action.
+!!!
+
+### Free Shipping
+
+When a `FreeShipping` action is undertaken, all shipments within the order have their prices negated. Just like with prior actions, the eligibility of this promotion is checked again whenever a shipment changes.
+
+### Create Line Items
+
+When a `CreateLineItem` action is undertaken, a series of line items are automatically added to the order, which may alter the order's price. The promotion with an action to add a line item can also have another action to add an adjustment to the order to nullify the cost of adding the product to the order.
+
+
+
+### Registering a New Action
+
+You can create a new action for Spree's promotion engine by inheriting from `Spree::PromotionAction`, like this:
+
+```ruby
+class MyPromotionAction < Spree::PromotionAction
+ def perform(options={})
+ ...
+ end
+end
+```
+
+***
+You can access promotion information using the `promotion` method within any `Spree::PromotionAction`.
+***
+
+This action must then be registered with Spree, which can be done by adding this code to `config/initializers/spree.rb`:
+
+```ruby
+Rails.application.config.spree.promotions.actions << MyPromotionAction
+```
+
+Once this has been registered, it will be available within Spree's interface. To provide translations for the interface, you will need to define them within your locale file. For instance, to define English translations for your new promotion action, use this code within `config/locales/en.yml`:
+
+```yaml
+en:
+ spree:
+ promotion_action_types:
+ my_promotion_action:
+ name: My Promotion Action
+ description: Performs my promotion action.
+```
+
+## Rules
+
+There are five rules which come with Spree 2.2 and Spree 2.3:
+
+* `FirstOrder`: The user's order is their first.
+* `ItemTotal`: The order's total is greater than (or equal to) a given value.
+* `Product`: An order contains a specific product.
+* `User` The order is by a specific user.
+* `UserLoggedIn`: The user is logged in.
+
+Spree 2.4 adds an two more rules in addition to the five listed above:
+* `One Use Per User`: Can be used only once per customer.
+* `Taxon(s)`: Order includes product(s) with taxons that you associate to this rule.
+
+Rules are used by Spree to determine if a promotion is applicable to an order and can be matched in one of two ways: all of the rules must match, or one rule must match. This is determined by the `match_policy` attribute on the `Promotion` object. As you will see in the Admin, you can set the match_policy to be "any" or "all" of the rules associated with the Promotion. When set to "any" the Promotion will be considered eligible if any one of the rules applies, when set to "all" it will be eligible only if all the rules apply.
+
+
+
+### Registering a New Rule
+
+As a developer, you can create and register a new rule for your Spree app with custom business logic specific to your needs. First define a class that inherits from `Spree::PromotionRule`, like this:
+
+```ruby
+module Spree
+ class Promotion
+ module Rules
+ class MyPromotionRule < Spree::PromotionRule
+ def applicable?(promotable)
+ promotable.is_a?(Spree::Order)
+ end
+
+ def eligible?(order, options = {})
+ ...
+ end
+
+ def actionable?(line_item)
+ ...
+ end
+ end
+ end
+ end
+end
+```
+
+The `eligible?` method should then return `true` or `false` to indicate if the promotion should be eligible for an order. You can retrieve promotion information by calling `promotion`.
+
+If your promotion supports some giving discounts on some line items, but not others, you should define `actionable?` to return true if the specified line item meets the criteria for promotion. It should return `true` or `false` to indicate if this line item can have a line item adjustment carried out on it.
+
+For example, if you are giving a promotion on specific products only, `eligible?` should return true if the order contains one of the products eligible for promotion, and `actionable?` should return true when the line item specified is one of the specific products for this promotion.
+
+
+You can then register the promotion using this code inside `config/initializers/spree.rb`:
+
+```ruby
+Rails.application.config.spree.promotions.rules << Spree::Promotion::Rules::MyPromotionRule
+```
+
+NOTE: proper location and file name for the rule in this example would be: `app/models/spree/promotion/rules/my_promotion_rule.rb`
+
+To get your rule to appear in the admin promotions interface you have a few more changes to make.
+
+Create a partial for your new rule in `app/views/spree/admin/promotions/rules/_my_promotion_rule.html.erb`.
+
+This file can be as simple as an empty file if your rule requires no parameters, or it may require more complex markup to enable setting values for your new rule. Check out some of the rule partials provided with Spree in the backend sources.
+
+And finally, your new rule must have a name and description defined for the locale you will be using it in. For English, edit `config/locales/en.yml` and add the following to support our new example rule:
+
+```yml
+en:
+ spree:
+ promotion_rule_types:
+ my_promotion_rule:
+ name: "My Promotion Rule"
+ description: "Rule to define my new promotion"
+```
+
+Restart your application. Once this rule has been registered, it will be available within Spree's admin interface.
+
+$$$
+Write about Spree::Promo::CouponApplicator
+$$$
diff --git a/guides/src/content/developer/core/shipments.md b/guides/src/content/developer/core/shipments.md
new file mode 100644
index 00000000000..8d097f8f505
--- /dev/null
+++ b/guides/src/content/developer/core/shipments.md
@@ -0,0 +1,513 @@
+---
+title: "Shipments"
+section: "core"
+---
+
+## Overview
+
+This guide explains how Spree represents shipping options and how it calculates expected costs, and shows how you can configure the system with your own shipping methods. After reading it you should know:
+
+* how shipments and shipping are implemented in Spree
+* how to specify your shipping structure
+* how split shipments work
+* how to configure products for special shipping treatment
+* how to capture shipping instructions
+
+Spree uses a very flexible and effective system to calculate shipping, accommodating the full range of shipment pricing: from simple flat rate to complex product-type- and weight-dependent calculations.
+
+The Shipment model is used to track how items are delivered to the buyer.
+
+Shipments have the following attributes:
+
+* `number`: The unique identifier for this shipment. It begins with the letter H and ends in an 11-digit number. This number is shown to the users, and can be used to find the order by calling `Spree::Shipment.find_by(number: number)`.
+* `tracking`: The identifier given for the shipping provider (i.e. FedEx, UPS, etc).
+* `shipped_at`: The time when the shipment was shipped.
+* `state`: The current state of the shipment.
+* `stock_location_id`: The ID of the Stock Location where the items for this shipment will be sourced from.
+
+A shipment can go through many different states, as illustrated below.
+
+
+
+An explanation of the different states:
+
+* `pending`: The shipment has backordered inventory units and/or the order is not paid for.
+* `ready`: The shipment has *no* backordered inventory units and the order is paid for.
+* `shipped`: The shipment is on its way to the buyer.
+* `canceled`: When an order is cancelled, all of its shipments will also be cancelled. When this happens, all items in the shipment will be restocked. If an order is "resumed", then the shipment will also be resumed.
+
+Explaining each piece of the shipment world inside of Spree separately and how each piece fits together can be a cumbersome task. Fortunately, using a few simple examples makes it much easier to grasp. In that spirit, the examples are shown first in this guide.
+
+## Examples
+
+### Simple Setup
+
+Consider you sell T-shirts to the US and Europe and ship from a single location, and you work with 2 deliverers:
+
+* USPS Ground (to US)
+* FedEx (to EU)
+
+and their pricing is as follow:
+
+* USPS charges $5 for one T-shirt and $2 for each additional one
+* FedEx charges $10 each, regardless of the quantity
+
+To achieve this setup you need the following configuration:
+
+* Shipping Categories: All your products are the same, so you only need to define one default shipping category. Each of your products would then need to be assigned to this shipping category.
+* 1 Stock Location: You are shipping all items from the same location, so you can use the default.
+* 2 Shipping Methods (Configuration->Shipping Methods) as follows:
+
+|Name|Zone|Calculator|
+|---:|---:|---:|
+|USPS Ground|US|Flexi Rate($5,$2)|
+|FedEx|EU_VAT|FlatRate-per-item($10)|
+
+### Advanced Setup
+
+Consider you sell products to a single zone (US) and you ship from 2 locations (Stock Locations):
+
+* New York
+* Los Angeles
+
+and you work with 3 deliverers (Shipping Methods):
+
+* FedEx
+* DHL
+* US postal service
+
+and your products can be classified into 3 Shipping Categories:
+
+* Light
+* Regular
+* Heavy
+
+and their pricing is as follow:
+
+FedEx charges:
+
+* $10 for all light items regardless of how many you have
+* $2 per regular item
+* $20 for the first heavy item and $15 for each additional one
+
+DHL charges:
+
+* $5 per item if it's light or regular
+* $50 per item if it's heavy
+
+USPS charges:
+
+* $8 per item if it's light or regular
+* $20 per item if it's heavy
+
+To achieve this setup you need the following configuration:
+
+* 4 Shipping Categories: Default, Light, Regular and Heavy
+* 3 Shipping Methods (Configuration->Shipping Methods): FedEx, DHL, USPS
+* 2 Stock Locations (Configuration->Stock Locations): New York, Los Angeles
+
+|S. Category / S. Method|DHL|FedEx|USPS|
+|---:|---:|---:|---:|
+|Light|Per Item ($5)|Flat Rate ($10)|Per Item ($8)|
+|Regular|Per Item ($5)|Per Item ($2)|Per Item ($8)|
+|Heavy|Per Item ($50)|Flexi Rate($20,$15)|Per Item ($20)|
+
+## Design & Functionality
+
+To properly leverage Spree's shipping system's flexibility you must understand a few key concepts:
+
+* Shipping Methods
+* Zones
+* Shipping Categories
+* Calculators (through Shipping Rates)
+
+### Shipping Methods
+
+Shipping methods are the actual services used to send the product. For example:
+
+* UPS Ground
+* UPS One Day
+* FedEx 2Day
+* FedEx Overnight
+* DHL International
+
+Each shipping method is only applicable to a specific `Zone`. For example, you wouldn't be able to get a package delivered internationally using a domestic-only shipping method. You can't ship from Dallas, USA to Rio de Janeiro, Brazil using UPS Ground (a US-only carrier).
+
+If you are using shipping categories, these can be used to qualify or disqualify a given shipping method.
+
+***
+**Note**: Shipping methods can now have multiple shipping categories assigned to them. This allows the shipping methods available to an order to be determined by the shipping categories of the items in a shipment.
+***
+
+### Zones
+
+Zones serve as a mechanism for grouping geographic areas together into a single entity. You can read all about how to configure and use Zones in the [Zones Guide](addresses#zones).
+
+The Shipping Address entered during checkout will define the zone the customer is in and limit the Shipping Methods available to him.
+
+### Shipping Categories
+
+Shipping Categories are useful if you sell products whose shipping pricing vary depending on the type of product (TVs and Mugs, for instance).
+
+***
+For simple setups, where shipping for all products is priced the same (ie. T-shirt-only shop), all products would be assigned to the default shipping category for the store.
+***
+
+Some examples of Shipping Categories would be:
+
+* Light (for lightweight items like stickers)
+* Regular
+* Heavy (for items over a certain weight)
+
+Shipping Categories are created in the admin interface ("Configuration" -> "Shipping Categories") and then assigned to products ("Products" -> "Edit").
+
+$$$
+Follow up: on a clean install + seed data I ended up with two Shipping Categories - "Default Shipping" and "Default"
+$$$
+
+During checkout, the shipping categories of the products in your order will determine which calculator will be used to price its shipping for each Shipping Method.
+
+### Calculators
+
+A Calculator is the component responsible for calculating the shipping price for each available Shipping Method.
+
+Spree ships with 5 default Calculators:
+
+* Flat rate (per order)
+* Flat rate (per item/product)
+* Flat percent
+* Flexible rate
+* Price sack
+
+Flexible rate is defined as a flat rate for the first product, plus a different flat rate for each additional product.
+
+You can define your own calculator if you have more complex needs. In that case, check out the [Calculators Guide](calculators).
+
+## UI
+
+### What the Customer Sees
+
+In the standard system, there is no mention of shipping until the checkout phase.
+
+After entering a shipping address, the system displays the available shipping options and their costs for each shipment in the order. Only the shipping options whose zones include the _shipping_ address are presented.
+
+The customer must choose a shipping method for each shipment before proceeding to the next stage. At the confirmation step, the shipping cost will be shown and included in the order's total.
+
+***
+You can enable collection of extra _shipping instructions_ by setting the option `Spree::Config.shipping_instructions` to `true`. This is set to `false` by default. See [Shipping Instructions](#shipping-instructions) below.
+***
+
+### What the Order's Administrator Sees
+
+`Shipment` objects are created during checkout for an order. Initially each records just the shipping method and the order it applies to. The administrator can update the record with the actual shipping cost and a tracking code, and may also (once only) confirm the dispatch. This confirmation causes a shipping date to be set as the time of confirmation.
+
+## Advanced Shipping Methods
+
+Spree comes with a set of calculators that should fit most of the shipping situations that may arise. If the calculators that come with Spree are not enough for your needs, you might want to use an extension - if one exists to meet your needs - or create a custom one.
+
+### Extensions
+
+There are a few Spree extensions which provide additional shipping methods, including special support for fees imposed by common carriers, or support for bulk orders. See the [Spree Extension Registry](https://github.com/spree-contrib) for the latest information.
+
+### Writing Your Own
+
+For more detailed information, check out the section on [Calculators](calculators).
+
+Your calculator should accept an array of `LineItem` objects and return a cost. It can look at any reachable data, but typically uses the address, the order and the information from variants which are contained in the line_items.
+
+## Product Configuration
+
+Store administrators can assign products to specific ShippingCategories or include extra information in variants to enable the calculator to determine results.
+
+Each product has a `ShippingCategory`, which adds product-specific information to the calculations beyond the standard information from the shipment. Standard information includes:
+
+* Destination address
+* Variants and quantities
+* Weight and dimension information, if given, for a variant
+
+`ShippingCategory` is basically a wrapper for a string. One use is to code up specific rates, eg. "Fixed $20" or "Fixed $40", from which a calculator could extract imposed prices (and not go through its other calculations).
+
+### Variant Configuration
+
+Variants can be specified with weight and dimension information. Some shipping method calculators will use this information if it is present.
+
+## Shipping Instructions
+
+The option `Spree::Config[:shipping_instructions]` controls collection of additional shipping instructions. This is turned off (set to `false`) by default. If an order has any shipping instructions attached, they will be shown in an order's shipment admin page and can also be edited at that stage. Observe that instructions are currently attached to the _order_ and not to actual _shipments_.
+
+## The Active Shipping Extension
+
+The popular `spree_active_shipping` extension harnesses the `active_shipping` gem to interface with carrier APIs such as USPS, Fedex and UPS, ultimately providing Spree-compatible calculators for the different delivery services of those carriers.
+
+To install the `spree-active-shipping` extension add the following to your `Gemfile`:
+
+```ruby
+gem 'spree_active_shipping'
+gem 'active_shipping', git: 'git://github.com/Shopify/active_shipping.git'
+```
+
+and run `bundle install` from the command line.
+
+As an example of how to use the [spree_active_shipping extension](https://github.com/spree/spree_active_shipping) we'll demonstrate how to configure it to work with the USPS API. The other carriers follow a very similar pattern.
+
+For each USPS delivery service you want to offer (e.g. "USPS Media Mail"), you will need to create a `ShippingMethod` with a descriptive name ("Configuration" -> "Shipping Methods") and a `Calculator` (registered in the `active_shipping` extension) that ties the delivery service and the shipping method together.
+
+### Default Calculators
+
+The `spree_active_shipping` extension comes with several pre-configured calculators out of the box. For example, here are the ones provided for the USPS carrier:
+
+```ruby
+def activate
+ [
+ #... calculators for Fedex and UPS not shown ...
+ Calculator::Usps::MediaMail,
+ Calculator::Usps::ExpressMail,
+ Calculator::Usps::PriorityMail,
+ Calculator::Usps::PriorityMailSmallFlatRateBox,
+ Calculator::Usps::PriorityMailRegularMediumFlatRateBoxes,
+ Calculator::Usps::PriorityMailLargeFlatRateBox
+ ].each(&:register)
+end
+```
+
+Each USPS delivery service you want to make available at checkout has to be associated with a corresponding shipping method. Which shipping methods are made available at checkout is ultimately determined by the zone of the customer's shipping address. The USPS' basic shipping categories are domestic and international, so we'll set up zones to mimic this distinction. We need to set up two zones then - a domestic one, consisting of the USA and its territories; and an international one, consisting of all other countries.
+
+With zones in place, we can now start adding some shipping methods through the admin panel. The only other essential requirement to calculate the shipping total at checkout is that each product and variant be assigned a weight.
+
+The `spree_active_shipping` gem needs some configuration variables set in order to consume the carrier web services.
+
+```ruby
+ # these can be set in an initializer in your site extension
+ Spree::ActiveShipping::Config.set(usps_login: "YOUR_USPS_LOGIN")
+ Spree::ActiveShipping::Config.set(fedex_login: "YOUR_FEDEX_LOGIN")
+ Spree::ActiveShipping::Config.set(fedex_password: "YOUR_FEDEX_PASSWORD")
+ Spree::ActiveShipping::Config.set(fedex_account: "YOUR_FEDEX_ACCOUNT")
+ Spree::ActiveShipping::Config.set(fedex_key: "YOUR_FEDEX_KEY")
+```
+
+### Adding Additional Calculators
+
+Additional delivery services that are not pre-configured as a calculator in the `spree_active_shipping` extension can be easily added. Say, for example, you need First Class International Parcels via the US Postal Service.
+
+First, create a calculator class that inherits from `Calculator::Usps::Base` and implements a description class method:
+
+```ruby
+class Calculator::Usps::FirstClassMailInternationalParcels < Calculator::Usps::Base
+ def self.description
+ "USPS First-Class Mail International Package"
+ end
+end
+```
+
+!!!
+Note that, unlike calculators that you write yourself, these calculators do not have to implement a `compute` instance method that returns a shipping amount. The superclasses take care of that requirement.
+!!!
+
+There is one gotcha to bear in mind: the string returned by the `description` method must _exactly_ match the name of the USPS delivery service. To determine the exact spelling of the delivery service, you'll need to examine what gets returned from the API:
+
+```ruby
+class Calculator::ActiveShipping < Calculator
+ def compute(line_items)
+ #....
+ rates = retrieve_rates(origin, destination, packages(order))
+ # the key of this hash is the name you need to match
+ # raise rates.inspect
+
+ return nil unless rates
+ rate = rates[self.description].to_f + (Spree::ActiveShipping::Config[:handling_fee].to_f || 0.0)
+ return nil unless rate
+ # divide by 100 since active_shipping rates are expressed as cents
+
+ return rate/100.0
+ end
+
+ def retrieve_rates(origin, destination, packages)
+ #....
+ # carrier is an instance of ActiveMerchant::Shipping::USPS
+ response = carrier.find_rates(origin, destination, packages)
+ # turn this beastly array into a nice little hash
+ h = Hash[*response.rates.collect { |rate| [rate.service_name, rate.price] }.flatten]
+ #....
+ end
+end
+```
+
+As you can see in the code above, the `spree_active_shipping` gem returns an array of services with their corresponding prices, which the `retrieve_rates` method converts into a hash. Below is what would get returned for an order with an international destination:
+
+```ruby
+{
+ "USPS Priority Mail International Flat Rate Envelope"=>1345,
+ "USPS First-Class Mail International Large Envelope"=>376,
+ "USPS USPS GXG Envelopes"=>4295,
+ "USPS Express Mail International Flat Rate Envelope"=>2895,
+ "USPS First-Class Mail International Package"=>396,
+ "USPS Priority Mail International Medium Flat Rate Box"=>4345,
+ "USPS Priority Mail International"=>2800,
+ "USPS Priority Mail International Large Flat Rate Box"=>5595,
+ "USPS Global Express Guaranteed Non-Document Non-Rectangular"=>4295,
+ "USPS Global Express Guaranteed Non-Document Rectangular"=>4295,
+ "USPS Global Express Guaranteed (GXG)"=>4295,
+ "USPS Express Mail International"=>2895,
+ "USPS Priority Mail International Small Flat Rate Box"=>1345
+}
+```
+
+From all of the viable shipping services in this hash, the `compute` method selects the one that matches the description of the calculator. At this point, an optional flat handling fee (set via preferences) can be added:
+
+```ruby
+rate = rates[self.description].to_f + (Spree::ActiveShipping::Config[:handling_fee].to_f || 0.0)
+```
+
+Finally, don't forget to register the calculator you added. In extensions, this is accomplished with the `activate` method:
+
+```ruby
+def activate
+ Calculator::Usps::FirstClassMailInternationalParcels.register
+end
+```
+
+## Filtering Shipping Methods On Criteria Other Than the Zone
+
+Ordinarily, it is the zone of the shipping address that determines which shipping methods are displayed to a customer at checkout. Here is how the availability of a shipping method is determined:
+
+```ruby
+class Spree::Stock::Estimator
+ def shipping_methods(package)
+ shipping_methods = package.shipping_methods
+ shipping_methods.delete_if { |ship_method| !ship_method.calculator.available?(package.contents) }
+ shipping_methods.delete_if { |ship_method| !ship_method.include?(order.ship_address) }
+ shipping_methods.delete_if { |ship_method| !(ship_method.calculator.preferences[:currency].nil? || ship_method.calculator.preferences[:currency] == currency) }
+ shipping_methods
+ end
+end
+```
+
+Unless overridden, the calculator's `available?` method returns `true` by default. It is, therefore, the zone of the destination address that filters out the shipping methods in most cases. However, in some circumstances it may be necessary to filter out additional shipping methods.
+
+Consider the case of the USPS First Class domestic shipping service, which is not offered if the weight of the package is greater than 13oz. Even though the USPS API does not return the option for First Class in this instance, First Class will appear as an option in the checkout view with an unfortunate value of 0, since it has been set as a Shipping Method.
+
+To ensure that First Class shipping is not available for orders that weigh more than 13oz, the calculator's `available?` method must be overridden as follows:
+
+```ruby
+class Calculator::Usps::FirstClassMailParcels < Calculator::Usps::Base
+ def self.description
+ "USPS First-Class Mail Parcel"
+ end
+
+ def available?(order)
+ multiplier = 1.3
+ weight = order.line_items.inject(0) do |weight, line_item|
+ weight + (line_item.variant.weight ? (line_item.quantity * line_item.variant.weight * multiplier) : 0)
+ end
+ #if weight in ounces > 13, then First Class Mail is not available for the order
+ weight > 13 ? false : true
+ end
+end
+```
+
+## Split Shipments
+
+### Introduction
+
+Split shipments are a new feature as of Spree 2.0 that addresses the needs of complex Spree stores that require sophisticated shipping and warehouse logic. This includes detailed inventory management and allows for shipping from multiple locations.
+
+
+
+### Creating Proposed Shipments
+
+This section steps through the basics of what is involved in determining shipments for an order. There are a lot of pieces that make up this process. They are explained in detail in the [Components of Split Shipments](#components-of-split-shipments) section of this guide.
+
+The process of determining shipments for an order is triggered by calling `create_proposed_shipments` on an `Order` object while transitioning to the `delivery` state during checkout. This process will first delete any existing shipments for an order and then determine the possible shipments available for that order.
+
+`create_proposed_shipments` will initially call `Spree::Stock::Coordinator.new(@order).packages`. This will return an array of packages. In order to determine which items belong in which package when they are being built, Spree uses an object called a `Splitter`, described in more detail [below](#the-packer).
+
+After obtaining the array of available packages, they are converted to shipments on the order object. Shipping rates are determined and inventory units are created during this process as well.
+
+At this point, the checkout process can continue to the delivery step.
+
+## Components of Split Shipments
+
+This section describes the four main components that power split shipments: [The Coordinator](#the-coordinator), [The Packer](#the-packer), [The Prioritizer](#the-prioritizer), and [The Estimator](#the-estimator).
+
+### The Coordinator
+
+The `Spree::Stock::Coordinator` is the starting point for determining shipments when calling `create_proposed_shipments` on an order. Its job is to go through each `StockLocation` available and determine what can be shipped from that location.
+
+The `Spree::Stock::Coordinator` will ultimately return an array of packages which can then be easily converted into shipments for an order by calling `to_shipment` on them.
+
+### The Packer
+
+A `Spree::Stock::Packer` object is an important part of the `create_proposed_shipments` process. Its job is to determine possible packages for a given StockLocation and order. It uses rules defined in classes known as `Splitters` to determine what packages can be shipped from a `StockLocation`.
+
+For example, we may have two splitters for a stock location. One splitter has a rule that any order weighing more than 50lbs should be shipped in a separate package from items weighing less. Our other splitter is a catch-all for any item weighing less than 50lbs. So, given one item in an order weighing 60lbs and two items weighing less, the Packer would use the rules defined in our splitters to come up with two separate packages: one containing the single 60lb item, the other containing our other two items.
+
+#### Default Splitters
+
+Spree comes with two default splitters which are run in sequence. This means that the first splitter takes the packages array from the order, and each subsequent splitter takes the output of the splitter that came before it.
+
+Let's take a look at what the default splitters do:
+
+* **Shipping Category Splitter**: Splits an order into packages based on items' shipping categories. This means that each package will only have items that all belong to the same shipping category.
+* **Weight Splitter**: Splits an order into packages based on a weight threshold. This means that each package has a mass weight. If a new item is added to the order and it causes a package to go over the weight threshold, a new package will be created so that all packages weigh less than the threshold. You can set the weight threshold by changing `Spree::Stock::Splitter::Weight.threshold` (defaults to `150`) in an initializer.
+
+#### Custom Splitters
+
+Note that splitters can be customized, and creating your own can be done with relative ease. By inheriting from `Spree::Stock::Splitter::Base`, you can create your own splitter.
+
+For an example of a simple splitter, take a look at Spree's [weight based splitter](https://github.com/spree/spree/blob/235e470b242225d7c75c7c4c4c033ee3d739bb36/core/app/models/spree/stock/splitter/weight.rb). This splitter pulls items with a weight greater than 150 into their own shipment.
+
+After creating your splitter, you need to add it to the array of splitters Spree
+uses. To do this, add the following to your application's spree initializer
+`spree.rb` file:
+
+```ruby
+Rails.application.config.spree.stock_splitters << Spree::Stock::Splitter::CustomSplitter
+```
+
+You can also completely override the splitters used in Spree, rearrange them, etc.
+To do this, add the following to your `spree.rb` file:
+
+```ruby
+Rails.application.config.spree.stock_splitters = [
+ Spree::Stock::Splitter::CustomSplitter,
+ Spree::Stock::Splitter::ShippingCategory
+]
+```
+
+Or if you don't want to split packages just set the option above to an empty
+array. e.g. a store with the following configuration in spree.rb won't have any
+package splitted.
+
+```ruby
+Rails.application.config.spree.stock_splitters = []
+```
+
+If you want to add different splitters for each `StockLocation`, you need to decorate the `Spree::Stock::Coordinator` class and override the `splitters` method.
+
+### The Prioritizer
+
+A `Spree::Stock::Prioritizer` object will decide which `StockLocation` should ship which package from an order. The prioritizer will attempt to come up with the best shipping situation available to the user.
+
+By default, the prioritizer will first select packages where the items are on hand. Then it will try to find packages where items are backordered. During this process, the `Spree::Stock::Adjuster` is also used to ensure each package has the correct number of items.
+
+The prioritizer is also a customization point. If you want to customize which packages should take priority for the order during this process, you can override the `sort_packages` method in `Spree::Stock::Prioritizer`.
+
+#### Customizing the Adjuster
+
+The `Adjuster` visits each package in an order and ensures the correct number of items are in each package. To customize this functionality, you need to do two things:
+
+* Subclass the [Spree::Stock::Adjuster](https://github.com/spree/spree/blob/a55db75bbebc40f5705fc3010d1e5a2190bde79b/core/app/models/spree/stock/adjuster.rb) class and override the the `adjust` method to get the desired functionality.
+* Decorate the `Spree::Stock::Coordinator` and override the `prioritize_packages` method, passing in your custom adjuster class to the `Prioritizer` initializer. For example, if our adjuster was called `Spree::Stock::CustomAdjuster`, we would do the following:
+
+```ruby
+Spree::Stock::Coordinator.class_eval do
+ def prioritize_packages(packages)
+ prioritizer = Prioritizer.new(order, packages, Spree::Stock::CustomAdjuster)
+ prioritizer.prioritized_packages
+ end
+end
+```
+
+### The Estimator
+
+The `Spree::Stock::Estimator` loops through the packages created by the packer in order to calculate and attach shipping rates to them. This information is then returned to the user so they can select shipments for their order and complete the checkout process.
diff --git a/guides/src/content/developer/core/taxation.md b/guides/src/content/developer/core/taxation.md
new file mode 100644
index 00000000000..cd71a2c4462
--- /dev/null
+++ b/guides/src/content/developer/core/taxation.md
@@ -0,0 +1,153 @@
+---
+title: Taxation
+section: core
+---
+
+## Overview
+
+Spree represents taxes for an order by using `tax_categories` and `tax_rates`.
+
+Products within Spree can be linked to Tax Categories, which are then used to influence the taxation rate for the products when they are purchased. One Tax Category can be set to being the default for the entire system, which means that if a product doesn't have a related tax category, then this default tax category would be used.
+
+A `tax_category` can have many `tax_rates`, which indicate the rate at which the products belonging to a specific tax category will be taxed at. A tax rate links a tax rate to a particular zone (see [Addresses](addresses) for more information about zones). When an order is placed in a specific zone, any of the products for that order which have a tax zone that matches the order's tax zone will be taxed.
+
+The standard sales tax policies commonly found in the USA can be modeled as well as Value Added Tax (VAT) which is commonly used in Europe. These are not the only types of tax rules that you can model in Spree. Once you obtain a sufficient understanding of the basic concepts you should be able to model the tax rules of your country or jurisdiction.
+
+***
+Taxation within the United States can get exceptionally complex, with different states, counties and even cities having different taxation rates. If you are shipping interstate within the United States, we would strongly advise you to use the [Spree Tax Cloud](https://github.com/spree-contrib/spree_tax_cloud) extension so that you get correct tax rates.
+***
+
+## Tax Categories
+
+The Spree default is to treat everything as exempt from tax. In order for a product to be considered taxable, it must belong to a tax category. The tax category is a special concept that is specific to taxation. The tax category is normally never seen by the user so you could call it something generic like "Taxable Goods." If you wish to tax certain products at different rates, however, then you will want to choose something more descriptive (ex. "Clothing.").
+
+***
+It can be somewhat tedious to set the tax category for every product. We're currently exploring ways to make this simpler. If you are importing inventory from another source you will likely be writing your own custom Ruby program that automates this process.
+***
+
+## Tax Rates
+
+A tax rate is essentially a percentage amount charged based on the sales price. Tax rates also contain other important information.
+
+* Whether product prices are inclusive of this tax
+* The zone in which the order address must fall within
+* The tax category that a product must belong to in order to be considered taxable.
+
+Spree will calculate tax based on the best matching zone for the order. It's also possible to have more than one applicable tax rate for a single zone. In order for a tax rate to apply to a particular product, that product must have a tax category that matches the tax category of the tax rate.
+
+## Basic Examples
+
+Let's say you need to charge 5% tax for all items that ship to New York and 6% on only clothing items that ship to Pennsylvania. This will mean you need to construct two different zones: one zone containing just the state of New York and another zone consisting of the single state of Pennsylvania.
+
+Here's another hypothetical scenario. You would like to charge 10% tax on all electronic items and 5% tax on everything else. This tax should apply to all countries in the European Union (EU). In this case you would construct just a single zone consisting of all the countries in the EU. The fact that you want to charge two different rates depending on the type of good does not mean you need two zones.
+
+***
+Please see the [Addresses guide](addresses) for more information on constructing a zone.
+***
+
+## Default Tax Zone
+
+Spree also has the concept of a default tax zone. When a user is adding items to their cart we do not yet know where the order will be shipped to, and so it's assumed that the cart is within the default tax zone until later. In some cases we may want to estimate the tax for the order by assuming the order falls within a particular tax zone.
+
+Why might we want to do this? The primary use case for this is for countries where there is already tax included in the price. In the EU, for example, most products have a Value Added Tax (VAT) included in the price. There are cases where it may be desirable to show the portion of the product price that includes tax. In order to calculate this tax amount we need to know the zone (and corresponding Tax Rate) that was assumed in the price.
+
+We may also reduce the order total by the tax amount if the order is being shipped outside the tax jurisdiction. Again, this requires us to know the zone assumed in making the original tax calculation so that the tax amount can be backed out.
+
+## Shipping vs. Billing Address
+
+Most tax jurisdictions base the tax on the shipping address of where the order is being shipped to. So in these cases the shipping address is used when determining the tax zone. Spree does, however, allow you to use the billing address to determine the zone.
+
+To determine tax based on billing address instead of shipping address you will need to set the `Spree::Config[:tax_using_ship_address]` preference to `false`.
+
+***
+`Zone.match` is a method used to determine the most applicable zone for taxation. In the case of multiple matches, the closer match will be used, with State zone matches having priority over Country zone matches.
+***
+
+## Calculators
+
+In order to charge tax in Spree you also need a `Spree::Calculator`. In most cases you should be able to use Spree's `DefaultTax` calculator. It is suitable for both sales tax and price-inclusive tax scenarios. For more information, please read the [Calculators guide](calculators).
+
+***
+The `DefaultTax` calculator uses the item total (exclusive of shipping) when computing sales tax.
+***
+
+## Tax Types
+
+There are two basic types of tax that a store owner might need to contend with. In the United States (and some other countries) store owners sometimes need to charge what is known as "sales tax." In the European Union (EU) and other countries stores owners need to deal with "tax inclusive" pricing which is often called Value Added Tax (VAT).
+
+***
+Most taxes can be considered one of these two types. For instance, in Australia customers pay a Goods and Services Tax (GST). This is basically equivalent to VAT in Europe.
+***
+
+In some cases you may need to charge one type of tax for orders falling within one zone and another type of tax for orders falling within a different zone. There are even some rare situations where you may need to charge both types of tax in the same zone. Spree supports all of these scenarios.
+
+### Sales Tax
+
+Sales tax is the default tax type for any tax rate in Spree.
+
+Let's take an example of a sales tax situation for the United States. Imagine that we have a zone that covers all of North America and that the zone is used for a tax rate which applies a 5% tax on products with the tax category of "Clothing".
+
+If the customer purchases a single clothing item for $17.99 and they live in the United States (which is within the North America zone we defined) they are eligible to pay sales tax.
+
+The sales tax calculation is $17.99 x 5% for a total tax of $0.8995, which is rounded up to two decimal places, to $0.90. This tax amount is then applied to the order as an adjustment.
+
+***
+See the [Adjustments Guide](adjustments) if you need more information on adjustments.
+***
+
+If the quantity of the item is changed to 2, then the tax amount doubles: ($17.99 x 2) x 0.05 is $1.799, which is again rounded up to two decimal places, applying a tax adjustment of $1.80.
+
+Let's now assume that we have another product that's a coffee mug, which doesn't have the "Clothing" tax category applied to it. Let's also assume this product costs $13.99, and there's no default tax category set up for the system. Under these circumstances, the coffee mug will not be taxed when it's added to the order.
+
+Finally, if the taxable address (either the shipping or billing, depending on the `Spree::Config[:tax_using_ship_address]` setting) is changed for the order to outside this taxable zone, then the tax adjustment on the order will be removed. If the address is changed back, the tax rate will be applied once more.
+
+### Tax Included
+
+Many jurisdictions have what is commonly referred to as a Value Added Tax (VAT.) In these cases the tax is typically applied to the price. This means that prices for items are "inclusive of tax" and no additional tax needs to be applied during checkout.
+
+In the case of tax inclusive pricing the store owner can enter all prices inclusive of tax if their home country is the default zone. If there is no default zone set, any taxes that are included in the price will be added to the net prices on the fly depending on the current order's tax zone.
+
+If the current order's tax zone is outside the default zone, prices will be shown and used with only the included taxes for that zone applied. If there is no VAT for that zone (for example when the current order's shipping address is outside the EU), the net price will be shown and used.
+
+***
+Keep in mind that each order records the price a customer paid (including the tax) as part of the line item record. This means you don't have to worry about changing prices or tax rates affecting older orders.
+***
+
+When tax is included in the price there is no order adjustment needed (unlike the sales tax case). Stores are, however, typically interested in showing the amount of tax the user paid. These totals are for informational purposes only and do not affect the order total.
+
+Let's start by looking at an example where there is a 5% included on all products and it's included in the price. We'll further assume that this tax should only apply to orders within the United Kingdom (UK).
+
+In the case where the order address is within the UK and we purchase a single clothing item for £17.99 we see an order total of £17.99. The tax rate adjustment applied is £17.99 x 5%, which is £0.8995, and that is rounded up to two decimal places, becoming £0.90.
+
+Now let's increase the quantity on the item from 1 to 2. The order total changes to £35.98 with a tax total of £1.799, which is again rounded up to now being £1.80.
+
+Next we'll add a different clothing item costing £19.99 to our order. Since both items are clothing and taxed at the same rate, they can be reduced to a single total, which means there's a single adjustment still applied to the order, calculated like this: (£17.99 + £19.99) x 0.05 = £1.899, rounded up to two decimal places: £1.90.
+
+Now let's assume an additional tax rate of 10% on a "Consumer Electronics" tax category. When we add a product with this tax category to our order with a price of £16.99, there will be a second adjustment added to the order, with a calculated total of £16.99 x 10%, which is £1.699. Rounded up, it's £1.70.
+
+Finally, if the order's address is changed to being outside this tax zone, then there will be two negative adjustments applied to remove these tax rates from the order.
+
+### Additional Examples
+
+
+#### Differing VATs for different product categories depending on the customer's zone
+
+As of January 1st, 2015, digital products sold within the EU must have the VAT of the receiving country applied. Physical products must have the seller's VAT applied. In order to set this up, please proceed as follows:
+
+1. Create zones for all EU countries and a zone for all EU countries except your home zone.
+
+2. Mark your home zone as the default zone so you can conveniently enter gross prices.
+
+3. Create a tax category "Digital products", and a tax category "Physical products".
+
+4. Add tax rates that are "included in tax" for the tax category "Physical goods" for all EU countries.
+
+5. Add two tax rates for the tax category "Physical products":
+ 1. One for your home country, with your home country's VAT
+ 2. One for the rest of the EU, also with your home country's VAT
+
+If you change the tax zone of the current order (by changing the relevant address), prices will now be shown and used including the correct VAT for the current order.
+
+!!!
+All of the examples in this guide are meant to be used for illustrative purposes. They are not meant to be used as definitive interpretations of tax law. You should consult your accountant or attorney for guidance on how much tax to collect and under what circumstances.
+!!!
diff --git a/guides/src/content/developer/customization/asset.md b/guides/src/content/developer/customization/asset.md
new file mode 100644
index 00000000000..db5f246925c
--- /dev/null
+++ b/guides/src/content/developer/customization/asset.md
@@ -0,0 +1,241 @@
+---
+title: "Asset Customization"
+section: customization
+---
+
+## Overview
+
+This guide covers how Spree manages its JavaScript, stylesheet and image
+assets and how you can extend and customize them including:
+
+- Understanding Spree's use of the Rails asset pipeline
+- Managing application specific assets
+- Managing extension specific assets
+- Overriding Spree's core assets
+
+## Spree's Asset Pipeline
+
+With the release of 3.1 Rails now supports powerful asset management
+features and Spree fully leverages these features to further extend and
+simplify its customization potential. Using asset customization
+techniques outlined below you be able to adapt all the JavaScript,
+stylesheets and images contained in Spree to easily provide a fully
+custom experience.
+
+All Spree generated (or upgraded) applications include an `app/assets`
+directory (as is standard for all Rails 3.1 apps). We've taken this one
+step further by subdividing each top level asset directory (images,
+JavaScript files, stylesheets) into frontend and backend directories. This is
+designed to keep assets from the frontend and backend from conflicting with each other.
+
+A typical assets directory for a Spree application will look like:
+
+ app
+ |-- assets
+ |-- images
+ | |-- spree
+ | |-- frontend
+ | |-- backend
+ |-- javascripts
+ | |-- spree
+ | |-- frontend
+ | | |-- all.js
+ | |-- backend
+ | |-- all.js
+ |-- stylesheets
+ | |-- spree
+ | |-- frontend
+ | | |-- all.css
+ | |-- backend
+ | |-- all.css
+
+Spree also generates four top level manifests (all.css & all.js, see
+above) that require all the core extension's and site specific
+stylesheets / JavaScript files.
+
+### How core extensions (engines) manage assets
+
+All core engines have been updated to provide four asset manifests that
+are responsible for bundling up all the JavaScript files and stylesheets
+required for that engine.
+
+For example, Spree provides the following manifests:
+
+ vendor
+ |-- assets
+ |-- javascripts
+ | |-- spree
+ | |-- frontend
+ | | |-- all.js
+ | |-- backend
+ | |-- all.js
+ |-- stylesheets
+ | |-- spree
+ | |-- frontend
+ | | |-- all.css
+ | |-- backend
+ | |-- all.css
+
+These manifests are included by default by the
+relevant all.css or all.js in the host Spree application. For example,
+`vendor/assets/javascripts/spree/backend/all.js` includes:
+
+```js
+//= require jquery
+//= require jquery_ujs
+
+//= require spree/backend
+
+//= require_tree .
+```
+
+External JavaScript libraries, stylesheets and images have also be
+relocated into vendor/assets (again Rails 3.1 standard approach), and
+all core extensions no longer have public directories.
+
+## Managing your application's assets
+
+Assets that customize your Spree store should go inside the appropriate
+directories inside `vendor/assets/images/spree`, `vendor/assets/javascripts/spree`,
+or `vendor/assets/stylesheets/spree`. This is done so that these assets do
+not interfere with other parts of your application.
+
+## Managing your extension's assets
+
+We're suggesting that all third party extensions should adopt the same
+approach as Spree and provide the same four (or less depending on
+what the extension requires) manifest files, using the same directory
+structure as outlined above.
+
+Third party extension manifest files will not be automatically included
+in the relevant all.(js|css) files so it's important to document the
+manual inclusion in your extensions installation instructions or provide
+a Rails generator to do so.
+
+For an example of an extension using a generator to install assets and
+migrations take a look at the [install_generator for spree_fancy](https://github.com/spree/spree_fancy/blob/master/lib/generators/spree_fancy/install/install_generator.rb).
+
+## Overriding Spree's core assets
+
+Overriding or replacing any of Spree's internal assets is even easier
+than before. It's recommended to attempt to replace as little as
+possible in a given JavaScript or stylesheet file to help ease future
+upgrade work required.
+
+The methods listed below will work for both applications, extensions and
+themes with one noticeable difference: Extension & theme asset files
+will not be automatically included (see above for instructions on how to
+include asset files from your extensions / themes).
+
+### Overriding individual CSS styles
+
+Say for example you want to replace the following CSS snippet:
+
+```css
+/* spree/app/assets/stylesheets/spree/frontend/screen.css */
+
+div#footer {
+ clear: both;
+}
+```
+
+You can now just create a new stylesheet inside
+`your_app/vendor/assets/stylesheets/spree/frontend/` and include the following CSS:
+
+```css
+/* vendor/assets/stylesheets/spree/frontend/foo.css */
+
+div#footer {
+ clear: none;
+ border: 1px solid red;
+}
+```
+
+The `frontend/all.css` manifest will automatically include `foo.css` and it
+will actually include both definitions with the one from `foo.css` being
+included last, hence it will be the rule applied.
+
+### Overriding entire CSS files
+
+To replace an entire stylesheet as provided by Spree you simply need to
+create a file with the same name and save it to the corresponding path
+within your application's or extension's `vendor/assets/stylesheets`
+directory.
+
+For example, to replace `spree/frontend/all.css` you would save the replacement
+to `your_app/vendor/assets/stylesheets/spree/frontend/all.css`.
+
+***
+This same method can also be used to override stylesheets provided by
+third-party extensions.
+***
+
+### Overriding individual JavaScript functions
+
+A similar approach can be used for JavaScript functions. For example, if
+you wanted to override the `show_variant_images` method:
+
+```javascript
+ // spree/app/assets/javascripts/spree/frontend/product.js
+
+var show_variant_images = function(variant_id) {
+ $('li.vtmb').hide();
+ $('li.vtmb-' + variant_id).show();
+ var currentThumb = $('#' +
+ $("#main-image").data('selectedThumbId'));
+
+ // if currently selected thumb does not belong to current variant,
+ // nor to common images,
+ // hide it and select the first available thumb instead.
+
+ if(!currentThumb.hasClass('vtmb-' + variant_id) &&
+ !currentThumb.hasClass('tmb-all')) {
+ var thumb = $($('ul.thumbnails li:visible').eq(0));
+ var newImg = thumb.find('a').attr('href');
+ $('ul.thumbnails li').removeClass('selected');
+ thumb.addClass('selected');
+ $('#main-image img').attr('src', newImg);
+ $("#main-image").data('selectedThumb', newImg);
+ $("#main-image").data('selectedThumbId', thumb.attr('id'));
+ }
+}
+```
+
+Again, just create a new JavaScript file inside
+`your_app/vendor/assets/javascripts/spree/frontend` and include the new method
+definition:
+
+```javascript
+ // your_app/vendor/assets/javascripts/spree/frontend/foo.js
+
+var show_variant_images = function(variant_id) {
+ alert('hello world');
+}
+```
+
+The resulting `frontend/all.js` would include both methods, with the latter
+being the one executed on request.
+
+### Overriding entire JavaScript files
+
+To replace an entire JavaScript file as provided by Spree you simply
+need to create a file with the same name and save it to the
+corresponding path within your application's or extension's
+`app/assets/javascripts` directory.
+
+For example, to replace `spree/frontend/all.js` you would save the replacement to
+`your_app/vendor/assets/javascripts/spree/frontend/all.js`.
+
+***
+This same method can be used to override JavaScript files provided
+by third-party extensions.
+***
+
+### Overriding images
+
+Finally, images can be replaced by substituting the required file into
+the same path within your application or extension as the file you would
+like to replace.
+
+For example, to replace the Spree logo you would simply copy your logo
+to: `your_app/vendor/assets/images/logo/spree_50.png`.
diff --git a/guides/src/content/developer/customization/authentication.md b/guides/src/content/developer/customization/authentication.md
new file mode 100644
index 00000000000..96338ce6dfb
--- /dev/null
+++ b/guides/src/content/developer/customization/authentication.md
@@ -0,0 +1,293 @@
+---
+title: "Custom Authentication"
+section: customization
+---
+
+## Overview
+
+This guide covers using a custom authentication setup with Spree, such
+as one provided by your own application. This is ideal in situations
+where you want to handle the sign-in or sign-up flow of your application
+uniquely, outside the realms of what would be possible with Spree. After
+reading this guide, you will be familiar with:
+
+- Setting up Spree to work with your custom authentication
+
+## Background
+
+Traditionally, applications that use Spree have needed to use the
+`Spree::User` model that came with the `spree_auth` component of Spree.
+With the advent of 1.2, this is no longer a restriction. The
+`spree_auth` component of Spree has been removed and is now purely
+opt-in. If you have an application that has used the `spree_auth`
+component in the past and you wish to continue doing so, you will need
+to add this extra line to your `Gemfile`:
+
+```ruby
+gem 'spree_auth_devise'
+```
+
+By having this authentication component outside of Spree, applications
+that wish to use their own authentication may do so, and applications
+that have previously used `spree_auth`'s functionality may continue
+doing so by using this gem.
+
+### The User Model
+
+This guide assumes that you have a pre-existing model inside your
+application that represents the users of your application already. This
+model could be provided by gems such as
+[Devise](https://github.com/plataformatec/devise) or
+[Sorcery](https://github.com/NoamB/sorcery). This guide also assumes
+that the application that this `User` model exists in is already a Spree
+application.
+
+This model **does not** need to be called `User`, but for the purposes
+of this guide the model we will be referring to **will** be called
+`User`. If your model is called something else, do some mental
+substitution wherever you see `User`.
+
+#### Initial Setup
+
+To begin using your custom `User` class, you must first edit Spree's
+initializer located at `config/initializers/spree.rb` by changing this
+line:
+
+```ruby
+Spree.user_class = "Spree::User"
+```
+
+To this:
+
+```ruby
+Spree.user_class = "User"
+```
+
+Next, you need to run the custom user generator for Spree which will
+create two files. The first is a migration that will add the necessary
+Spree fields to your users table, and the second is an extension that
+lives at `lib/spree/authentication_helpers.rb` to the
+`Spree::Core::AuthenticationHelpers` module inside of Spree.
+
+Run this generator with this command:
+
+```bash
+$ bundle exec rails g spree:custom_user User
+```
+
+This will tell the generator that you want to use the `User` class as
+the class that represents users in Spree. Run the new migration by
+running this:
+
+```bash
+$ bundle exec rake db:migrate
+```
+
+Next you will need to define some methods to tell Spree where to find
+your application's authentication routes.
+
+#### Authentication Helpers
+
+There are some authentication helpers of Spree's that you will need to
+possibly override. The file at `lib/spree/authentication_helpers.rb`
+contains the following code to help you do that:
+
+```ruby
+module Spree
+ module AuthenticationHelpers
+ def self.included(receiver)
+ receiver.send :helper_method, :spree_login_path
+ receiver.send :helper_method, :spree_signup_path
+ receiver.send :helper_method, :spree_logout_path
+ receiver.send :helper_method, :spree_current_user
+ end
+
+ def spree_current_user
+ current_user
+ end
+
+ def spree_login_path
+ main_app.login_path
+ end
+
+ def spree_signup_path
+ main_app.signup_path
+ end
+
+ def spree_logout_path
+ main_app.logout_path
+ end
+ end
+end
+```
+
+In your `ApplicationController` add those lines:
+
+```ruby
+include Spree::AuthenticationHelpers
+include Spree::Core::ControllerHelpers::Auth
+include Spree::Core::ControllerHelpers::Common
+include Spree::Core::ControllerHelpers::Order
+include Spree::Core::ControllerHelpers::Store
+helper 'spree/base'
+```
+
+Each of the methods defined in this module return values that are the
+most common in Rails applications today, but you may need to customize
+them. In order, they are:
+
+* `spree_current_user` Used to tell Spree what the current user
+of a request is.
+* `spree_login_path` The location of the login/sign in form in
+your application.
+* `spree_signup_path` The location of the sign up form in your
+application.
+* `spree_logout_path` The location of the logout feature of your
+application.
+
+***
+URLs inside the `spree_login_path`, `spree_signup_path` and
+`spree_logout_path` methods **must** have `main_app` prefixed if they
+are inside your application. This is because Spree will otherwise
+attempt to route to a `login_path`, `signup_path` or `logout_path`
+inside of itself, which does not exist. By prefixing with `main_app`,
+you tell it to look at the application's routes.
+***
+
+You will need to define the `login_path`, `signup_path` and
+`logout_path` routes yourself, by using code like this inside your
+application's `config/routes.rb` if you're using Devise:
+
+```ruby
+devise_for :users
+devise_scope :user do
+ get '/login', to: "devise/sessions#new"
+ get '/signup', to: "devise/registrations#new"
+ delete '/logout', to: "devise/sessions#destroy"
+end
+```
+
+Of course, this code will be different if you're not using Devise.
+Simply do not use the `devise_scope` method and change the controllers
+and actions for these routes.
+
+You can also customize the `spree_login_path`, `spree_signup_path`
+and `spree_logout_path` methods inside
+`lib/spree/authentication_helpers.rb` to use the routing helper methods
+already provided by the authentication setup you have, if you wish.
+
+***
+Any modifications made to `lib/spree/authentication_helpers.rb`
+while the server is running will require a restart, as wth any other
+modification to other files in `lib`.
+***
+
+## The User Model
+
+In your User Model you have to add:
+
+```ruby
+include Spree::UserMethods
+include Spree::UserAddress
+include Spree::UserPaymentSource
+```
+The first of these methods are the ones added for the `has_and_belongs_to_many` association
+called "spree_roles". This association will retrieve all the roles that
+a user has for Spree.
+
+The second of these is the `spree_orders` association. This will return
+all orders associated with the user in Spree. There's also a
+`last_incomplete_spree_order` method which will return the last
+incomplete spree order for the user. This is used internal to Spree to
+persist order data across a user's login sessions.
+
+The third and fourth associations are for address information for a
+user. When a user places an order, the address information for that
+order will be linked to that user so that it is available for subsequent
+orders.
+
+The next method is one called `has_spree_role?` which can be used to
+check if a user has a specific role. This method is used internally to
+Spree to check if the user is authorized to perform specific actions,
+such as accessing the admin section. Admin users of your system should
+be assigned the Spree admin role, like this:
+
+```ruby
+user = User.find_by(email: "master@example.com")
+user.spree_roles << Spree::Role.find_or_create_by(name: "admin")
+```
+
+To test that this has worked, use the `has_spree_role?` method, like
+this:
+
+```ruby
+user.has_spree_role?("admin")
+```
+
+If this returns `true`, then the user has admin permissions within
+Spree.
+
+Finally, if you are using the API component of Spree, there are more
+methods added. The first is the `spree_api_key` getter and setter
+methods, used for the API key that is used with Spree. The next two
+methods are `generate_spree_api_key!` and `clear_spree_api_key`
+which will generate and clear the Spree API key respectively.
+
+## Login link
+
+To make the login link appear on Spree pages, you will need to use a
+Deface override. Create a new file at
+`app/overrides/auth_login_bar.rb` and put this content inside it:
+
+```ruby
+Deface::Override.new(virtual_path: "spree/shared/_nav_bar",
+ name: "auth_login_bar",
+ insert_before: "li#search-bar",
+ partial: "spree/shared/login_bar",
+ disabled: false,
+ original: 'eb3fa668cd98b6a1c75c36420ef1b238a1fc55ad')
+```
+
+This override references a partial called "spree/shared/login_bar".
+This will live in a new partial called
+`app/views/spree/shared/_login_bar.html.erb` in your application. You
+may choose to call this file something different, the name is not
+important. This file will then contain this code:
+
+```erb
+<%% if spree_current_user %>
+
+<%% end %>
+```
+
+This will then use the URL helpers you have defined in
+`lib/spree/authentication_helpers.rb` to define three links, one to
+allow users to logout, one to allow them to login, and one to allow them
+to signup. These links will be visible on all customer-facing pages of
+Spree.
+
+## Signup promotion
+
+In Spree, there is a promotion that acts on the user signup which will
+not work correctly automatically when you're not using the standard
+authentication method with Spree. To fix this, you will need to trigger
+this event after a user has successfully signed up in your application
+by setting a session variable after successful signup in whatever
+controller deals with user signup:
+
+```ruby
+session[:spree_user_signup] = true
+```
+
+This line will cause the Spree event notifiers to be notified of this
+event and to apply any promotions to an order that are triggered once a
+user signs up.
diff --git a/guides/src/content/developer/customization/checkout.md b/guides/src/content/developer/customization/checkout.md
new file mode 100644
index 00000000000..bf335ff7173
--- /dev/null
+++ b/guides/src/content/developer/customization/checkout.md
@@ -0,0 +1,384 @@
+---
+title: "The Checkout Flow API"
+section: customization
+---
+
+## Overview
+
+The Spree checkout process has been designed for maximum flexibility. It's been redesigned several times now, each iteration has benefited from the feedback of real world deployment experience. It is relatively simple to customize the checkout process to suit your needs. Secure transmission of customer information is possible via SSL and credit card information is never stored in the database.
+
+The customization of the flow of the checkout can be done by using Spree's `checkout_flow` DSL, described in the [Checkout Flow DSL](#the-checkout-flow-dsl) section below.
+
+## Default Checkout Steps
+
+The Spree checkout process consists of the following steps. With the exception of the Registration step, each of these steps corresponds to a state of the `Spree::Order` object:
+
+* Registration (Optional - only if using spree_auth_devise extension, can be toggled through the `Spree::Auth::Config[:registration_step]` configuration setting)
+* Address Information
+* Delivery Options (Shipping Method)
+* Payment
+* Confirmation
+
+The following sections will provide a walk-though of a checkout from a user's perspective, and offer some information on how to configure the default behavior of the various steps.
+
+### Registration
+
+Prior to beginning the checkout process, the customer will be prompted to create a new account or to login to their existing account. By default, there is also a "guest checkout" option which allows users to specify only their email address if they do not wish to create an account.
+
+Technically, the registration step is not an actual state in the `Spree::Order` state machine. The `spree_auth_devise` gem (an extension that comes with Spree by default) adds the `check_registration` before filter to the all actions of `Spree::CheckoutController` (except for obvious reasons the `registration` and `update_registration` actions), which redirects to a registration page unless one of the following is true:
+
+* `Spree::Auth::Config[:registration_step]` preference is not `true`
+* user is already logged in
+* the current order has an email address associated with it
+
+The method is defined like this:
+
+```ruby
+def check_registration
+ return unless Spree::Auth::Config[:registration_step]
+ return if spree_current_user or current_order.email
+ store_location
+ redirect_to spree.checkout_registration_path
+end
+```
+
+The configuration of the guest checkout option is done via [Preferences](preferences). Spree will allow guest checkout by default. Use the `allow_guest_checkout` preference to change the default setting.
+
+### Address Information
+
+This step allows the customer to add both their billing and shipping information. Customers can click the "use billing address" option to use the same address for both. Selecting this option will have the effect of hiding the shipping address fields using JavaScript. If users have disabled JavaScript, the section will not disappear but it will copy over the address information once submitted. If you would like to automatically copy the address information via JavaScript on the client side, that is an exercise left to the developer. We have found the server side approach to be simpler and easier to maintain.
+
+The address fields include a select box for choosing state/province. The list of states will be populated via JavaScript and will contain all of the states listed in the database for the currently selected country. If there are no states configured for a particular country, or if the user has JavaScript disabled, the select box will be replaced by a text field instead.
+
+***
+The default "seed" data for Spree only includes the U.S. states. It's easy enough to add states or provinces for other countries but beyond the scope of the Spree project to maintain such a list.
+***
+
+***
+The state field can be disabled entirely by using the `Spree::Config[:address_requires_state]` preference. You can also allow for an "alternate phone" field by using the `Spree::Config[:alternative_shipping_phone]` and `Spree::Config[:alternative_shipping]` fields.
+***
+
+The list of countries that appear in the country select box can also be configured. Spree will list all countries by default, but you can configure exactly which countries you would like to appear. The list can be limited to a specific set of countries by configuring the `Spree::Config[:checkout_zone]` preference and setting its value to the name of a [Zone](addresses#zones) containing the countries you wish to use. Spree assumes that the list of billing and shipping countries will be the same. You can always change this logic via an extension if this does not suit your needs.
+
+### Delivery Options
+
+$$$
+Better shipment documentation here after split_shipments merge.
+$$$
+
+During this step, the user may choose a delivery method. Spree assumes the list of shipping methods to be dependent on the shipping address. This is one of the reasons why it is difficult to support single page checkout for customers who have disabled JavaScript.
+
+### Payment
+
+This step is where the customer provides payment information. This step is intentionally placed last in order to minimize security issues with credit card information. Credit card information is never stored in the database so it would be impossible to have a subsequent step and still be able to submit the information to the payment gateway. Spree submits the information to the gateway before saving the model so that the sensitive information can be discarded before saving the checkout information.
+
+Spree stores only the last four digits of the credit card number along with the expiration information. The full credit card number and verification code are never stored in the Spree database.
+
+Several gateways such as ActiveMerchant and Beanstream provide a secure method for storing a "payment profile" in your database. This approach typically involves the use of a "token" which can be used for subsequent purchases but only with your merchant account. If you are using a secure payment profile it would then be possible to show a final "confirmation" step after payment information is entered.
+
+If you do not want to use a gateway with payment profiles then you will need to customize the checkout process so that your final step submits the credit card information. You can then perform an authorization before the order is saved. This is perfectly secure because the credit card information is not ever saved. It's transmitted to the gateway and then discarded like normal.
+
+!!!
+Spree discards the credit card number after this step is processed. If
+you do not have a gateway with payment profiles enabled then your card
+information will be lost before it's time to authorize the card.
+!!!
+
+For more information about payments, please see the [Payments guide](payments).
+
+### Confirmation
+
+This is the final opportunity for the customer to review their order before
+submitting it to be processed. Users have the opportunity to return to any step
+in the process using either the back button or by clicking on the appropriate
+step in the "progress breadcrumb."
+
+This step is disabled by default (except for payment methods that support
+payment profiles), but can be enabled by overriding the `confirmation_required?`
+method in `Spree::Order`.
+
+## Checkout Architecture
+
+The following is a detailed summary of the checkout architecture. A complete
+understanding of this architecture will allow you to be able to customize the
+checkout process to handle just about any scenario you can think of. Feel free
+to skip this section and come back to it later if you require a deeper
+understanding of the design in order to customize your checkout.
+
+### Checkout Routes
+
+Three custom routes in spree_core handle all of the routing for a checkout:
+
+```ruby
+put '/checkout/update/:state', to: 'checkout#update', as: :update_checkout
+get '/checkout/:state', to: 'checkout#edit', as: :checkout_state
+get '/checkout', to: 'checkout#edit', as: :checkout
+```
+
+The '/checkout' route maps to the `edit` action of the
+`Spree::CheckoutController`. A request to this route will redirect to the
+current state of the current order. If the current order was in the "address"
+state, then a request to '/checkout' would redirect to '/checkout/address'.
+
+The '/checkout/:state' route is used for the previously mentioned route, and
+also maps to the `edit` action of `Spree::CheckoutController`.
+
+The '/checkout/update/:state' route maps to the
+`Spree::CheckoutController#update` action and is used in the checkout form to
+update order data during the checkout process.
+
+### Spree::CheckoutController
+
+The `Spree::CheckoutController` drives the state of an order during checkout.
+Since there is no "checkout" model, the `Spree::CheckoutController` is not a
+typical RESTful controller. The spree_core and spree_auth_devise gems expose a
+few different actions for the `Spree::CheckoutController`.
+
+The `edit` action renders the checkout/edit.html.erb template, which then
+renders a partial with the current state, such as
+`app/views/spree/checkout/address.html.erb`. This partial shows state-specific
+fields for the user to fill in. If you choose to customize the checkout flow to
+add a new state, you will need to create a new partial for this state.
+
+The `update` action performs the following:
+
+* Updates the `current_order` with the paramaters passed in from the current
+ step.
+* Transitions the order state machine using the `next` event after successfully
+ updating the order.
+* Executes callbacks based on the new state after successfully transitioning.
+* Redirects to the next checkout step if the `current_order.state` is anything
+ other than `complete`, else redirect to the `order_path` for `current_order`
+
+***
+For security reasons, the `Spree::CheckoutController` will not update the
+order once the checkout process is complete. It is therefore impossible for an
+order to be tampered with (ex. changing the quantity) after checkout.
+***
+
+### Filters
+
+The `spree_core` and the default authentication gem (`spree_auth_devise`) gems
+define several `before_actions` for the `Spree::CheckoutController`:
+
+* `load_order`: Assigns the `@order` instance variable and sets the `@order.state` to the `params[:state]` value. This filter also runs the "before" callbacks for the current state.
+* `check_authorization`: Verifies that the `current_user` has access to `current_order`.
+* `check_registration`: Checks the registration status of `current_user` and redirects to the registration step if necessary.
+
+### The Order Model and State Machine
+
+ The `Spree::Order` state machine is the foundation of the checkout process. Spree makes use of the [state_machine](https://github.com/pluginaweek/state_machine) gem in the `Spree::Order` model as well as in several other places (such as `Spree::Shipment` and `Spree::InventoryUnit`.)
+
+The default checkout flow for the `Spree::Order` model is defined in
+`app/models/spree/order/checkout.rb` of spree_core.
+
+An `Spree::Order` object has an initial state of 'cart'. From there any number
+of events transition the `Spree::Order` to different states. Spree does not
+have a separate model or database table for the shopping cart. What the user
+considers a "shopping cart" is actually an in-progress `Spree::Order`. An order
+is considered in-progress, or incomplete when its `completed_at` attribute is
+`nil`. Incomplete orders can be easily filtered during reporting and it's also
+simple enough to write a quick script to periodically purge incomplete orders
+from the system. The end result is a simplified data model along with the
+ability for store owners to search and report on incomplete/abandoned orders.
+
+***
+For more information on the state machine gem please see the [README](https://github.com/pluginaweek/state_machine)
+***
+
+## Checkout Customization
+
+It is possible to override the default checkout workflow to meet your store's needs.
+
+### Customizing an Existing Step
+
+Spree allows you to customize the individual steps of the checkout process.
+There are a few distinct scenarios that we'll cover here.
+
+* Adding logic either before or after a particular step.
+* Customizing the view for a particular step.
+
+### Adding Logic Before or After a Particular Step
+
+The state_machine gem allows you to implement callbacks before or after
+transitioning to a particular step. These callbacks work similarly to [Active Record Callbacks](http://guides.rubyonrails.org/active_record_callbacks.html)
+in that you can specify a method or block of code to be executed prior to or
+after a transition. If the method executed in a before_transition returns false,
+then the transition will not execute.
+
+So, for example, if you wanted to verify that the user provides a valid zip code
+before transitioning to the delivery step, you would first implement a
+`valid_zip_code?` method, and then tell the state machine to run this method
+before that transition, placing this code in a file called
+`app/models/spree/order_decorator.rb`:
+
+```ruby
+Spree::Order.state_machine.before_transition to: :delivery, do: :valid_zip_code?
+```
+
+This callback would prevent transitioning to the `delivery` step if
+`valid_zip_code?` returns false.
+
+### Customizing the View for a Particular Step
+
+Each of the default checkout steps has its own partial defined in the
+spree frontend `app/views/spree/checkout` directory. Changing the view for an
+existing step is as simple as overriding the relevant partial in your site
+extension. It's also possible the default partial in question defines a usable
+theme hook, in which case you could add your functionality by using
+[Deface](https://github.com/spree/deface)
+
+## The Checkout Flow DSL
+
+Since Spree 1.2, Spree comes with a new checkout DSL that allows you succinctly define the
+different steps of your checkout. This new DSL allows you to customize *just*
+the checkout flow, while maintaining the unrelated admin states, such as
+"canceled" and "resumed", that an order can transition to. Ultimately, it
+provides a shorter syntax compared with overriding the entire state machine for
+the `Spree::Order` class.
+
+The default checkout flow for Spree is defined like this, adequately
+demonstrating the abilities of this new system:
+
+```ruby
+checkout_flow do
+ go_to_state :address
+ go_to_state :delivery
+ go_to_state :payment, if: ->(order) {
+ order.update_totals
+ order.payment_required?
+ }
+ go_to_state :confirm, if: ->(order) { order.confirmation_required? }
+ go_to_state :complete
+ remove_transition from: :delivery, to: :confirm
+```
+
+we can pass a block on each checkout step definition and work some logic to
+figure if the step is required dynamically. e.g. the confirm step might only
+be necessary for payment gateways that support payment profiles.
+
+These conditional states present a situation where an order could transition
+from delivery to one of payment, confirm or complete. In the default checkout,
+we never want to transition from delivery to confirm, and therefore have removed
+it using the `remove_transition` method of the Checkout DSL. The resulting
+transitions between states look like the image below:
+
+$$$
+State diagram
+$$$
+
+These two helper methods are provided on `Spree::Order` instances for your
+convenience:
+
+* `checkout_steps`: returns a list of all the potential states of the checkout.
+* `has_step?`: Used to check if the current order fulfills the requirements for a specific state.
+
+If you want a list of all the currently available states for the checkout, use
+the `checkout_steps` method, which will return the steps in an array.
+
+### Modifying the checkout flow
+
+To add or remove steps to the checkout flow, you can use the `insert_checkout_step`
+and `remove_checkout_step` helpers respectively.
+
+The `insert_checkout_step` takes a `before` or `after` option to determine where to
+insert the step:
+
+```ruby
+insert_checkout_step :new_step, before: :address
+# or
+insert_checkout_step :new_step, after: :address
+```
+
+The `remove_checkout_step` will remove just one checkout step at a time:
+
+```ruby
+remove_checkout_step :address
+remove_checkout_step :delivery
+```
+
+What will happen here is that when a user goes to checkout, they will be asked
+to (potentially) fill in their payment details and then (potentially) confirm
+the order. This is the default behavior of the payment and the confirm steps
+within the checkout. If they are not required to provide payment or confirmation
+for this order then checking out this order will result in its immediate completion.
+
+To completely re-define the flow of the checkout, use the `checkout_flow` helper:
+
+```ruby
+checkout_flow do
+ go_to_state :payment
+ go_to_state :complete
+end
+```
+
+### The Checkout View
+After creating a checkout step, you'll need to create a partial for the checkout
+controller to load for your custom step. If your additonal checkout step is
+`new_step` you'll need to a `spree/checkout/_new_step.html.erb` partial.
+
+### The Checkout "Breadcrumb"
+
+The Spree code automatically creates a progress "breadcrumb" based on the
+available checkout states. The states listed in the breadcrumb come from the
+`Spree::Order#checkout_steps` method. If you add a new state you'll want to add
+a translation for that state in the relevant translation file located in the
+`config/locales` directory of your extension or application:
+
+```ruby
+en:
+ order_state:
+ new_step: New Step
+```
+
+***
+The default use of the breadcrumb is entirely optional. It does not need
+to correspond to checkout states, nor does every state need to be represented.
+Feel free to customize this behavior to meet your exact requirements.
+***
+
+## Payment Profiles
+
+The default checkout process in Spree assumes a gateway that allows for some
+form of third party support for payment profiles. An example of such a service
+would be [Authorize.net CIM](http://www.authorize.net/solutions/merchantsolutions/merchantservices/cim/)
+Such a service allows for a secure and PCI compliant means of storing the users
+credit card information. This allows merchants to issue refunds to the credit
+card or to make changes to an existing order without having to leave Spree and
+use the gateway provider's website. More importantly, it allows us to have a
+final "confirmation" step before the order is processed since the number is
+stored securely on the payment step and can still be used to perform the
+standard authorization/capture via the secure token provided by the gateway.
+
+Spree provides a wrapper around the standard active merchant API in order to
+provide a common abstraction for dealing with payment profiles. All `Gateway`
+classes now have a `payment_profiles_supported?` method which indicates whether
+or not payment profiles are supported. If you are adding Spree support to a
+`Gateway` you should also implement the `create_profile` method. The following
+is an example of the implementation of `create_profile` used in the
+`AuthorizeNetCim` class:
+
+```ruby
+# Create a new CIM customer profile ready to accept a payment
+def create_profile(payment)
+ if payment.source.gateway_customer_profile_id.nil?
+ profile_hash = create_customer_profile(payment)
+ payment.source.update_attributes({
+ gateway_customer_profile_id: profile_hash[:customer_profile_id],
+ gateway_payment_profile_id: profile_hash[:customer_payment_profile_id])
+ })
+ end
+end
+```
+
+!!!
+Most gateways do not yet support payment profiles but the default
+checkout process of Spree assumes that you have selected a gateway that supports
+this feature. This allows users to enter credit card information during the
+checkout without having to store it in the database. Spree has never stored
+credit card information in the database but prior to the use of profiles, the
+only safe way to handle this was to post the credit card information in the
+final step. It should be possible to customize the checkout so that the credit
+card information is entered on the final step and then you can authorize the
+card before Spree automatically discards the sensitive data before saving.
+!!!
diff --git a/guides/src/content/developer/customization/dependencies.md b/guides/src/content/developer/customization/dependencies.md
new file mode 100644
index 00000000000..2c698903350
--- /dev/null
+++ b/guides/src/content/developer/customization/dependencies.md
@@ -0,0 +1,85 @@
+---
+title: Using Dependendencies system
+section: customization
+---
+
+# Using Dependendencies system
+
+## Overview
+
+Dependendencies is a a new way to customize Spree. With Dependencies you can easily replace parts of Spree internals with your custom classes. You can replace Services, Abilities and Serializers. More will come in the future.
+
+
+ Dependencies are available in [Spree 3.7](/release_notes/3_7_0.html) and later.
+
+
+## Controller level customization
+
+To replace [serializers](https://github.com/Netflix/fast_jsonapi) or Services in a specific API endpoint you can create a simple decorator:
+
+Create a `app/controllers/spree/api/v2/storefront/cart_controller_decorator.rb`
+```ruby
+ module MyCartControllerDecorator
+ def resource_serializer
+ MyNewAwesomeCartSerializer
+ end
+
+ def add_item_service
+ MyNewAwesomeAddItemToCart
+ end
+ end
+ Spree::Api::V2::Storefront::CartController.prepend MyCartControllerDecorator
+```
+
+## API level customization
+
+Storefront and Platform APIs have separate Dependencies injection points so you can easily customize one without touching the other.
+
+In your Spree initializer (`config/initializers/spree.rb`) please add:
+
+```ruby
+Spree::Api::Dependencies[:storefront_cart_serializer] = 'MyNewAwesomeCartSerializer'
+Spree::Api::Dependencies[:storefront_cart_add_item_service] = 'MyNewAwesomeAddItemToCart'
+```
+
+This will swap the default Cart serializer and Add Item to Cart service for your custom ones within all Storefront API endpoints that uses those classes.
+
+
+ Values set in the initializer has to be strings, eg. `MyNewAwesomeAddItemToCart`
+
+
+## Application (global) customization
+
+You can also inject classes globally to the entire Spree stack. Be careful about this though as this touches every aspect of the application (both APIs, Admin Panel and default Rails frontend if you're using it).
+
+```ruby
+Spree::Dependencies[:cart_add_item_service] = 'MyNewAwesomeAddItemToCart'
+```
+
+or
+
+```ruby
+Spree.dependencies do |dependencies|
+ dependencies.cart_add_item_service = 'MyNewAwesomeAddItemToCart'
+end
+```
+
+You can mix and match both global and API level customizations:
+
+```ruby
+Spree::Dependencies[:cart_add_item_service] = 'MyNewAwesomeAddItemToCart'
+Spree::Api::Dependencies[:storefront_cart_add_item_service] = 'AnotherAddItemToCart'
+```
+
+The second line will have precedence over the first one, and the Storefront API will use `AnotherAddItemToCart` and the rest of the application will use `MyNewAwesomeAddItemToCart`
+
+
+ Values set in the initializer has to be strings, eg. `MyNewAwesomeAddItemToCart`
+
+
+## Default values
+
+Default values can be easily checked looking at the source code of Dependencies classes:
+
+- [Application (global) dependencies](https://github.com/spree/spree/blob/master/core/app/models/spree/app_dependencies.rb)
+- [API level dependencies](https://github.com/spree/spree/blob/master/api/app/models/spree/api_dependencies.rb)
diff --git a/guides/src/content/developer/customization/i18n.md b/guides/src/content/developer/customization/i18n.md
new file mode 100644
index 00000000000..5e3f8943361
--- /dev/null
+++ b/guides/src/content/developer/customization/i18n.md
@@ -0,0 +1,188 @@
+---
+title: "Internationalization"
+section: customization
+---
+
+## Overview
+
+This guide covers how Spree uses Rails' internationalization features, and how
+you can leverage and extend these features in your Spree contributions and
+extensions.
+
+## How Spree i18n works
+
+Spree uses the standard Rails approach to internationalization so we suggest
+take some time to review the
+[official Rails i18n guide](http://guides.rubyonrails.org/i18n.html) to help you
+get started.
+
+### The spree_i18n project
+
+Spree now stores all of the translation information in a separate GitHub project
+known as [spree_i18n](https://github.com/spree/spree_i18n). This is a stand
+alone project with a large number of volunteer committers who maintain the
+locale files. This is basically the same approach followed by the Rails project
+which keeps their localizations in
+[rails-i18n](https://github.com/svenfuchs/rails-i18n).
+
+The project is actually a Spree extension. This extension contains translations and
+uses the [globalize3 gem](https://github.com/svenfuchs/globalize3) to provide
+translations for model records.
+
+!!!
+You will need to install the [spree_i18n](https://github.com/spree/spree_i18n)
+gem if you want to use any of the community supplied translations of Spree.
+!!!
+
+### Translation Files
+
+Each language is stored in a YAML file located in `config/locales`. Each YAML
+file contains one top level key which is the language code for the translations
+contained within that file. The following is a snippet showing the basic layout
+of a locale file:
+
+```yaml
+pt-BR:
+ spree:
+ say_no: "Não"
+ say_yes: "Sim"
+```
+
+***
+All translations for Spree are "namespaced" within the `spree` key so that they
+don't conflict with translations from other parts of the parent application.
+***
+
+#### Localization Files
+
+Spree maintains its localization information in a YAML file using a naming
+convention similar to that of the Rails project. Each of the localization
+filenames contains a prefix representing the language code of the locale. For
+example, the Russian translation is contained in `config/locales/ru.yml`.
+
+***
+Spree has over 43 locale files and counting. See the [GitHub
+Repository](https://github.com/spree/spree_i18n/tree/master/config/locales) for a
+complete list.
+***
+
+#### Required Files
+
+Each locale that you wish to support will require both a Rails and Spree
+translation. The required Spree translation files are available automatically
+when you install the `spree_i18n` gem.
+
+You don't need to copy any files from `spree_i18n` or `rails-i18n` for their
+translations to be available within your application. They are made available
+automatically, because both `spree_i18n` and `rails-i18n` are railties.
+
+### Translating Views
+
+When reviewing the source of any view in Spree you'll notice that all text is
+rendered by passing a string to a helper method similar to:
+
+```erb
+<%%= Spree.t(:price) %>
+```
+
+The *Spree.t()* helper method looks up the currently configured locale and retrieves
+the translated value from the relevant locale YAML file. Assuming a default
+locale, this translation would be fetched from the en translations collated from
+the application, `spree_i18n` and `rails-i18n`. Its relative key within those
+translation files would need to be this:
+
+```yaml
+en:
+ spree:
+ price: Price
+```
+
+### The Default Locale
+
+Since Spree is basically a Rails application it has the same default locale as
+any Rails application. The default locale is `en` which use the English
+language. We can verify this in the rails console
+
+```ruby
+>> I18n.locale
+=> :en
+```
+
+You can also see in the console how the default locale values are translated
+into English
+
+```ruby
+>> Spree.t(:action)
+=> Action
+```
+
+## Deploying the Translations
+
+The `spree_i18n` gem is configured in the same manner as any Rubygem in a Rails
+application. Simply add it to the `Gemfile.` using the git url.
+
+```ruby
+gem 'spree_i18n', github: 'spree/spree_i18n'
+```
+
+### Setting the Default Locale
+
+The default locale for Rails, and therefore Spree, is `en`. This can be changed by setting
+`config.i18n.default_locale` in `config/application.rb`. This setting is ignored
+unless the relevant translation file are within `#{Rails.root}/config/locales`
+or the `spree_i18n` gem.
+
+### Setting the Default Currency
+
+***
+This functionality was new in Spree 1.2. Please refer to the appropriate
+guide if you are using an older version.
+***
+
+In earlier versions of Spree, we used `number_to_currency` to display prices for
+products. This caused a problem when somebody selected a different I18n locale,
+as the prices would be displayed in their currency: 20 Japanese Yen, rather than
+20 American Dollars, for instance.
+
+To fix this problem, we're now parsing the prices through the Money gem which
+will display prices consistently across all I18n locales. To now change the
+currency for your site, go to Admin, then Configuration, then General Settings.
+Changing the currency will only change the currency symbol across all prices of
+your store.
+
+There are configuration options for currency:
+
+* `Spree::Config[:currency]`: 3-letter currency code representing the current currency.
+
+### Localizing Seed Data
+
+Spree use [Carmen](https://github.com/jim/carmen) to seed the Country and State data. You can localize the seed data by adding Carmen configuration to your `seeds.rb`. See example below:
+
+```ruby
+# add Carmen counfiguration with the following 2 lines
+require 'carmen'
+Carmen.i18n_backend.locale = :ja
+
+Spree::Core::Engine.load_seed if defined?(Spree::Core)
+Spree::Auth::Engine.load_seed if defined?(Spree::Auth)
+```
+
+## Creating and Modifying Locales
+
+While we have used [LocaleApp](http://localeapp.com) in the past to manage the translations for the spree_i18n project, Localeapp does not have support for different branches within the same project. As such, please submit Pull Requests or issues directly to https://github.com/spree/spree_i18n for missing translations.
+
+## Localizing Extensions
+
+Spree extensions can contain their own `config/locales` directory where
+developers can include YAML files for each language they wish to support.
+
+We strongly urge all extension developers to ensure all customer facing text is
+rendered via the `Spree.t()` helper method even if they only include a single default
+language locale file (as other users can simply include the required YAML file
+and translations in their site extension).
+
+***
+Since Spree extensions are equivalent to Rails Engines they can provide
+localization information automatically (just like a standalone Rails
+application.)
+***
diff --git a/guides/src/content/developer/customization/images.md b/guides/src/content/developer/customization/images.md
new file mode 100644
index 00000000000..b230f4c17ef
--- /dev/null
+++ b/guides/src/content/developer/customization/images.md
@@ -0,0 +1,127 @@
+---
+title: 'Images Customization'
+section: customization
+---
+
+## Overview
+
+This guide explains how to change Product Images dimensions and different storage options for both **ActiveStorage** and **Paperclip**.
+
+## ActiveStorage
+
+ActiveStorage is the default attachment storage system since [Spree 3.6](https://guides.spreecommerce.org/release_notes/spree_3_6_0.html) and [Rails 5.2](https://guides.rubyonrails.org/5_2_release_notes.html).
+To read more about ActiveStorage head to the [official documentation](https://edgeguides.rubyonrails.org/active_storage_overview.html).
+
+### Image dimensions
+
+To change the default image dimensions or add new ones you need to create a decorator file `app/models/spree/image_decorator.rb`:
+
+```ruby
+module YourApplication
+ module Spree
+ module ImageDecorator
+ module ClassMethods
+ def styles
+ {
+ mini: '48x48>',
+ small: '100x100>',
+ product: '240x240>',
+ large: '600x600>',
+ }
+ end
+ end
+
+ def self.prepended(base)
+ base.inheritance_column = nil
+ base.singleton_class.prepend ClassMethods
+ end
+ end
+ end
+end
+
+Spree::Image.prepend ::YourApplication::Spree::ImageDecorator
+```
+
+You can also create image variations on the fly in your templates, eg.
+
+```erb
+<%%= image_tag(main_app.url_for(@product.images.first.attachment.variant(resize: '150x150'))) %>
+```
+
+### Using Amazon S3 as storage system
+
+Please refer to the official [Active Storage documentation](https://guides.rubyonrails.org/active_storage_overview.html#amazon-s3-service)
+
+You can also use [Microsoft Azure Storage](https://guides.rubyonrails.org/active_storage_overview.html#microsoft-azure-storage-service)
+or [Google Cloud Storage](https://guides.rubyonrails.org/active_storage_overview.html#google-cloud-storage-service)
+
+## Paperclip
+
+**Paperclip** support will be removed in Spree 4.0. To migrate to **ActiveStorage** please read [the official migration guide](https://github.com/thoughtbot/paperclip/blob/master/MIGRATING.md).
+
+### Image dimensions
+
+Until Spree 3.6 we've used Thoughtbot's
+[paperclip](https://github.com/thoughtbot/paperclip) gem to manage
+images for products. All the normal paperclip options are available on
+the `Image` class. If you want to modify the default Spree product and
+thumbnail image sizes, simply create an `app/models/spree/image_decorator.rb` file and override the attachment sizes:
+
+```ruby
+Spree::Image.class_eval do
+ attachment_definitions[:attachment][:styles] = {
+ mini: '48x48>', # thumbs under image
+ small: '100x100>', # images on category view
+ product: '240x240>', # full product image
+ large: '600x600>' # light box image
+ }
+end
+```
+
+You may also add additional image sizes for use in your templates
+(:micro for shopping cart view, for example).
+
+### Image resizing option syntax
+
+Default behavior is to resize the image and maintain aspect ratio (i.e.
+the :product version of a 480x400 image will be 240x200). Some commonly
+used options are:
+
+- trailing `#`, image will be centrally cropped, ensuring the requested
+ dimensions
+- trailing `>`, image will only be modified if it is currently larger
+ than the requested dimensions. (i.e. the :small thumb for a 100x100
+ original image will be unchanged)
+
+### Using Amazon S3 as storage system
+
+Start with adding AWS-SDK to your `Gemfile` with: `gem 'aws-sdk-s3'`, then install the gem by running `bundle install`.
+
+When that's done you need to configure Spree to use Amazon S3. You can add an initializer or just use the spree.rb initializer located at `config/intializers/spree.rb`.
+
+```ruby
+attachment_config = {
+ s3_credentials: {
+ access_key_id: ENV['AWS_ACCESS_KEY_ID'],
+ secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
+ bucket: ENV['S3_BUCKET_NAME']
+ },
+
+ storage: :s3,
+ s3_region: ENV['S3_REGION'],
+ s3_headers: { "Cache-Control" => "max-age=31557600" },
+ s3_protocol: "https",
+ bucket: ENV['S3_BUCKET_NAME'],
+ url: ":s3_domain_url",
+
+ path: "/:class/:id/:style/:basename.:extension",
+ default_url: "/:class/:id/:style/:basename.:extension",
+}
+
+attachment_config.each do |key, value|
+ Spree::Image.attachment_definitions[:attachment][key.to_sym] = value
+end
+
+```
+
+Note that I use the `url: ":s3_domain_url"` setting, this enabled the DNS lookup for your images without specifying the specific zone endpoint. You need to use a bucket name that makes a valid subdomain. So do not use dots if you are planning on using the DNS lookup config.
diff --git a/guides/src/content/developer/customization/index.md b/guides/src/content/developer/customization/index.md
new file mode 100644
index 00000000000..9d0a74ee49b
--- /dev/null
+++ b/guides/src/content/developer/customization/index.md
@@ -0,0 +1,90 @@
+---
+title: "Customization Overview"
+section: customization
+---
+
+This guide explains the customization and extension techniques you can
+use to adapt a generic Spree store to meet your specific design and
+functional requirements, including:
+
+- Explanation of how customizations are organized and shared
+- Overview of the three major customization options: View, Asset &
+ Logic
+
+For more detailed information and a step-by-step tutorial on creating
+extensions for Spree be sure to checkout the
+[Extensions](extensions_tutorial.html) guide.
+
+### Managing Customizations
+
+Spree supports three methods for managing and organizing your
+customizations. While they all support exactly the same options for
+customization they differ in terms of re-usability. So before you get
+started you need to decide what sort of customizations you are going to
+make.
+
+#### Application Specific
+
+Application specific customizations are the most common type of
+customization applied to Spree. It's generally used by developers and
+designers to tweak Spree's behaviour or appearance to match a particular
+business's operating procedures, branding, or provide a unique feature.
+
+All application specific customizations are stored within the host
+application where Spree is installed (please see the Installation
+section of the [Getting Started with Spree](getting_started_tutorial.html) guide,
+for how to setup the host application). Application customizations are
+not generally shared or re-used in any way.
+
+#### Extension
+
+Extensions enable developers to enhance or add functionality to Spree,
+and are generally discrete pieces of functionality that are shared and
+intended to be installed in multiple Spree implementations.
+
+Extensions are generally distributed as ruby gems and implemented as
+standard Rails 3 engines so they provide a natural way to bundle all the
+changes needed to implement larger features.
+
+Visit the [Extension Registry](https://github.com/spree-contrib) to
+get an idea of the type and volume of extensions available.
+
+#### Theme
+
+Themes are designed to overhaul the entire look and feel of a Spree
+store (or its administration system). Themes are distributed in exactly
+the same manner as extensions, but don't generally include logic
+customizations.
+
+***
+For more implementation details on Extensions and Themes please
+refer to the [Extensions & Themes](extensions_tutorial.html) guide.
+***
+
+### Customization Options
+
+Once you've decided how you're going to manage your customizations, you
+then need to choose the correct option to achieve the desired changes.
+
+#### View Customizations
+
+Allows you to change and/or extend the look and feel of a Spree store
+(and its administration system). For details see the [View
+Customization](view_customization.html) guide.
+
+#### Asset Customizations
+
+Allows changing the static assets provided by Spree, this includes
+stylesheets, JavaScript files and images. For details see the [Asset
+Customization](asset_customization.html) guide.
+
+#### Use S3 for storage
+
+Setup Spree to store your images on S3. For details see the
+ [Use S3 for storage](s3_storage.html) guide
+
+#### Logic Customizations
+
+Enables the changing and/or extension of the logic of Spree to meet your
+specific business requirements. For details see the [Logic
+Customization](logic_customization.html) guide.
diff --git a/guides/src/content/developer/customization/logic.md b/guides/src/content/developer/customization/logic.md
new file mode 100644
index 00000000000..262906e6981
--- /dev/null
+++ b/guides/src/content/developer/customization/logic.md
@@ -0,0 +1,71 @@
+---
+title: Logic Customization
+section: customization
+---
+
+## Overview
+
+This guide explains how to customize the internal Spree code to meet
+your exact business requirements.
+
+## Extending Classes
+
+All of Spree's business logic (models, controllers, helpers, etc) can
+easily be extended / overridden to meet your exact requirements using
+standard Ruby idioms.
+
+Standard practice for including such changes in your application or
+extension is to create a file within the relevant **app/models/spree** or
+**app/controllers/spree** directory with the original class name with
+**\_decorator** appended.
+
+**Adding a custom method to the Product model:**
+`app/models/spree/product_decorator.rb`
+
+```ruby
+Spree::Product.class_eval do
+ def some_method
+ ...
+ end
+end
+```
+
+**Adding a custom action to the ProductsController:**
+`app/controllers/spree/products_controller_decorator.rb`
+
+```ruby
+Spree::ProductsController.class_eval do
+ def some_action
+ ...
+ end
+end
+```
+
+---
+
+The exact same format can be used to redefine an existing method.
+
+---
+
+### Accessing Product Data
+
+If you extend the Products controller with a new method, you may very
+well want to access product data in that method. You can do so by using
+the `:load_data before_action`.
+
+```ruby
+Spree::ProductsController.class_eval do
+ before_action :load_data, only: :some_action
+
+ def some_action
+ ...
+ end
+end
+```
+
+---
+
+`:load_data` will use `params[:id]` to lookup the product by its
+permalink.
+
+---
diff --git a/guides/src/content/developer/customization/s3_storage.md b/guides/src/content/developer/customization/s3_storage.md
new file mode 100644
index 00000000000..c82f94ab9e7
--- /dev/null
+++ b/guides/src/content/developer/customization/s3_storage.md
@@ -0,0 +1,51 @@
+---
+title: "Use S3 for storage"
+section: customization
+---
+
+## How to use S3 with ActiveStorage
+
+Since [Spree 3.6](https://guides.spreecommerce.org/release_notes/spree_3_6_0.html) ActiveStorage is the default attachment system.
+
+Please refer to the official [Active Storage documentation](https://guides.rubyonrails.org/active_storage_overview.html#amazon-s3-service)
+
+## How to use S3 with Paperclip
+
+Start with adding AWS-SDK to your `Gemfile` with: `gem 'aws-sdk-s3'`, then install the gem by running `bundle install`.
+
+When that's done you need to configure Spree to use Amazon S3. You can add an initializer or just use the spree.rb initializer located at `config/intializers/spree.rb`.
+
+```ruby
+attachment_config = {
+
+ s3_credentials: {
+ access_key_id: ENV['AWS_ACCESS_KEY_ID'],
+ secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
+ bucket: ENV['S3_BUCKET_NAME']
+ },
+
+ storage: :s3,
+ s3_region: ENV['S3_REGION'],
+ s3_headers: { "Cache-Control" => "max-age=31557600" },
+ s3_protocol: "https",
+ bucket: ENV['S3_BUCKET_NAME'],
+ url: ":s3_domain_url",
+
+ styles: {
+ mini: "48x48>",
+ small: "100x100>",
+ product: "240x240>",
+ large: "600x600>"
+ },
+
+ path: "/:class/:id/:style/:basename.:extension",
+ default_url: "/:class/:id/:style/:basename.:extension",
+ default_style: "product"
+}
+
+attachment_config.each do |key, value|
+ Spree::Image.attachment_definitions[:attachment][key.to_sym] = value
+end
+
+```
+Note that I use the `url: ":s3_domain_url"` setting, this enabled the DNS lookup for your images without specifying the specific zone endpoint. You need to use a bucket name that makes a valid subdomain. So do not use dots if you are planning on using the DNS lookup config.
diff --git a/guides/src/content/developer/customization/view.md b/guides/src/content/developer/customization/view.md
new file mode 100644
index 00000000000..f946ba9a2d3
--- /dev/null
+++ b/guides/src/content/developer/customization/view.md
@@ -0,0 +1,305 @@
+---
+title: 'View Customization'
+section: customization
+---
+
+## Overview
+
+View customization allows you to extend or replace any view within a
+Spree store. This guide explains the options available, including:
+
+- Overwriting view templates
+- Using Deface for small view customizations
+
+## Template Replacements
+
+Spree supports the duplication of views within an application or extension that will
+completely replace the file of the same name in Spree.
+
+To override any of Spree's default views including those for the admin
+interface, simply create a file with the same filename in your app/views
+directory.
+
+For example, to override the main layout, create the file
+`YOUR_SITE_OR_EXTENSION/app/views/spree/layouts/spree_application.html.erb`
+
+You can export all views from spree frontend into your application
+using
+
+```bash
+rails generate spree:frontend:copy_views
+```
+
+This is the recommended way of customizing views in Spree.
+
+## Using Deface
+
+Deface is a standalone Rails library that enables you to customize Erb
+templates without needing to directly edit the underlying view file.
+Deface allows you to use standard CSS3 style selectors to target any
+element (including Ruby blocks), and perform an action against all the
+matching elements.
+
+We recommend using Deface only for really small changes.
+
+For example, take the Checkout Registration template, which looks like
+this:
+
+```erb
+<%%= render 'spree/shared/error_messages', target: user %>
+
<%%= Spree.t(:registration) %>
+
+
+
+
+ <%% if Spree::Config[:allow_guest_checkout] %>
+
+ <%%= render 'spree/shared/error_messages', target: order %>
+
+
+```
+
+If you wanted to insert some code just before the +#registration+ div on the page you would define an override as follows:
+
+```ruby
+Deface::Override.new(virtual_path : "spree/checkout/registration",
+ insert_before: "div#registration",
+ text : "
Registration is the future!
",
+ name : "registration_future")
+```
+
+This override **inserts**
Registration is the future!
**before** the div with the id of "registration".
+
+### Available actions
+
+Deface applies an **action** to element(s) matching the supplied CSS selector. These actions are passed when defining a new override are supplied as the key while the CSS selector for the target element(s) is the value, for example:
+
+```ruby
+remove: "p.junk"
+
+insert_after: "div#wow p.header"
+
+insert_bottom: "ul#giant-list"
+```
+
+Deface currently supports the following actions:
+
+- :remove - Removes all elements that match the supplied selector
+- :replace - Replaces all elements that match the supplied selector, with the content supplied
+- :replace_contents - Replaces the contents of all elements that match the supplied selector
+- :surround - Surrounds all elements that match the supplied selector, expects replacement markup to contain <%%= render_original %> placeholder
+- :surround_contents - Surrounds the contents of all elements that match the supplied selector, expects replacement markup to contain <%%= render_original %> placeholder
+- :insert_after - Inserts after all elements that match the supplied selector
+- :insert_before - Inserts before all elements that match the supplied selector
+- :insert_top - Inserts inside all elements that match the supplied selector, as the first child
+- :insert_bottom - Inserts inside all elements that match the supplied selector, as the last child
+- :set_attributes - Sets attributes on all elements that match the supplied selector, replacing existing attribute value if present or adding if not. Expects :attributes option to be passed.
+- :add_to_attributes - Appends value to attributes on all elements that match the supplied selector, adds attribute if not present. Expects :attributes option to be passed.
+- :remove_from_attributes - Removes value from attributes on all elements that match the supplied selector. Expects :attributes option to be passed.
+
+---
+
+Not all actions are applicable to all elements. For example, :insert_top and :insert_bottom expects a parent element with children.
+
+---
+
+### Supplying content
+
+Deface supports three options for supplying content to be used by an override:
+
+- :text - String containing markup
+- :partial - Relative path to a partial
+- :template - Relative path to a template
+
+---
+
+As Deface operates on the Erb source the content supplied to an override can include Erb, it is not limited to just HTML. You also have access to all variables accessible in the original Erb context.
+
+---
+
+### Targeting elements
+
+While Deface allows you to use a large subset of CSS3 style selectors (as provided by Nokogiri), the majority of Spree's views have been updated to include a custom HTML attribute (data-hook), which is designed to provide consistent targets for your overrides to use.
+
+As Spree views are changed over coming versions, the original HTML elements maybe edited or be removed. We will endeavour to ensure that data-hook / id combination will remain consistent within any single view file (where possible), thus making your overrides more robust and upgrade proof.
+
+For example, spree/products/show.html.erb looks as follows:
+
+```erb
+
+```
+
+As you can see from the example above the `data-hook` can be present in
+a number of ways:
+
+- On elements with **no** `id` attribute the `data-hook` attribute
+ contains a value similar to what would be included in the `id`
+ attribute.
+- On elements with an `id` attribute the `data-hook` attribute does
+ **not** normally contain a value.
+- Occasionally on elements with an `id` attribute the `data-hook` will
+ contain a value different from the elements id. This is generally to
+ support migration from the old 0.60.x style of hooks, where the old
+ hook names were converted into `data-hook` versions.
+
+The suggested way to target an element is to use the `data-hook`
+attribute wherever possible. Here are a few examples based on
+**products/show.html.erb** above:
+
+```ruby
+replace: "[data-hook='product_show']"
+
+insert_top: "#thumbnails[data-hook]"
+
+remove: "[data-hook='cart_form']"
+```
+
+You can also use a combination of both styles of selectors in a single
+override to ensure maximum protection against changes:
+
+```ruby
+ insert_top: "[data-hook='thumbnails'], #thumbnails[data-hook]"
+```
+
+### Targeting ruby blocks
+
+Deface evaluates all the selectors passed against the original erb view
+contents (and importantly not against the finished / generated HTML). In
+order for Deface to make ruby blocks contained in a view parseable they
+are converted into a pseudo markup as follows.
+
+---
+
+Version 1.0 of Deface, used in Spree 2.1, changed the code tag syntax.
+Formerly code tags were parsed as `` and ``. They are now parsed as `` and ``.
+Deface overrides which used selectors like `code[erb-loud]` should now
+use `erb[loud]`.
+
+---
+
+Given the following Erb file:
+
+```erb
+<%% if products.empty? %>
+ <%%= Spree.t(:no_products_found) %>
+<%% elsif params.key?(:keywords) %>
+
+```
+
+We want our override to insert another field container after the price field container. We can do this by creating a new file `app/overrides/add_sale_price_to_product_edit.rb` and adding the following content:
+
+```ruby
+Deface::Override.new(virtual_path: 'spree/admin/products/_form',
+ name: 'add_sale_price_to_product_edit',
+ insert_after: "erb[loud]:contains('text_field :price')",
+ text: "
+ <%%= f.field_container :sale_price do %>
+ <%%= f.label :sale_price, raw(Spree.t(:sale_price) + content_tag(:span, ' *')) %>
+ <%%= f.text_field :sale_price, value:
+ number_to_currency(@product.sale_price, unit: '') %>
+ <%%= f.error_message_on :sale_price %>
+ <%% end %>
+ ")
+```
+
+We also need to delegate `sale_price` to the master variant in order to get the
+updated product edit form working.
+
+We can do this by creating a new file `app/models/spree/product_decorator.rb` and adding the following content to it:
+
+```ruby
+module Spree
+ Product.class_eval do
+ delegate :sale_price, :sale_price=, to: :master
+ end
+end
+```
+
+Now, when we head to `http://localhost:3000/admin/products` and edit a product, we should be able to set a sale price for the product and be able to view it on our sale page, `http://localhost:3000/sale`. Note that you will likely need to restart our example Spree application (created in the [Getting Started](getting_started_tutorial) tutorial).
diff --git a/guides/src/content/developer/tutorials/extensions_tutorial.md b/guides/src/content/developer/tutorials/extensions_tutorial.md
new file mode 100644
index 00000000000..c8ecb16b39a
--- /dev/null
+++ b/guides/src/content/developer/tutorials/extensions_tutorial.md
@@ -0,0 +1,287 @@
+---
+title: Extensions
+section: tutorial
+---
+
+## Introduction
+
+This tutorial continues where we left off in the [Getting Started](getting_started_tutorial) tutorial. Now that we have a basic Spree store up and running, let's spend some time customizing it. The easiest way to do this is by using Spree extensions.
+
+### What is a Spree Extension?
+
+Extensions are the primary mechanism for customizing a Spree site. They provide a convenient mechanism for Spree developers to share reusable code with one another. Even if you do not plan on sharing your extensions with the community, they can still be a useful way to reuse code within your organization. Extensions are also a convenient mechanism for organizing and isolating discrete chunks of functionality.
+
+## Installing an Extension
+
+We are going to be adding the [spree_i18n](https://github.com/spree-contrib/spree_i18n) extension to our store. SpreeI18n is a extension containing community contributed translations of Spree & ability to supply different attribute values per language such as product names and descriptions. Extensions can also add models, controllers, and views to create new functionality.
+
+There are three steps we need to take to install spree_i18n.
+
+First, we need to add the gem to the bottom of our `Gemfile`:
+
+```ruby
+gem 'spree_i18n', github: 'spree-contrib/spree_i18n', branch: 'master'
+```
+
+***
+If you are using a 3.0.x or 2.x version of Spree, you'll need to change the `branch` to `X-X-stable`
+to match the version of Spree you're using. For example, use `3-0-stable` if you're using Spree `3.0.x`.
+***
+
+Now, let's install the gem via Bundler with the following command:
+
+```bash
+$ bundle install
+```
+
+Finally, let's copy over the required migrations and assets from the extension with the following command:
+
+```bash
+$ bundle exec rails g spree_i18n:install
+```
+
+Answer **yes** when prompted to run migrations.
+
+## Creating an Extension
+
+### Getting Started
+
+Let's build a simple extension. Suppose we want the ability to mark certain products as being on sale. We'd like to be able to set a sale price on a product and show products that are on sale on a separate products page. This is a great example of how an extension can be used to build on the solid Spree foundation.
+
+So let's start by generating the extension. Run the following command from a directory of your choice outside of our Spree application:
+
+```bash
+$ spree extension simple_sales
+```
+
+This creates a `spree_simple_sales` directory with several additional files and directories. After generating the extension make sure you change to its directory:
+
+```bash
+$ cd spree_simple_sales
+```
+
+### Adding a Sale Price to Variants
+
+The first thing we need to do is create a migration that adds a sale_price column to [variants](/developer/products.html#variants).
+
+We can do this with the following command:
+
+```bash
+bundle exec rails g migration add_sale_price_to_spree_variants sale_price:decimal
+```
+
+Because we are dealing with prices, we need to now edit the generated migration to ensure the correct precision and scale. Edit the file `db/migrate/XXXXXXXXXXX_add_sale_price_to_spree_variants.rb` so that it contains the following:
+
+```ruby
+class AddSalePriceToSpreeVariants < SpreeExtension::Migration[4.2]
+ def change
+ add_column :spree_variants, :sale_price, :decimal, precision: 8, scale: 2
+ end
+end
+```
+
+***
+We're not inheriting directly from ActiveRecord::Migration, instead we're using
+[SpreeExtension::Migration](https://github.com/spree-contrib/spree_extension/blob/master/lib/spree_extension/migration.rb) to support multiple Rails versions.
+***
+
+### Adding Our Extension to the Spree Application
+
+Before we continue development of our extension, let's add it to the Spree application we created in the [last tutorial](/developer/getting_started_tutorial.html). This will allow us to see how the extension works with an actual Spree store while we develop it.
+
+Within the `mystore` application directory, add the following line to the bottom of our `Gemfile`:
+
+```ruby
+gem 'spree_simple_sales', path: '../spree_simple_sales'
+```
+
+You may have to adjust the path somewhat depending on where you created the extension. You want this to be the path relative to the location of the `mystore` application.
+
+Once you have added the gem, it's time to bundle:
+
+```bash
+$ bundle install
+```
+
+Finally, let's run the `spree_simple_sales` install generator to copy over the migration we just created (answer **yes** if prompted to run migrations):
+
+```bash
+# context: Your Spree store's app root (i.e. Rails.root); not the extension's root path.
+$ rails g spree_simple_sales:install
+```
+
+### Adding a Controller Action to HomeController
+
+Now we need to extend `Spree::HomeController` and add an action that selects "on sale" products.
+
+***
+Note for the sake of this example that `Spree::HomeController` is only included
+in spree_frontend so you need to make it a dependency on your extensions *.gemspec file.
+***
+
+Make sure you are in the `spree_simple_sales` root directory and run the following command to create the directory structure for our controller decorator:
+
+```bash
+$ mkdir -p app/controllers/spree
+```
+
+Next, create a new file in the directory we just created called `home_controller_decorator.rb` and add the following content to it:
+
+```ruby
+Spree::HomeController.class_eval do
+ def sale
+ @products = Spree::Product.joins(:variants_including_master).where('spree_variants.sale_price is not null').distinct
+ end
+end
+```
+
+This will select just the products that have a variant with a `sale_price` set.
+
+We also need to add a route to this action in our `config/routes.rb` file. Let's do this now. Update the routes file to contain the following:
+
+```ruby
+Spree::Core::Engine.routes.draw do
+ get "/sale" => "home#sale"
+end
+```
+
+### Viewing On Sale Products
+
+#### Setting the Sale Price for a Variant
+
+Now that our variants have the attribute `sale_price` available to them, let's update the sample data so we have at least one product that is on sale in our application. We will need to do this in the rails console for the time being, as we have no admin interface to set sale prices for variants. We will be adding this functionality in the [next tutorial](deface_overrides_tutorial) in this series, Deface overrides.
+
+So, in order to do this, first open up the rails console:
+
+```bash
+$ rails console
+```
+
+Now, follow the steps I take in selecting a product and updating its master variant to have a sale price. Note, you may not be editing the exact same product as I am, but this is not important. We just need one "on sale" product to display on the sales page.
+
+```irb
+> product = Spree::Product.first
+=> #
+
+> variant = product.master
+=> #, position: nil, lock_version: 0, on_demand: false, cost_currency: nil, sale_price: nil>
+
+> variant.sale_price = 8.00
+=> 8.0
+
+> variant.save
+=> true
+```
+
+### Creating a View
+
+Now we have at least one product in our database that is on sale. Let's create a view to display these products.
+
+First, create the required views directory with the following command:
+
+```bash
+$ mkdir -p app/views/spree/home
+```
+
+Next, create the file `app/views/spree/home/sale.html.erb` and add the following content to it:
+
+```erb
+
+```
+
+If you navigate to `http://localhost:3000/sale` you should now see the product(s) listed that we set a `sale_price` on earlier in the tutorial. However, if you look at the price, you'll notice that it's not actually displaying the correct price. This is easy enough to fix and we will cover that in the next section.
+
+### Decorating Variants
+
+Let's fix our extension so that it uses the `sale_price` when it is present.
+
+First, create the required directory structure for our new decorator:
+
+```bash
+$ mkdir -p app/models/spree
+```
+
+Next, create the file `app/models/spree/variant_decorator.rb` and add the following content to it:
+
+```ruby
+Spree::Variant.class_eval do
+ alias_method :orig_price_in, :price_in
+ def price_in(currency)
+ return orig_price_in(currency) unless sale_price.present?
+ Spree::Price.new(variant_id: self.id, amount: self.sale_price, currency: currency)
+ end
+end
+```
+
+Here we alias the original method `price_in` to `orig_price_in` and override it. If there is a `sale_price` present on the product's master variant, we return that price. Otherwise, we call the original implementation of `price_in`.
+
+### Testing Our Decorator
+
+It's always a good idea to test your code. We should be extra careful to write tests for our Variant decorator since we are modifying core Spree functionality. Let's write a couple of simple unit tests for `variant_decorator.rb`
+
+#### Generating the Test App
+
+An extension is not a full Rails application, so we need something to test our extension against. By running the Spree `test_app` rake task, we can generate a barebones Spree application within our `spec` directory to run our tests against.
+
+We can do this with the following command from the root directory of our extension:
+
+```bash
+$ bundle exec rake test_app
+```
+
+After this command completes, you should be able to run `rspec` and see the following output:
+
+```bash
+No examples found.
+
+Finished in 0.00005 seconds
+0 examples, 0 failures
+```
+
+Great! We're ready to start adding some tests. Let's replicate the extension's directory structure in our spec directory by running the following command
+
+```bash
+$ mkdir -p spec/models/spree
+```
+
+Now, let's create a new file in this directory called `variant_decorator_spec.rb` and add the following tests to it:
+
+```ruby
+require 'spec_helper'
+
+describe Spree::Variant do
+ describe "#price_in" do
+ it "returns the sale price if it is present" do
+ variant = create(:variant, sale_price: 8.00)
+ expected = Spree::Price.new(variant_id: variant.id, currency: "USD", amount: variant.sale_price)
+
+ result = variant.price_in("USD")
+
+ expect(result.variant_id).to eq(expected.variant_id)
+ expect(result.amount.to_f).to eq(expected.amount.to_f)
+ expect(result.currency).to eq(expected.currency)
+ end
+
+ it "returns the normal price if it is not on sale" do
+ variant = create(:variant, price: 15.00)
+ expected = Spree::Price.new(variant_id: variant.id, currency: "USD", amount: variant.price)
+
+ result = variant.price_in("USD")
+
+ expect(result.variant_id).to eq(expected.variant_id)
+ expect(result.amount.to_f).to eq(expected.amount.to_f)
+ expect(result.currency).to eq(expected.currency)
+ end
+ end
+end
+```
+
+These specs test that the `price_in` method we overrode in our `VariantDecorator` returns the correct price both when the sale price is present and when it is not.
+
+## Summary
+
+In this tutorial you learned how to both install extensions and create your own. A lot of core Spree development concepts were covered and you gained exposure to some of the Spree internals.
+
+In the [next part](deface_overrides_tutorial) of this tutorial series, we will cover [Deface](https://github.com/spree/deface) overrides and look at ways to improve our current extension.
diff --git a/guides/src/content/developer/tutorials/getting_started_tutorial.md b/guides/src/content/developer/tutorials/getting_started_tutorial.md
new file mode 100644
index 00000000000..3bc33dd8ab2
--- /dev/null
+++ b/guides/src/content/developer/tutorials/getting_started_tutorial.md
@@ -0,0 +1,131 @@
+---
+title: Getting Started
+section: tutorial
+---
+
+## Prerequisites
+
+Before starting this tutorial, make sure you have Ruby and RubyGems installed on your system. This is fairly straightforward, but differs depending on which operating system you use.
+
+By following this tutorial, you will create a simple Spree project called `mystore`. Before you can start building the application, you need to make sure that you have Rails itself installed.
+
+To run Spree 3.6 you need the latest Rails version, 5.2.0
+
+### Installing Rails
+
+In most cases, the easiest way to install Rails is to take advantage of RubyGems:
+
+```bash
+$ gem install rails -v 5.2.0
+```
+
+### Installing Bundler
+
+Bundler is the current standard for maintaining Ruby gem dependencies. It is
+recommended that you have a decent working knowledge of Bundler and how it's
+used within Rails before attempting to install Spree. You can install Bundler
+using the following command:
+
+```bash
+$ gem install bundler
+```
+
+### Installing Image Magick
+
+Spree also uses the ImageMagick library for manipulating images. Using this library allows for automatic resizing of product images and the creation of product image thumbnails. ImageMagick is not a Rubygem and it can be a bit tricky to install. There are, however, several excellent sources of information on the Web for how to install it. A basic Google search should help you if you get stuck.
+
+If you are using macOS, a recommended approach is to install ImageMagick
+using [Homebrew](http://mxcl.github.com/homebrew/). This can be done with the
+following command:
+
+```bash
+$ brew install imagemagick
+```
+
+If you are using Unix or Windows check out [Imagemagick.org](http://www.imagemagick.org/) for more detailed instructions on how to setup ImageMagick for your particular system.
+
+## Creating a New Spree Project
+
+The distribution of Spree as a Rubygem allows it to be used in a new Rails project or added to an existing Rails project. This guide will assume you are creating a brand new store and will walk you through the process, starting with the creation of a new Rails application.
+
+### Creating the Rails Application
+
+Let's start by creating a standard Rails application using the following command:
+
+```bash
+$ rails _5.2.0_ new mystore
+```
+
+### Adding Spree to Your Rails Application
+
+Now that we have a basic Rails application we can add Spree to it. This approach would also work with existing Rails applications that have been around for a long time (assuming they are using the correct version of Rails.)
+
+After you create the store application, switch to its folder to continue work directly in that application:
+
+```bash
+$ cd mystore
+```
+
+Add Spree gems to your Gemfile:
+
+```ruby
+gem 'spree', '~> 3.6'
+gem 'spree_auth_devise', '~> 3.3'
+gem 'spree_gateway', '~> 3.3'
+```
+
+Run `bundle install`
+
+Use the install generators to set up Spree:
+
+```shell
+rails g spree:install --user_class=Spree::User
+rails g spree:auth:install
+rails g spree_gateway:install
+```
+
+***
+
+## Hello, Spree!
+
+You now have a functional Spree application after running only a few commands! To see it, you need to start a web server on your development machine. You can do this by running another command:
+
+```
+$ rails server
+```
+
+This will fire up an instance of the Webrick web server by default (Spree can
+also use several other web servers). To see your application in action, open a
+browser window and navigate to [http://localhost:3000](http://localhost:3000).
+You should see the Spree default home page:
+
+
+
+To stop the web server, hit Ctrl-C in the terminal window where it's running. In development mode, Spree does not generally require you to stop the server; changes you make in files will be automatically picked up by the server.
+
+### Logging Into the Backend
+
+The next thing you'll probably want to do is to log into the admin interface.
+Use your browser window to navigate to
+[http://localhost:3000/admin](http://localhost:3000/admin). You can login with
+the username `spree@example.com` and password `spree123`.
+
+***
+If you elected not to use the `--auto-accept` option when you added Spree to your Rails app, and did not install the seed data, the admin user will not yet exist in your database. You can run a simple rake task to create a new admin user.
+
+```bash
+$ rake spree_auth:admin:create
+```
+***
+
+Upon successful authentication, you should see the admin screen:
+
+
+
+Feel free to explore some of the backend features that Spree has to offer and to verify that your installation is working properly.
+
+## Wrapping Up
+
+If you've followed the steps described in this tutorial, you should now have a fully functional Spree application up and running.
+
+This tutorial is part of a series. The next tutorial in this series is the [Extensions Tutorial](extensions_tutorial).
diff --git a/guides/src/content/developer/tutorials/index.md b/guides/src/content/developer/tutorials/index.md
new file mode 100644
index 00000000000..46fadac2e51
--- /dev/null
+++ b/guides/src/content/developer/tutorials/index.md
@@ -0,0 +1,6 @@
+---
+title: "Tutorials"
+section: tutorial
+---
+
+## Tutorial
diff --git a/guides/src/content/developer/tutorials/migration.md b/guides/src/content/developer/tutorials/migration.md
new file mode 100644
index 00000000000..cc5e8d45dda
--- /dev/null
+++ b/guides/src/content/developer/tutorials/migration.md
@@ -0,0 +1,483 @@
+---
+title: Migrating to Spree
+section: advanced
+---
+
+## Overview
+
+This section explains how to convert existing sites or data sets for
+use with Spree. It is a mix of tips and information about the relevant
+APIs, and so is definitely intended for developers. After reading it you
+should know:
+
+- techniques for programmatic import of products
+- tips for migrating themes
+- examples of the API in use.
+
+!!!
+This documentation on this topic is out of date and we're
+working to update it. In the meantime if you see things in here that are
+confusing it's possible that they no longer apply, etc.
+!!!
+
+## Overview
+
+This guide is a mix of tips and information about the relevant APIs,
+intended to help simplify the process of getting a new site set up -
+whether you're developing a fresh site or moving from an existing
+commerce platform.
+
+The first section discusses various formats of data. Then we look in
+detail at import of the product catalogue. Sometimes you may want to
+import legacy order details, so there's a short discussion on this.
+
+Finally, there are some tips about how to ease the theme development
+process.
+
+## Data Import Format
+
+This part discusses some options for getting data into the system,
+including some discussion of using relevant formats.
+
+### Direct SQL import
+
+Can we just format our data as SQL tables and import it directly? In
+principle yes, but it takes effort to get the format right,
+particularly
+when dealing with associations between tables, and you need to ensure
+that the new data meets the system's validation rules. It's probably
+easier to go the code route.
+
+There are cases where direct import is useful. One key case is when
+moving between hosting platforms. Another is when cloning some project:
+collaborators can just import a database dump prepared by someone else,
+and save the time of the code import.
+
+### Rails Fixtures
+
+Spree uses fixtures to load up the sample data. It's a convenient
+format for small collections of data, but can be tricky when working with
+large data sets, especially if there are many interconnections and if you
+need to be careful with validation.
+
+Note that Rails can dump slices of the database in fixture format. This
+is sometimes useful.
+
+### SQL or XML legacy data
+
+This is the case where you are working with legacy data in formats like
+SQL or XML, and the question is more how to get the useful data out.
+
+Some systems may be able to export their data in various standard
+spreadsheet formats - it's worth checking for this.
+
+Tools like REXML or Nokogiri can be used to parse XML and either build
+a spreadsheet representation or execute product-building actions
+directly.
+
+For SQL, you can try to build a Rails interface to the data (eg. search
+for help with legacy mappings) and dump a simplified format. It might
+help
+to use views or complex queries to flatten multi-table data into a
+single table - which can then be treated like a spreadsheet.
+
+### Spreadsheet format
+
+Most of the information about products can be flattened into
+spreadsheet
+form, and a 2D table is convenient to work with. Clients are often
+comfortable with the format too, and able to supply their inventories
+in this format.
+
+For example, your spreadsheet could have the following columns:
+
+### Fixed Details
+
+- product name
+- master price
+- master sku
+- taxon membership
+- shipping category
+- tax category
+- dimensions and weight
+- list of images
+- description
+
+### Several Properties
+- one column for each property type used in your catalogue
+
+### Variant Specifications
+- option types for the product
+- one variant per column, each listing the option values and the price/sku
+
+Note that if you know how many fixed columns and properties to expect,
+then it's easy to determine which columns represent variants etc.
+
+Some of these columns might have simple punctuation etc. to add structure
+to the field. For example, we've used:
+
+- Html tags in the description
+- WxHxD for a shorthand for the dimensions
+- "green & small = small_green_shirt @ $10.00" to code up a variant which is small and green, has sku *small_green_shirt* and costs $10.
+- "foo\nbar" in the taxons column to encode membership of two taxons
+- "alpha > beta > gamma" in the taxons column to encode membership a particular nesting.
+
+The taxon nesting notation is useful for when 'gamma' doesn't uniquely
+identify a taxon (and so you need some context, ie a few ancestor
+taxons), or for when the taxon structure isn't fixed in advance and so is
+dynamically created as the products are entered.
+
+Another possibility for coding variants is to have each variant on a
+separate row, and to leave the fixed fields empty when a row is a variant of the
+last-introduced product. This is easier to read.
+
+### Seed code
+
+This is more a technique for getting the data loaded at the right time.
+Technically, the product catalogue is *seed data*, standard data which
+is needed for the app to work properly.
+
+Spree has several options for loading seed data, but perhaps the easiest
+to use here is to put ruby files in *site/db/default/*. These files are
+processed when *rake db:seed* is called, and will be processed in the order of the
+migration timestamps.
+
+Your ruby script can use one of the XLS or CSV format reading libraries
+to read an external file, or if the data set is not too big, you could
+embed the CSV text in the script itself, eg. using the **END** convention.
+
+***
+If the order of loading is important, choose names for the files
+so that alphabetical order gives the correct load order…
+***
+
+### Important system-wide settings
+
+A related but important topic is the Spree core settings that your app
+will need to function correctly, eg to disable backordering or to
+configure the mail subsystem. You can (mostly) set these from the admin
+interface, but we recommend using initializers for these. See the
+[preferences
+guide](preferences.html#persisting-modifications-to-preferences) for
+more info.
+
+## Catalog creation
+
+This section covers everything relating to import of a product set,
+including the product details, variants, properties and options,
+images, and taxons.
+
+### Preliminaries
+
+Let's assume that you are working from a CSV-compatible format, and so
+are reading one product per row, and each row contains values for the fixed
+details, properties, and variants configuration.
+
+We won't always explicitly save changes to records: we assume that your
+upload scripts will call *save* at appropriate times or use
+*update_attribute+
+etc.
+
+### Products
+
+Products must have at least a name and a price in order to pass
+validation, and we set the description too.
+
+```ruby
+p = Spree::Product.create name: 'some product', price: 10.0,
+description: 'some text here'
+```
+
+Observe that the permalink and timestamps are added automatically.
+You may want to set the 'meta' fields for SEO purposes.
+
+***
+It's important to set the *available_on* field. Without this
+being a date in the past, the product won't be listed in the standard
+displays.
+***
+
+```ruby
+p.available_on = Time.current
+```
+
+#### The Master variant
+
+Every product has a master variant, and this is created automatically
+when the product is created. It is accessible via *p.master*, but note that many
+of its fields are accessible through the product via delegation. Example:
+*p.price* does the same as *p.master.price*. Delegation also allows field
+modification, so *p.price = 2 * p.price* doubles the product's (master) price.
+
+The dimensions and weight fields should be self-explanatory.
+The *sku* field holds the product's stock code, and you will want to set
+this if the product does not have option variants.
+
+#### Stock levels
+
+If you don't have option variants, then you may also need to register
+some stock for the master variant. The exact steps depend on how you
+have configured Spree's [inventory system](inventory.html), but most sites
+will just need to assign to *p.on_hand*, eg *p.on_hand = 100*.
+
+#### Shipping category
+
+A product's [shipping category](shipments.html#shipping-categories) field
+provides product-specific information for the shipping
+calculators, eg to indicate that a product requires additional insurance
+or can only be surface shipped. If no special conditions are needed, you
+can leave this field as nil.
+The *Spree::ShippingCategory* model is effectively a wrapper for a
+string. You can either generate the list of categories in advance, or use
+*where.first_or_create* to reuse previous objects or create new ones
+when required.
+
+```ruby
+p.shipping_category = Spree::ShippingCategory.where(name: 'Type A').first_or_create
+```
+
+#### Tax category
+
+This is a similar idea to the shipping category, and guides the
+calculation of product taxes, eg to distinguish clothing items from electrical
+goods.
+The model wraps a name *and* a description (both strings), and you can
+leave the field as nil if no special treatment is needed.
+
+You can use the *where.first_or_create* technique, though you probably
+want to set up the entire [tax configuration](taxation.html) before you start
+loading products.
+
+You can also fill in this information automatically at a *later* date,
+e.g. use the taxon information to decide which tax categories something
+belongs in.
+
+### Taxons
+
+Adding a product to a particular taxon is easy: just add the taxon to
+the list of taxons for a product.
+
+```ruby
+p.taxons << some_taxon
+```
+
+Recall that taxons work like subclassing in OO languages, so a product
+in taxon T is also contained in T's ancestors, so you should usually assign a
+product to the most specific applicable taxon - and do not need to assign it to
+all of the taxon's ancestors.\
+However, you can assign products to as many taxons as you want,
+including ancestor taxons. This feature is more useful with sibling taxons, e.g.
+assigning a red and green shirt to both 'red clothes' and 'green
+clothes'.
+
+***
+Yes, this also means that child taxons don't have to be distinct, ie
+they can overlap.
+***
+
+When uploading from a spreadsheet, you might have one or more taxons
+listed for a product, and these taxons will be identified by name.
+Individual taxon names don't have to be unique, e.g. you could have
+'shirts' under 'male clothing', and 'shirts' under 'female clothing'.
+In this case, you need some context, eg 'male clothing > shirts' vs.
+'female clothing > shirts'.
+
+Do you need to create the taxon structure in advance? Not always: as the
+code below shows, it is possible to create taxons as and when they are
+needed, but this can be cumbersome for deep hierarchies. One compromise is to
+create the top levels (say the top 2 or 3 levels) in advance, then use
+the taxon information column to do some product-specific fine tuning.
+
+The following code uses a list of (newline-separated) taxon descriptions-
+possibly using 'A > B > C' style of context to assign the taxons for a product. Notice the use of
+*where.first_or_create*.
+
+```ruby
+# create outside of loop
+ main_taxonomy = Spree::Taxonomy.where(name: 'Products').first_or_create
+
+# inside of main loop
+the_taxons = []
+taxon_col.split(/[\r\n]*/).each do |chain|
+ taxon = nil
+ names = chain.split
+ names.each do |name|
+ taxon = Spree::Taxon.where.first_or_create
+ end
+ the_taxons << taxon
+end
+p.taxons = the_taxons
+
+```
+
+You can use similar code to set up other taxonomies, e.g. to have a
+taxonomy for brands and product ranges, like 'Guitars' with child
+'Acoustic'. You could use various property or option values to drive the
+creation of such taxonomies.
+
+### Product Properties
+
+The first step is to create the property 'types'. These should be
+known in advance so you can define these at the start of the script. You
+should give the internal name and presentation name. For simplicity, the code
+examples have these names as the same string.
+
+```ruby
+size_prop = Spree::Property.where(name: 'size', presentation: 'Size').first_or_create
+```
+
+Then you just set the value for the property-product pair.
+Assuming value*size_info+ which is derived from the relevant
+column, this means:
+
+```ruby
+Spree::ProductProperty.create property: size_prop, product: p, value: size_info
+```
+
+#### Product prototypes
+
+The admin interface uses a system of 'prototypes' to speed up data
+entry, which seeds a product with a given set of option types and (empty)
+property values. It probably isn't so useful when creating products
+programmatically, since the code will need to do the hard work of
+creating variants and setting properties anyway. However, we mention it
+here for completeness.
+
+### Variants
+
+Variants allow different versions of a product to be offered, e.g.
+allowing variations in size and color for clothing. If a product comes in only
+one configuration, you don't need to use variants - the master variant,
+already created, is sufficient.
+
+Otherwise, you need to declare what the allowed option types are (e.g.
+size, color, quality rating, etc) for your product, and then create variants
+which (usually) have a single option value for each of the product's option
+types (e.g. 'small' and 'red' etc).
+
+***
+Spree's core generally assumes that each variant has exactly one
+option value for each of the product's option types, but the current
+code is tolerant of missing values. Certain extensions may be more
+strict, e.g. ones for providing advanced variant selection.
+***
+
+#### Creating variants
+
+New variants require only a product to be associated with, but it is
+useful to set an identifying *sku* code too. The price field is optional: if it is not
+explicitly set, the new variant will use the master variant's price (the same applies to
+*cost_price* too). You can also set the *weight*, *width*, *height*, and *depth* too.
+
+```ruby
+v = Spree::Variant.create product: p, sku: "some_sku_code", price: NNNN
+```
+
+***
+The price is only copied at creation, so any subsequent changes to
+a product's price will need to be copied to all of its variants.
+***
+
+Next, you may also want to register some stock for this variant.
+The exact steps depend on how you have configured Spree's
+[inventory system](inventory.html), but most sites
+will just need to assign to *v.on_hand*, eg *v.on_hand = 100*.
+
+You now need to set some option types and values, so customers can
+choose between the variants.
+
+#### Option types
+
+The option types to use will vary from product to product, so you will
+need to give this information for each product - or assume a default
+and only use different names when this column is empty.
+
+You can probably declare most of the option types in advance, and so
+just look up the names when required, though for fine control, you can
+use the *where.first_or_create* technique, with something like this:
+
+```ruby
+p.option_types = option_names_col.map do |name|
+ Spree::OptionType.where(name: name, presentation: name).first_or_create
+end
+```
+
+#### Option values
+
+Option values represent the choices possible for some option type.
+Again, you could declare them in advance, or use *where.first_or_create*. You'll
+probably find it easier to create/retrieve the option values as you create each variant.
+
+Suppose you are using a notation like *"Green & Small = small_green_shirt @ $10.00"*
+to encode each variant in the spreadsheet, and this is stored in the variable
+*opt_info*. The following extracts the three key pieces of information and sets
+the option values for the new variant (see below for variant creation).
+
+```ruby
+*, opts, sku, price = opt_info.match(/(.+)\s=\s(\w+)\s@\s\$(.+)/).to_a
+v = Spree::Variant.create product: p, sku: sku, price: price
+v.option_values = opts.split('&').map do |nm|
+ Spree::OptionValue.where.first_or_create nm.strip
+end
+```
+
+***
+You don't have to stick with system-wide option types: you can
+create types specifically for groups of products such as a product range from a single
+manufacturer. In such cases, the range might have a particular color
+scheme and there can be advantages to isolating the scheme's options in its
+own type and set of values, rather than trying to work with a more general
+setup. It also avoids filling up a type with lots of similar options -
+and so reduces the number of options when using faceted search etc. You can
+also attach resources like color swatches to the more specific values.
+
+#### Ordering of option values
+You might want option values to appear in a certain order, such as by
+increasing size or by alphabetical order. The *Spree::OptionValue* model uses
+*acts_as_list* for setting the order, and option types will use the *position* field when retrieving
+their associated values. The position is scoped to the relevant option type.
+
+If you create option values in advance, just create them in the required
+order and the plugin will set the *position* automatically.
+
+```ruby
+color_type = Spree::OptionType.create name: 'Color', presentation: 'Color'
+color_options = %w[Red Blue Green].split.map { |n|
+ Spree::OptionValue.create name: n, presentation: n,
+ option_type: color_type }
+```
+
+Otherwise, you could enforce the ordering*after_ loading up all of the
+variants, using something like this:
+
+```ruby
+color_type.option_values.sort_by(&:name).each_with_index do |val,pos|
+ val.update_attribute(:position, pos + 1)
+end
+```
+
+#### Further reading
+
+[Steph Skardal](https://github.com/stephskardal) has produced a useful
+blog post on [product
+optioning](http://blog.endpoint.com/2010/01/rails-ecommerce-spree-hooks-comments.html).
+This discusses how the variant option representation works and how she
+used it to build an extension for enhanced product option selection.
+
+### Product and Variant images
+
+Spree uses [paperclip](https://github.com/thoughtbot/paperclip) to
+manage image attachments and their various size formats. (See the [Customization Guide](logic#product-images) for info on altering the image formats.)
+You can attach images to products and to variants - the mechanism is
+polymorphic. Given some local image file, the following will associate the image and
+create all of the size formats.
+
+```ruby
+#for image for product (all variants) represented by master variant
+img = Spree::Image.create(attachment: File.open(path), viewable: product.master)
+
+#for image for single variant
+img = Spree::Image.create(attachment: File.open(path), viewable: variant)
+```
+
+Paperclip also supports external [storage of images in S3](https://github.com/thoughtbot/paperclip/blob/master/lib/paperclip/storage.rb)
diff --git a/guides/src/content/developer/tutorials/security.md b/guides/src/content/developer/tutorials/security.md
new file mode 100644
index 00000000000..b6aca86a6eb
--- /dev/null
+++ b/guides/src/content/developer/tutorials/security.md
@@ -0,0 +1,311 @@
+---
+title: Security
+section: advanced
+---
+
+## Overview
+
+Proper application design, intelligent programming, and secure infrastructure are all essential in creating a secure e-commerce store using any software (Spree included). The Spree team has done its best to provide you with the tools to create a secure and profitable web presence, but it is up to you to take these tools and put them in good practice. We highly recommend reading and understanding the [Rails Security Guide](http://guides.rubyonrails.org/security.html).
+
+## Reporting Security Issues
+
+Please do not announce potential security vulnerabilities in public. We have a [dedicated email address](mailto:security@spreecommerce.com). We will work quickly to determine the severity of the issue and provide a fix for the appropriate versions. We will credit you with the discovery of this patch by naming you in a blog post.
+
+If you would like to provide a patch yourself for the security issue **do not open a pull request for it**. Instead, create a commit on your fork of Spree and run this command:
+
+```bash
+$ git format-patch HEAD~1..HEAD --stdout > patch.txt
+```
+
+This command will generate a file called `patch.txt` with your changes. Please email a description of the patch along with the patch itself to our [dedicated email address](mailto:security@spreecommerce.com).
+
+## Authentication
+
+If you install spree_auth_devise when setting up your app, we use a third party authentication library for Ruby known as [Devise](https://github.com/plataformatec/devise). This library provides a host of useful functionality that is in turn available to Spree, including the following features:
+
+* Authentication
+* Strong password encryption (with the ability to specify your own algorithms)
+* "Remember Me" cookies
+* "Forgot my password" emails
+* Token-based access (for REST API)
+
+### Devise Configuration
+
+***
+A default Spree install comes with the [spree_auth_devise](https://github.com/spree/spree_auth_devise) gem, which provides authentication for Spree using Devise. This section of the guide covers the default setup. If you're using your own authentication, please consult the manual for that authentication engine.
+***
+
+We have configured Devise to handle only what is needed to authenticate with a Spree site. The following details cover the default configurations:
+
+* Passwords are stored in the database encrypted with the salt.
+* User authentication is done through the database query.
+* User registration is enabled and the user's login is available immediately (no validation emails).
+* There is a remember me and password recovery tool built in and enabled through Devise.
+
+These configurations represent a reasonable starting point for a typical e-commerce site. Devise can be configured extensively to allow for a different feature set but that is currently beyond the scope of this document. Developers are encouraged to visit the [Devise wiki](https://github.com/plataformatec/devise/wiki) for more details.
+
+### REST API
+
+The REST API behaves slightly differently than a standard user. First, an admin has to create the access key before any user can query the REST API. This includes generating the key for the admin him/herself. This is not the case if `Spree::Api::Config[:requires_authentication]` is set to `false`.
+
+In cases where `Spree::Api::Config[:requires_authentication]` is set to `false`, read-only requests in the API will be possible for all users. For actions that modify data within Spree, a user will need to have an API key and then their user record would need to have permission to perform those actions.
+
+It is up to you to communicate that key. As an added measure, this authentication has to occur on every request made through the REST API as no session or cookies are created or stored for the REST API.
+
+### Authorization
+
+Spree uses the excellent [CanCan](https://github.com/ryanb/cancan) gem to provide authorization services. If you are unfamiliar with it, you should take a look at Ryan Bates' [excellent screencast](http://railscasts.com/episodes/192-authorization-with-cancan) on the topic (or read the [transcribed version](http://asciicasts.com/episodes/192-authorization-with-cancan)). A detailed explanation of CanCan is beyond the scope of this guide.
+
+### Default Rules
+
+The follow Spree source code is taken from `ability.rb` and provides some insight into the default authorization rules:
+
+```ruby
+if user.respond_to?(:has_spree_role?) && user.has_spree_role?('admin')
+ can :manage, :all
+else
+ #############################
+ can [:read,:update,:destroy], Spree.user_class, id: user.id
+ can :create, Spree.user_class
+ #############################
+ can :read, Order do |order, token|
+ order.user == user || order.token && token == order.token
+ end
+ can :update, Order do |order, token|
+ order.user == user || order.token && token == order.token
+ end
+ can :create, Order
+
+ can :read, Address do |address|
+ address.user == user
+ end
+
+ #############################
+ can :read, Product
+ can :index, Product
+ #############################
+ can :read, Taxon
+ can :index, Taxon
+ #############################
+end
+```
+
+The above rule set has the following practical effects for Spree users
+
+* Admin role can access anything (the rest of the rules are ignored)
+* Anyone can create a `User`, only the user associated with an account can perform read or update operations for that user.
+* Anyone can create an `Order`, only the user associated with the order can perform read or update operations.
+* Anyone can read product pages and look at lists of `Products` (including search operations).
+* Anyone can read or view a list of `Taxons`.
+
+### Enforcing the Rules
+
+CanCan is only effective in enforcing authorization rules if it's asked. In other words, if the source code does not check permissions there is no way to deny access based on those permissions. This is generally handled by adding the appropriate code to your Rails controllers. For more information please see the [CanCan Wiki](https://github.com/ryanb/cancan/wiki).
+
+### Custom Authorization Rules
+
+We have modified the original CanCan concept to make it easier for extension developers and end users to add their own custom authorization rules. For instance, if you have an "artwork extension" that allows users to attach custom artwork to an order, you will need to add rules so that they have permissions to do so.
+
+The trick to adding custom authorization rules is to add an `AbilityDecorator` to your extension and then to register these abilities. The following code is an example of how to restrict access so that only the owner of the artwork can update it or view it.
+
+```ruby
+class AbilityDecorator
+ include CanCan::Ability
+ def initialize(user)
+ can :read, Artwork do |artwork|
+ artwork.order && artwork.order.user == user
+ end
+ can :update, Artwork do |artwork|
+ artwork.order && artwork.order.user == user
+ end
+ end
+end
+
+Spree::Ability.register_ability(AbilityDecorator)
+```
+
+### Custom Roles in the Admin Namespace
+
+If you plan on allowing a custom role you create to access the Spree administrative
+panels, there are a couple of considerations to keep in mind.
+
+Spree authorizes all of its administrative panels with two CanCan authorization
+commands: `:admin` and the name of the action being authorized. If you want a
+custom role to be able to access a particular admin panel, you have to specify
+that your role *can* access both :admin and the name of the action on the relevant
+resource. For example, if you want your Sales Representatives to be able to access the Admin
+Orders panel without giving them access to anything else in the Admin namespace,
+you would have to specify the following in an `AbilityDecorator`:
+
+```ruby
+class AbilityDecorator
+ include CanCan::Ability
+ def initialize(user)
+ if user.respond_to?(:has_spree_role?) && user.has_spree_role?('sales_rep')
+ can [:admin, :index, :show], Spree::Order
+ end
+ end
+end
+
+Spree::Ability.register_ability(AbilityDecorator)
+```
+
+This is required by the following code in Spree's `Admin::BaseController` which
+is the controller every controller in the Admin namespace inherits from.
+
+```ruby
+def authorize_admin
+ if respond_to?(:model_class, true) && model_class
+ record = model_class
+ else
+ record = Object
+ end
+ authorize! :admin, record
+ authorize! action, record
+end
+```
+
+If you need to create custom controllers for your own models under the Admin
+namespace, you will need to manually specify the model your controller manipulates
+by defining a `model_class` method in that controller.
+
+```ruby
+module Spree
+ module Admin
+ class WidgetsController < BaseController
+ def index
+ # Relevant code in here
+ end
+ private
+ def model_class
+ Widget
+ end
+ end
+ end
+end
+```
+
+This is necessary because CanCan cannot, by default, detect the model used to
+authorize controllers under the Admin namespace. By specifying `model_class`, Spree
+knows what to tell CanCan to use to authorize your controller.
+
+
+If you inherit from `ResourceController` instead of directly `BaseController`, and if you define new
+collection actions, you need to override `ResourceController#collection_actions` which contains only `[:index]` by default.
+
+
+```ruby
+module Spree
+ module Admin
+ class WidgetsController < ResourceController
+ def index
+ # Relevant code in here
+ end
+
+ def new_coll_action
+ # relevant code
+ end
+
+ def collection_actions
+ [:index, :new_coll_action]
+ end
+ private
+ def model_class
+ Widget
+ end
+ end
+ end
+end
+```
+
+### Tokenized Permissions
+
+There are situations where it may be desirable to restrict access to a particular resource without requiring a user to authenticate in order to have that access. Spree allows so-called "guest checkouts" where users just supply an email address and they're not required to create an account. In these cases you still want to restrict access to that order so only the original customer can see it. The solution is to use a "tokenized" URL.
+
+http://example.com/orders?token=aidik313dsfs49d
+
+Spree provides a `TokenizedPermission` model used to grant access to various resources through a secure token. This model works in conjunction with the `Spree::TokenResource` module which can be used to add tokenized access functionality to any Spree resource.
+
+```ruby
+module Spree
+ module Core
+ module TokenResource
+ module ClassMethods
+ def token_resource
+ has_one :tokenized_permission, as: :permissable
+ delegate :token, to: :tokenized_permission, allow_nil: true
+ after_create :create_token
+ end
+ end
+
+ def create_token
+ permission = build_tokenized_permission
+ permission.token = token = ::SecureRandom::hex(8)
+ permission.save!
+ token
+ end
+
+ def self.included(receiver)
+ receiver.extend ClassMethods
+ end
+ end
+ end
+end
+
+ActiveRecord::Base.class_eval { include Spree::Core::TokenResource }
+```
+
+The `Order` model is one such model in Spree where this interface is already in use. The following code snippet shows how to add this functionality through the use of the `token_resource` declaration:
+
+```ruby
+Spree::Order.class_eval do
+ token_resource
+end
+```
+
+If we examine the default CanCan permissions for `Order` we can see how tokens can be used to grant access in cases where the user is not authenticated.
+
+```ruby
+can :read, Spree::Order do |order, token|
+ order.user == user || order.token && token == order.token
+end
+
+can :update, Spree::Order do |order, token|
+ order.user == user || order.token && token == order.token
+end
+
+can :create, Spree::Order
+```
+
+This configuration states that in order to read or update an order, you must be either authenticated as the correct user, or supply the correct authorizing token.
+
+The final step is to ensure that the token is passed to CanCan when the authorization is performed, which is done in the controller.
+
+```ruby
+authorize! action, resource, session[:access_token]
+```
+
+## Credit Card Data
+
+### PCI Compliance
+
+All store owners wishing to process credit card transactions should be familiar with [PCI Compliance](http://en.wikipedia.org/wiki/Pci_compliance). Spree makes
+absolutely no warranty regarding PCI compliance (or anything else for that matter - see the [LICENSE](https://github.com/spree/spree/blob/master/license.md) for details.) We do, however, follow common sense security practices in handling credit card data.
+
+### Transmit Exactly Once
+
+Spree uses extreme caution in its handling of credit cards. In production mode, credit card data is transmitted to Spree via SSL. The data is immediately relayed to your chosen payment gateway and then discarded. The credit card data is never stored in the database (not even temporarily) and it exists in memory on the server for only a fraction of a second before it is discarded.
+
+Spree does store the last four digits of the credit card and the expiration month and date. You could easily customize Spree further if you wanted and opt out of storing even that little bit of information.
+
+### Payment Profiles
+
+Spree also supports the use of "payment profiles." This means that you can "store" a customer's credit card information in your database securely. More precisely you store a "token" that allows you to use the credit card again. The credit card gateway is actually the place where the credit card is stored. Spree ends up storing a token that can be used to authorize new charges on that same card without having to store sensitive credit card details.
+
+Spree has out of the box support for [Authorize.net CIM](http://www.authorize.net/solutions/merchantsolutions/merchantservices/cim/) payment profiles.
+
+### Other Options
+
+There are also third-party extensions for Paypal's [Express Checkout](https://developer.paypal.com/docs/classic/products/express-checkout/) (formerly called Paypal Express.) These types of checkout services handle processing of the credit card information offsite (the data never touches your server) and greatly simplify the requirements for PCI compliance.
+
+[Braintree](https://braintreepayments.com) also offers a very interesting gateway option that achieves a similar benefit to Express Checkout but allows the entire process to appear to be taking place on the site. In other words, the customer never appears to leave the store during the checkout. They describe this as a "transparent redirect." The Braintree team is very interested in helping other Ruby developers use their gateway and have provided support to Spree developers in the past who were interested in using their product.
diff --git a/guides/src/content/developer/tutorials/testing.md b/guides/src/content/developer/tutorials/testing.md
new file mode 100644
index 00000000000..65fc6135e14
--- /dev/null
+++ b/guides/src/content/developer/tutorials/testing.md
@@ -0,0 +1,144 @@
+---
+title: Testing Spree Applications
+section: advanced
+---
+
+## Overview
+
+The Spree project currently uses [RSpec](http://rspec.info) for all of its tests. Each of the gems that makes up Spree has a test suite that can be run to verify the code base.
+
+The Spree test code is an evolving story. We started out with RSpec, then switched to Shoulda and now we're back to RSpec. RSpec has evolved considerably since we first tried it. When looking to improve the test coverage of Spree we took another look at RSpec and it was the clear winner in terms of strength of community and documentation.
+
+## Testing Spree Components
+
+Spree consists of several different gems (see the [Source Code Guide](navigating#layout-and-structure) for more details.) Each of these gems has its own test suite which can be found in the `spec` directory. Since these gems are also Rails engines, they can't really be tested in complete isolation - they need to be tested within the context of a Rails application.
+
+You can easily build such an application by using the Rake task designed for this purpose, running it inside the component you want to test:
+
+```bash
+$ bundle exec rake test_app
+```
+
+This will build the appropriate test application inside of your `spec` directory. It will also add the gem under test to your `Gemfile` along with the `spree_core` gem (since all of the gems depend on this.)
+
+This rake task will regenerate the application (after deleting the existing one) each time you run it. It will also run the migrations for you automatically so that your test database is ready to go. There is no need to run `rake db:migrate` or `rake db:test:prepare` after running `test_app`.
+
+### Running the Specs
+
+Once your test application has been built, you can then run the specs in the standard RSpec manner:
+
+```bash
+$ bundle exec rspec spec
+```
+
+We also set up a build script that mimics what our build server performs. You can run it from the root of the Spree project like this:
+
+```bash
+$ bash build.sh
+```
+
+If you wish to run spec for a single file then you can do so like this:
+
+```bash
+$ bundle exec rspec spec/models/spree/state_spec.rb
+```
+
+If you wish to test a particular line number of the spec file then you can do so like this:
+
+```bash
+$ bundle exec rspec spec/models/spree/state_spec.rb:7
+```
+
+### Using Factories
+
+Spree uses [factory_bot](https://github.com/thoughtbot/factory_bot) to create valid records for testing purpose. All of the factories are also packaged in the gem. So if you are writing an extension or if you just want to play with Spree models then you can use these factories as illustrated below.
+
+```bash
+$ rails console
+$ require 'spree/testing_support/factories'
+```
+
+The `spree_core` gem has a good number of factories which can be used for testing. If you are writing an extension or just testing Spree you can make use of these factories.
+
+## Testing Your Spree Application
+
+Currently, Spree does not come with any tests that you can install into your application. What we would advise doing instead is either copying the tests from the components of Spree and modifying them as you need them or writing your own test suite.
+
+### Unit Testing
+
+Spree itself is well unit-tested. However, when you install a Spree store for the first time, your new app doesn't have any tests of its own. When you start modifying parts of Spree code in your own app, you'll want to add unit tests that cover the extension or modification you made.
+
+### Integration Testing
+
+In the early days, Rails developers preferred fixtures and seed data. As apps grew, fixtures and seed data went out of vogue in favor of Factories. Factories can have their own problems, but at this point are widely considered superior to a large fixture/seed data setup. This [blog post](https://semaphoreci.com/blog/2014/01/14/rails-testing-antipatterns-fixtures-and-factories.html) discusses some background consideration.
+
+Below are some examples for how to create a test suite using Factories (with FactoryBot). As discussed above, you can copy all of the Spree Factories from the Spree core, or you can write your own Factories.
+
+We recommend a fully integration suite covering your checkout. You can also write integration tests for the Admin area, but many people put less attention into this because it is not user-facing. As with the unit tests, the most important thing to test is the modifications you make that make your Spree store different from the default Spree install.
+
+
+
+#### Testing as Someone Logged In
+
+If you're using spree_auth_devise, your app already comes with the Warden gem, which can be used to log-in a user through your test suite
+
+```ruby
+let(:user) { FactoryBot.create(:user) }
+before(:each) do
+ login_as(user, scope: :spree_user)
+end
+```
+
+This lets your Spree app behave as if this user is logged in.
+
+
+#### Testing as Someone Not Logged In
+
+For Spree 2.2 and prior, Spree keeps track of the order for a logged out user using a session variable. Here's an example that may work for you in Spree 2.2 and earlier:
+
+```ruby
+let (:order) { FactoryBot.create(:order) }
+before(:each) do
+ page.set_rack_session(order_id: order.id)
+ page.set_rack_session(access_token: order.token)
+end
+```
+
+In Spree 2.3, a signed cookie is used to keep track of the guest user's cart. In the example below, we make two stubs onto objects in Spree to fake-out the guest token (in this text example, we simply set it to 'xyz'). In the example, presume that the order factory will have a lineitem in it for an associated product called "Some Product":
+
+```ruby
+describe "cart to registration page", type: :feature do
+ let(:order) { FactoryBot.create(:order, guest_token: "xyz") }
+ # user should be nil for logged out user
+
+
+ describe "as someone not logged in" do
+ before(:each) do
+ order
+ SecureRandom.stub!(:urlsafe_base64)
+ .with(any_args)
+ .and_return("xyz")
+
+ Spree::OrdersController.any_instance
+ .stub(:cookies)
+ .and_return(mock(:cookies, signed: {guest_token: "xyz"}))
+ end
+
+ it "should let me load the shopping cart page" do
+ visit '/cart'
+ page.status_code.should eq(200)
+ expect(page).to have_content 'Some Product'
+ end
+end
+```
+
+$$$
+TODO: We still are looking for a way to set a signed cookie on a RackTest driver. If you get that working, you can remove the stub on the Spree::OrdersController in the code example above.
+
+Thomas Walpole thinks code like this might work, but we have yet to get this working correctly:
+
+```ruby
+kg = ActiveSupport::KeyGenerator.new(Rails.application.secrets.secret_key_base, iterations:1000)
+guest_token_cookie_value_to_set = ActiveSupport::MessageVerifier.new(kg.generate_key("signed cookie"), digest: 'SHA1', serializer: ActiveSupport::MessageEncryptor::NullSerializer).generate("\"#{guest_token}\"")
+```
+$$$
diff --git a/guides/src/content/developer/upgrades/one-dot-oh-to-one-dot-one.md b/guides/src/content/developer/upgrades/one-dot-oh-to-one-dot-one.md
new file mode 100644
index 00000000000..8f8af56b5cf
--- /dev/null
+++ b/guides/src/content/developer/upgrades/one-dot-oh-to-one-dot-one.md
@@ -0,0 +1,73 @@
+---
+title: Upgrading Spree from 1.0.x to 1.1.x
+section: upgrades
+hidden: false
+order: 14
+---
+
+## Overview
+
+This guide covers upgrading a 1.0.x Spree store, to a 1.1.x store. This
+guide has been written from the perspective of a blank Spree 1.0.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 1.1.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 1-1-stable branch.
+
+## Upgrade Rails
+
+Spree 1.1 depends on any Rails 3.2 release afer Rails 3.2.9. Ensure that you have that dependency specified in your Gemfile:
+
+````ruby
+gem 'rails', '~> 3.2.9'```
+
+Along with this, you may have to also update your assets group in the Gemfile:
+
+```ruby
+group :assets do
+ gem 'sass-rails', '~> 3.2.5'
+ gem 'coffee-rails', '~> 3.2.1'
+ gem 'uglifier', '>= 1.0.3'
+end
+
+gem 'jquery-rails', '2.1.4'
+````
+
+For more information, please refer to the [Upgrading Ruby on Rails guide](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-3-1-to-rails-3-2).
+
+## Upgrade Spree
+
+For best results, use the 1-1-stable branch from GitHub:
+
+````ruby
+gem 'spree', github: 'spree/spree', branch: '1-1-stable'```
+
+Run `bundle update spree`.
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+## Remove references to spree_api assets
+
+Spree API no longer provides any asset files, so references to these must be removed from:
+
+* app/assets/stylesheets/store/all.css
+* app/assets/stylesheets/admin/all.css
+* app/assets/javascripts/store/all.js
+* app/assets/javascripts/admin/all.js
+
+## Read the release notes
+
+For information about what has changed in this release, please read the [1.1.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_1_1_0.html).
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/one-dot-one-to-one-dot-two.md b/guides/src/content/developer/upgrades/one-dot-one-to-one-dot-two.md
new file mode 100644
index 00000000000..35f8b5a8c11
--- /dev/null
+++ b/guides/src/content/developer/upgrades/one-dot-one-to-one-dot-two.md
@@ -0,0 +1,60 @@
+---
+title: Upgrading Spree from 1.1.x to 1.2.x
+section: upgrades
+order: 13
+---
+
+## Overview
+
+This guide covers upgrading a 1.1.x Spree store, to a 1.2.x store. This
+guide has been written from the perspective of a blank Spree 1.1.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 1.2.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 1-2-stable branch.
+
+## Upgrade Spree
+
+For best results, use the 1-2-stable branch from GitHub:
+
+````ruby
+gem 'spree', github: 'spree/spree', branch: '1-2-stable'```
+
+Run `bundle update spree`.
+
+## Authentication dependency
+
+In this release, the `spree_auth` component was moved out of the main set of
+gems into an extension, called `spree_auth_devise`. If you want to continue using Spree's authentication, then you will need to specify this extension as a dependency in your `Gemfile`:
+
+```ruby
+gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '1-2-stable'```
+
+Run `bundle install` to install this extension.
+
+### Rename current_user to current_spree_user
+
+To ensure that Spree does not conflict with any authentication provided by the application, Spree has renamed its `current_user` variable to `current_spree_user`. You should make this change wherever necessary within your application.
+
+Similar to this, any references to `@user` are now `@spree_user`.
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+This may copy over additional migrations from spree_auth_devise and run them as well.
+
+## Read the release notes
+
+For information about changes contained with this release, please read the [1.2.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_1_2_0.html).
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/one-dot-three-to-two-dot-oh.md b/guides/src/content/developer/upgrades/one-dot-three-to-two-dot-oh.md
new file mode 100644
index 00000000000..1db4386cfcd
--- /dev/null
+++ b/guides/src/content/developer/upgrades/one-dot-three-to-two-dot-oh.md
@@ -0,0 +1,69 @@
+---
+title: Upgrading Spree from 1.3.x to 2.0.x
+section: upgrades
+order: 11
+---
+
+## Overview
+
+This guide covers upgrading a 1.3.x Spree store, to a 2.0.x store. This
+guide has been written from the perspective of a blank Spree 1.3.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 2.0.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 2-0-stable branch.
+
+Given that this is a major release, you may want to read through the [2.0.0 release notes](http://guides.spreecommerce.org/release_notes/spree_2_0_0.html) to see what has changed before proceeding with this upgrade.
+
+## Upgrade Spree
+
+For best results, use the 2-0-stable branch from GitHub:
+
+````ruby
+gem 'spree', github: 'spree/spree', branch: '2-0-stable'```
+
+Run `bundle update spree`.
+
+## Bump jquery-rails
+
+This version of Spree bumps the dependency for jquery-rails to this:
+
+```ruby
+gem 'jquery-rails', '3.0.0'```
+
+Ensure that you have a line such as this in your Gemfile to allow that dependency.
+
+## Remove middleware
+
+The two pieces of middleware previously inserted into `config/application.rb` have now been deprecated. Remove these two lines:
+
+```ruby
+config.middleware.use "Spree::Core::Middleware::RedirectLegacyProductUrl"
+config.middleware.use "Spree::Core::Middleware::SeoAssist"```
+
+## Rename assets
+
+In Spree 2, assets have been renamed.
+
+In `store/all.css` and `store/all.js`, you will need to rename the references from `spree_core` to `spree_frontend`. Similarly to this, in `admin/all.css` and `admin/all.js`, you will need to rename the references from `spree_core` to `spree_backend`.
+
+Additionally, remove references to `spree_promo` from these files. That component of Spree has now been merged with the Core component.
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+## Read the release notes
+
+For information about changes contained with this release, please read the [2.0.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_2_0_0.html).
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/one-dot-two-to-one-dot-three.md b/guides/src/content/developer/upgrades/one-dot-two-to-one-dot-three.md
new file mode 100644
index 00000000000..663a409ee63
--- /dev/null
+++ b/guides/src/content/developer/upgrades/one-dot-two-to-one-dot-three.md
@@ -0,0 +1,91 @@
+---
+title: Upgrading Spree from 1.2.x to 1.3.x
+section: upgrades
+order: 12
+---
+
+## Overview
+
+This guide covers upgrading a 1.2.x Spree store, to a 1.3.x store. This
+guide has been written from the perspective of a blank Spree 1.2.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 1.3.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 1-3-stable branch.
+
+## Upgrade Spree
+
+For best results, use the 1-3-stable branch from GitHub:
+
+````ruby
+gem 'spree', github: 'spree/spree', branch: '1-3-stable'```
+
+Run `bundle update spree`.
+
+## Bump jquery-rails
+
+This version of Spree bumps the dependency for jquery-rails to this:
+
+```ruby
+gem 'jquery-rails', '2.2.0'```
+
+Ensure that you have a line such as this in your Gemfile to allow that dependency.
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+## Replace money usages
+
+In older versions of Spree, we had a helper method called `money` which
+occasionally formatted money amounts incorrectly. Specifically, if the `I18n.locale` was changed, currencies started to display in that amount, rather than the proper amount. An item that was once $100, would suddenly become 100Â¥ if the locale was switched to Japanese, for instance.
+
+In Spree 1.3, money handling
+has been reworked by a major contribution by the [Free Running
+Technologies](http://www.freerunningtech.com/) team. See [#2197](https://github.com/spree/spree/pull/2197) for details.
+
+Prices are now stored in a separate table, called `spree_prices`. This table tracks the variant, the price amount, and the currency. This allows for variants to have different prices in different currencies.
+
+Along with this, we introduced the `Spree::Money` class which is used to display amounts correctly. Where previously Spree would have done this:
+
+```erb
+
<%%= money adjustment.amount %>
```
+
+We now use this:
+
+```erb
+
<%%= adjustment.display_amount.to_html %>
```
+
+Alternatively, you can use `Spree::Money.new(amount)` to get a `Spree::Money` representation. Calling `to_html` on that object will format it neatly for HTML views, and calling `to_s` will format it nicely everywhere else.
+
+### Variant.active scope
+
+Along with these changes, the `Spree::Variant.active` scope now takes an argument for the currency. Whatever currency is specified will return variants in that currency. Previously it may have been enough to just do this:
+
+```ruby
+@product.variants.active```
+
+But now you must specify a currency:
+
+```ruby
+@product.variants.active("USD")```
+
+Or you can rely on the current currency within views:
+
+```ruby
+@product.variants.active(current_currency)```
+
+## Read the release notes
+
+For information about changes contained with this release, please read the [1.3.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_1_3_0.html).
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/point-seventy-to-one-dot-oh.md b/guides/src/content/developer/upgrades/point-seventy-to-one-dot-oh.md
new file mode 100644
index 00000000000..bd1a6eaa5e7
--- /dev/null
+++ b/guides/src/content/developer/upgrades/point-seventy-to-one-dot-oh.md
@@ -0,0 +1,90 @@
+---
+title: Upgrading Spree from 0.70.x to 1.0.x
+section: upgrades
+order: 15
+---
+
+## Overview
+
+This guide covers upgrading a 0.70.x Spree store, to a 1.0.x store. This
+guide has been written from the perspective of a blank Spree 0.70.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 1.0.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 1-0-stable branch.
+
+Worth noting here is that Spree 1.0 was the first release to properly use the
+features of Rails engines. This means that Spree needs to be mounted manually
+within the `config/routes.rb` file of the application, and that the classes
+such as `Product` and `Variant` from Spree are now namespaced within a module,
+so that they are now `Spree::Product` and `Spree::Variant`. Tables are
+similarly namespaced (i.e. `spree_products` and `spree_variants`).
+
+Along with this, migrations must be copied over to the application using the
+`rake railties:install:migrations` command, rather than a `rails g spree:site`
+command as before.
+
+## Upgrade Rails
+
+Spree 1.0 depends on any Rails 3.1 release afer Rails 3.1.10. Ensure that you have that dependency specified in your Gemfile:
+
+````ruby
+gem 'rails', '~> 3.1.10'
+
+## Upgrade Spree
+
+For best results, use the 1-0-stable branch from GitHub:
+
+```ruby
+gem 'spree', github: 'spree/spree', branch: '1-0-stable'```
+
+Run `bundle update spree`.
+
+## Rename middleware classes
+
+In `config/application.rb`, there are two pieces of middleware:
+
+```ruby
+config.middleware.use "RedirectLegacyProductUrl"
+config.middleware.use "SeoAssist"```
+
+These classes are now namespaced within Spree:
+
+```ruby
+config.middleware.use "Spree::Core::Middleware::RedirectLegacyProductUrl"
+config.middleware.use "Spree::Core::Middleware::SeoAssist"```
+
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+## Mount the Spree engine
+
+Within `config/routes.rb`, you must now mount the Spree engine:
+
+```ruby
+mount Spree::Core::Engine, at: '/'```
+
+This is the standard way of adding engines to Rails applications.
+
+## Remove spree_dash assets
+
+Spree's dash component was removed as a dependency of Spree, and so references
+to its assets must be removed also. Remove references to spree_dash from:
+
+* app/assets/stylesheets/store/all.css
+* app/assets/javascripts/store/all.js
+* app/assets/stylesheets/admin/all.css
+* app/assets/javascripts/admin/all.js
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/point-sixty-to-point-seventy.md b/guides/src/content/developer/upgrades/point-sixty-to-point-seventy.md
new file mode 100644
index 00000000000..bef4567ab40
--- /dev/null
+++ b/guides/src/content/developer/upgrades/point-sixty-to-point-seventy.md
@@ -0,0 +1,93 @@
+---
+title: Upgrading Spree from 0.60.x to 0.70.x
+section: upgrades
+order: 16
+---
+
+## Overview
+
+This guide covers upgrading a 0.60.x Spree store, to a 0.70.x store. This
+guide has been written from the perspective of a blank Spree 0.60.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 0.70.x store once this
+upgrade is complete.
+
+## Upgrade Rails
+
+Spree 0.60.x depends on Rails 3.0.12, whereas Spree 0.70.x depends on any Rails
+version from 3.1.1 up to 3.1.4. The first step in upgrading Spree is to
+upgrade the Rails version in the `Gemfile`:
+
+````ruby
+gem 'rails', '3.1.12'```
+
+For more information, please read the [Upgrading Ruby on Rails Guide](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-3-0-to-rails-3-1).
+
+## Upgrade Spree
+
+For best results, use the 0-70-stable branch from GitHub:
+
+```ruby
+gem 'spree', github: 'spree/spree', branch: '0-70-stable'```
+
+Run `bundle update rails` and `bundle update spree` and verify that was successful.
+
+## Remove debug_rjs configuration
+
+In `config/environments/development.rb`, remove this line:
+
+```ruby
+config.action_view.debug_rjs = true```
+
+## Remove lib/spree_site.rb
+
+This file is no longer used in 0.70.x versions of Spree.
+
+## Set up new data
+
+To migrate the data across, use these commands:
+
+```bash
+rails g spree:site
+rake db:migrate```
+
+## The Asset Pipline
+
+With the upgrade to Rails 3.1 comes the [asset pipeline](http://guides.rubyonrails.org/asset_pipeline.html). You need to add these gems to your Gemfile in order to support Spree's assets being served:
+
+```ruby
+group :assets do
+ gem 'sass-rails', '~> 3.1.5'
+ gem 'coffee-rails', '~> 3.1.1'
+
+ # See https://github.com/sstephenson/execjs#readme for more supported runtimes
+ # gem 'therubyracer'
+
+ gem 'uglifier', '>= 1.0.3'
+end
+
+gem 'jquery-rails', '2.2.1'```
+
+Along with these gems, you will need to enable assets within the class definition inside `config/application.rb`:
+
+```ruby
+module YourStore
+ class Application < Rails::Application
+
+ # ...
+
+ # Enable the asset pipeline
+ config.assets.enabled = true
+
+ # Version of your assets, change this if you want to expire all your assets
+ config.assets.version = '1.0'
+
+ end
+end```
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/three-dot-five-to-three-dot-six.md b/guides/src/content/developer/upgrades/three-dot-five-to-three-dot-six.md
new file mode 100644
index 00000000000..838575dcbc6
--- /dev/null
+++ b/guides/src/content/developer/upgrades/three-dot-five-to-three-dot-six.md
@@ -0,0 +1,54 @@
+---
+title: Upgrading Spree from 3.5.x to 3.6.x
+section: upgrades
+order: 1
+---
+
+This guide covers upgrading a 3.5 Spree application, to a 3.6 application.
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.6.1'
+gem 'spree_auth_devise', '~> 3.3'
+gem 'spree_gateway', '~> 3.3'
+```
+
+### Update your Rails version to 5.2
+
+Please follow the
+[official Rails guide](http://guides.rubyonrails.org/5_2_release_notes.html#upgrading-to-rails-5-2)
+to upgrade your store.
+
+### Run `bundle update`
+
+### Migrate to ActiveStorage (optional)
+
+Please follow the [official paperclip guide](https://github.com/thoughtbot/paperclip/blob/master/MIGRATING.md) if you
+want to use ActiveStorage instead of paperclip.
+
+You cann still use paperclip for attachment management by setting `SPREE_USE_PAPERCLIP` environment variable to `true`, but keep in mind that paperclip is DEPRECATED and we will remove paperclip support in Spree 4.0.
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+You're good to go!
+
+## Read the release notes
+
+For information about changes contained within this release, please read the [3.6.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_3_6_0.html).
+
+## Verify that everything is OK
+
+Run you test suite, click around in your application and make sure it's performing as normal. Fix any deprecation warnings you see.
diff --git a/guides/src/content/developer/upgrades/three-dot-four-to-three-dot-five.md b/guides/src/content/developer/upgrades/three-dot-four-to-three-dot-five.md
new file mode 100644
index 00000000000..83551f6aa0a
--- /dev/null
+++ b/guides/src/content/developer/upgrades/three-dot-four-to-three-dot-five.md
@@ -0,0 +1,64 @@
+---
+title: Upgrading Spree from 3.4.x to 3.5.x
+section: upgrades
+order: 2
+---
+
+This guide covers upgrading a 3.4 Spree store, to a 3.5 store.
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.5.0'
+gem 'spree_auth_devise', '~> 3.3'
+gem 'spree_gateway', '~> 3.3'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+### Install Spree Analytics Trackers extension
+
+If you were previously using Analytics Trackers feature you need to install it as an extension
+as it was [extracted from the core](https://github.com/spree/spree/pull/8408).
+
+1. Add [Spree Analytics Trackers](https://github.com/spree-contrib/spree_analytics_trackers) to your `Gemfile`:
+
+```ruby
+gem 'spree_analytics_trackers', github: 'spree-contrib/spree_analytics_trackers'
+```
+
+2. Install the gem using Bundler:
+
+```bash
+bundle install
+```
+
+3. Copy and run migrations:
+
+```bash
+bundle exec rails g spree_analytics_trackers:install
+```
+
+You're good to go!
+
+## Read the release notes
+
+For information about changes contained within this release, please read the [3.5.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_3_5_0.html).
+
+## Verify that everything is OK
+
+Run you test suite, click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
diff --git a/guides/src/content/developer/upgrades/three-dot-oh-to-three-dot-one.md b/guides/src/content/developer/upgrades/three-dot-oh-to-three-dot-one.md
new file mode 100644
index 00000000000..aba4f63bc37
--- /dev/null
+++ b/guides/src/content/developer/upgrades/three-dot-oh-to-three-dot-one.md
@@ -0,0 +1,85 @@
+---
+title: Upgrading Spree from 3.0.x to 3.1.x
+section: upgrades
+order: 6
+---
+
+This guide covers upgrading a 3.0.x Spree store, to a 3.1.x store. This
+guide has been written from the perspective of a blank Spree 3.0.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 3.1.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 3-1-stable branch.
+
+## Upgrade Rails
+
+For this Spree release, you will need to upgrade your Rails version to at least 4.2.6.
+
+```ruby
+gem 'rails', '~> 4.2.6'
+```
+
+## Upgrade Spree
+
+For best results, use the spree gem in version 3.1.x:
+
+```ruby
+gem 'spree', '~> 3.1.0.rc1'
+```
+
+Run `bundle update spree`.
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+## Spree Auth Devise & Spree Gateway
+
+If you are using Spree Gateway and/or Spree Auth Devise you should also upgrade them:
+
+```ruby
+gem 'spree_auth_devise', '~> 3.1.0.rc1'
+gem 'spree_gateway', '~> 3.1.0.rc1'
+```
+
+For Spree Auth Devise run installer:
+
+ rails g spree:auth:install
+
+(you don't have to override config/initializers/devise.rb)
+
+## Additional information
+
+### Make sure to v1 namespace custom rabl templates & overrides.
+
+If your rabl templates reference others with extend you'll need to add the v1 namespace.
+
+For example:
+
+```ruby
+extends 'spree/api/zones/show'
+```
+
+Becomes:
+
+```ruby
+extends 'spree/api/v1/zones/show'
+```
+
+### Remove Spree::Config.check_for_spree_alerts
+
+If you were disabling the alert checks you'll now want to remove this preference as it's no longer used.
+
+## Read the release notes
+
+For information about changes contained within this release, please read the [3.1.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_3_1_0.html).
+
+## Verify that everything is OK
+
+Run you test suite, click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
diff --git a/guides/src/content/developer/upgrades/three-dot-one-to-three-dot-two.md b/guides/src/content/developer/upgrades/three-dot-one-to-three-dot-two.md
new file mode 100644
index 00000000000..7242a2f84bc
--- /dev/null
+++ b/guides/src/content/developer/upgrades/three-dot-one-to-three-dot-two.md
@@ -0,0 +1,64 @@
+---
+title: Upgrading Spree from 3.1.x to 3.2.x
+section: upgrades
+order: 5
+---
+
+This guide covers upgrading a 3.1.x Spree store, to a 3.2.x store.
+
+### Update your Rails version to 5.0
+
+Please follow the
+[official Rails guide](http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-4-2-to-rails-5-0)
+to upgrade your store.
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.2.0'
+gem 'spree_auth_devise', '~> 3.2.0'
+gem 'spree_gateway', '~> 3.2.0'
+```
+
+### Update your extensions
+
+We're changing how extensions dependencies work. Previously you had to match
+extension branch to Spree branch. Starting from now `master` branch of all
+`spree-contrib` extensions should work with Spree >= `3.1` and < `4.0`. Please change
+your extensions in Gemfile eg.:
+
+from:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero', branch: '3-1-stable'
+```
+
+to:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+## Read the release notes
+
+For information about changes contained within this release, please read the [3.2.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_3_2_0.html).
+
+## Verify that everything is OK
+
+Run you test suite, click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
diff --git a/guides/src/content/developer/upgrades/three-dot-six-to-three-dot-seven.md b/guides/src/content/developer/upgrades/three-dot-six-to-three-dot-seven.md
new file mode 100644
index 00000000000..d0b99339170
--- /dev/null
+++ b/guides/src/content/developer/upgrades/three-dot-six-to-three-dot-seven.md
@@ -0,0 +1,73 @@
+---
+title: Upgrading Spree from 3.6 to 3.7
+section: upgrades
+order: 0
+---
+
+This guide covers upgrading a 3.6 Spree application, to version 3.7.
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.7.0.rc1'
+gem 'spree_auth_devise', '~> 3.4'
+gem 'spree_gateway', '~> 3.4'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_api:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+### Migrate Taxon icons to Spree Assets
+
+We renamed `TaxonIcon` to `TaxonImage` to clarify usage of this model.
+If you were using `TaxonIcon` please run this to migrate your icons to images:
+
+```bash
+rails db:migrate_taxon_icons_to_images
+```
+
+### Ensure all Orders associated to Store
+
+Orders needs to be associated to Stores.
+To ensure all existing `Order` are associated with `Store` please run this:
+
+```bash
+rails db:associate_orders_with_store
+```
+
+This will associate all Orders without Store to the default Store.
+This can take some time depending on your volume of data.
+
+### Ensure all Orders have currency present
+
+To enhance multi currency capabilities we've made `currency` presence
+obligatory in `Order` model. To ensure all existing `Orders` have `currency`
+present please run this command:
+
+```bash
+rails db:ensure_order_currency_presence
+```
+
+This will set `currency` in Orders without currency set to `Spree::Config[:default_currency]` value. This can take some time depending on your volume of data.
+
+### Replace `guest_token` with `token` in your codebase
+
+`Order#guest_token` was renamed to `Order#token` in order to unify the experience for guest checkouts and orders placed by signed in users.
+
+### Read the release notes
+
+For information about changes contained within this release, please read the [3.7.0 Release Notes](https://guides.spreecommerce.org/release_notes/spree_3_7_0.html).
diff --git a/guides/src/content/developer/upgrades/three-dot-three-to-three-dot-four.md b/guides/src/content/developer/upgrades/three-dot-three-to-three-dot-four.md
new file mode 100644
index 00000000000..f0b4be4d0d1
--- /dev/null
+++ b/guides/src/content/developer/upgrades/three-dot-three-to-three-dot-four.md
@@ -0,0 +1,49 @@
+---
+title: Upgrading Spree from 3.3.x to 3.4.x
+section: upgrades
+order: 3
+---
+
+This guide covers upgrading a 3.3 Spree store, to a 3.4 store.
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.4.0'
+gem 'spree_auth_devise', '~> 3.3'
+gem 'spree_gateway', '~> 3.3'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+### Migrate Spree::Taxon icons to Spree Assets
+
+We changed `Spree::Taxon` icon to use `Spree::Asset` to unify attachment usage
+across all Spree models. If you were using icon images in `Spree::Taxon`
+please run this to migrate your icons:
+
+```bash
+rails db:migrate_taxon_icons
+```
+
+## Read the release notes
+
+For information about changes contained within this release, please read the [3.4.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_3_4_0.html).
+
+## Verify that everything is OK
+
+Run you test suite, click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
diff --git a/guides/src/content/developer/upgrades/three-dot-two-to-three-dot-three.md b/guides/src/content/developer/upgrades/three-dot-two-to-three-dot-three.md
new file mode 100644
index 00000000000..8f5285be872
--- /dev/null
+++ b/guides/src/content/developer/upgrades/three-dot-two-to-three-dot-three.md
@@ -0,0 +1,106 @@
+---
+title: Upgrading Spree from 3.2.x to 3.3.x
+section: upgrades
+order: 4
+---
+
+This guide covers upgrading a 3.2.x Spree store, to a 3.3.x store.
+
+### Update your Rails version to 5.1
+
+Please follow the
+[official Rails guide](http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-5-0-to-rails-5-1)
+to upgrade your store.
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.3.0'
+gem 'spree_auth_devise', '~> 3.3'
+gem 'spree_gateway', '~> 3.3'
+```
+
+### Update your extensions
+
+We're changing how extensions dependencies work. Previously you had to match
+extension branch to Spree branch. Starting from Spree 3.2 release date `master` branch of all
+`spree-contrib` extensions should work with Spree >= `3.1` and < `4.0`. Please change
+your extensions in Gemfile eg.:
+
+from:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero', branch: '3-1-stable'
+```
+
+to:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+### Include `UserMethods` in your `User` class
+
+With this release we're not including this automatically. You need to do it manually if you're not using `spree_auth_devise`.
+
+You need to include `Spree::UserMethods` in your user class, eg.
+
+```ruby
+class User
+ include UserAddress
+ include UserMethods
+ include UserPaymentSource
+end
+```
+
+### Update `aws-sdk` gem to `>= 2.0`
+
+Spree 3.3 comes with paperclip 5.1 support so if you're using Amazon S3 storage you need to change in your Gemfile, from:
+
+```ruby
+gem 'aws-sdk', '< 2.0'
+```
+
+to:
+
+```ruby
+gem 'aws-sdk', '>= 2.0'
+```
+
+and run `bundle update aws-sdk`
+
+In your paperclip configuration you also need to specify
+`s3_region` attribute eg. https://github.com/spree/spree/blame/master/guides/content/developer/customization/s3_storage.md#L27
+
+Seel also [RubyThursday episode](https://rubythursday.com/episodes/ruby-snack-27-upgrade-paperclip-and-aws-sdk-in-prep-for-rails-5) walkthrough of upgrading paperclip in your project.
+
+### Add jquery.validate to your project if you've used it directly from Spree
+
+If your application.js file includes line
+`//= require jquery.validate/jquery.validate.min`
+you will need to add it this file manually to your project because this library was
+[removed from Spree in favour of native HTML5 validation](https://github.com/spree/spree/pull/8173).
+
+## Read the release notes
+
+For information about changes contained within this release, please read the [3.3.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_3_3_0.html).
+
+## Verify that everything is OK
+
+Run you test suite, click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
diff --git a/guides/src/content/developer/upgrades/two-dot-oh-to-two-dot-one.md b/guides/src/content/developer/upgrades/two-dot-oh-to-two-dot-one.md
new file mode 100644
index 00000000000..e72f5f65005
--- /dev/null
+++ b/guides/src/content/developer/upgrades/two-dot-oh-to-two-dot-one.md
@@ -0,0 +1,57 @@
+---
+title: Upgrading Spree from 2.0.x to 2.1.x
+section: upgrades
+order: 10
+---
+
+## Overview
+
+This guide covers upgrading a 2.0.x Spree store, to a 2.1.x store. This
+guide has been written from the perspective of a blank Spree 2.0.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 2.1.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 2-1-stable branch.
+
+This is the first Spree release which supports Rails 4 exclusively. Spree
+releases after this point will continue to support Rails 4 only.
+
+## Upgrade Rails
+
+For this Spree release, you will need to upgrade your Rails version to at least 4.0.0.
+
+It is recommended to read through the [Upgrading Ruby on Rails
+guide](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-
+from-rails-3-2-to-rails-4-0) to learn what needs to be done for your
+application to migrate to Rails 4.
+
+````ruby
+gem 'rails', '~> 4.0.0'```
+
+## Upgrade Spree
+
+For best results, use the 2-1-stable branch from GitHub:
+
+```ruby
+gem 'spree', github: 'spree/spree', branch: '2-1-stable'```
+
+Run `bundle update spree`.
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+## Read the release notes
+
+For information about changes contained with this release, please read the [2.1.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_2_1_0.html).
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/two-dot-one-to-two-dot-two.md b/guides/src/content/developer/upgrades/two-dot-one-to-two-dot-two.md
new file mode 100644
index 00000000000..d459342338b
--- /dev/null
+++ b/guides/src/content/developer/upgrades/two-dot-one-to-two-dot-two.md
@@ -0,0 +1,63 @@
+---
+title: Upgrading Spree from 2.1.x to 2.2.x
+section: upgrades
+order: 9
+---
+
+## Overview
+
+This guide covers upgrading a 2.1.x Spree store, to a 2.2.x store. This
+guide has been written from the perspective of a blank Spree 2.1.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 2.2.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 2-2-stable branch.
+
+## Upgrade Rails
+
+For this Spree release, you will need to upgrade your Rails version to at least 4.0.6.
+
+```ruby
+gem 'rails', '~> 4.0.6'
+```
+
+## Upgrade Spree
+
+For best results, use the 2-2-stable branch from GitHub:
+
+````ruby
+gem 'spree', github: 'spree/spree', branch: '2-2-stable'```
+
+Run `bundle update spree`.
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+## Read the release notes
+
+For information about changes contained with this release, please read the [2.2.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_2_2_0.html).
+
+### Rename assets
+
+As mentioned in the release notes, asset paths have changed. Change the references on the left, to the ones on the right:
+
+* `admin/spree_backend` => `spree/backend`
+* `store/spree_frontend` => `spree/frontend`
+
+This applies across the board on Spree, and may need to be done in your store's extensions.
+
+### Paperclip settings have been removed from master
+
+Please consult [this section](http://guides.spreecommerce.org/release_notes/spree_2_2_0.html#paperclip-settings-have-been-removed) of the release notes if you were using custom Paperclip settings. This will direct you what to do in that particular case.
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/two-dot-three-to-two-dot-four.md b/guides/src/content/developer/upgrades/two-dot-three-to-two-dot-four.md
new file mode 100644
index 00000000000..bf73602547d
--- /dev/null
+++ b/guides/src/content/developer/upgrades/two-dot-three-to-two-dot-four.md
@@ -0,0 +1,48 @@
+---
+title: Upgrading Spree from 2.3.x to 2.4.x
+section: upgrades
+order: 7
+---
+
+This guide covers upgrading a 2.3.x Spree store, to a 2.4.x store. This
+guide has been written from the perspective of a blank Spree 2.3.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 2.4.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 2-4-stable branch.
+
+## Upgrade Rails
+
+For this Spree release, you will need to upgrade your Rails version to at least 4.1.8.
+
+```ruby
+gem 'rails', '~> 4.1.8'
+```
+
+## Upgrade Spree
+
+For best results, use the 2-4-stable branch from GitHub:
+
+````ruby
+gem 'spree', github: 'spree/spree', branch: '2-4-stable'```
+
+Run `bundle update spree`.
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+## Read the release notes
+
+For information about changes contained within this release, please read the [2.4.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_2_4_0.html).
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/two-dot-two-to-two-dot-three.md b/guides/src/content/developer/upgrades/two-dot-two-to-two-dot-three.md
new file mode 100644
index 00000000000..f0519d93ac3
--- /dev/null
+++ b/guides/src/content/developer/upgrades/two-dot-two-to-two-dot-three.md
@@ -0,0 +1,52 @@
+---
+title: Upgrading Spree from 2.2.x to 2.3.x
+section: upgrades
+order: 8
+---
+
+## Overview
+
+This guide covers upgrading a 2.2.x Spree store, to a 2.3.x store. This
+guide has been written from the perspective of a blank Spree 2.2.x store with
+no extensions.
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 2.3.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 2-3-stable branch.
+
+This is the first Spree release which supports Rails 4.1.
+
+## Upgrade Rails
+
+For this Spree release, you will need to upgrade your Rails version to at least 4.1.2.
+
+```ruby
+gem 'rails', '~> 4.1.2'
+```
+
+## Upgrade Spree
+
+For best results, use the 2-3-stable branch from GitHub:
+
+````ruby
+gem 'spree', github: 'spree/spree', branch: '2-3-stable'```
+
+Run `bundle update spree`.
+
+## Copy and run migrations
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+## Read the release notes
+
+For information about changes contained with this release, please read the [2.3.0 Release Notes](http://guides.spreecommerce.org/release_notes/spree_2_3_0.html).
+
+## Verify that everything is OK
+
+Click around in your store and make sure it's performing as normal. Fix any deprecation warnings you see.
+````
diff --git a/guides/src/content/developer/upgrades/upgrade_guides.md b/guides/src/content/developer/upgrades/upgrade_guides.md
new file mode 100644
index 00000000000..d8b6e994e81
--- /dev/null
+++ b/guides/src/content/developer/upgrades/upgrade_guides.md
@@ -0,0 +1,27 @@
+---
+title: "Upgrade Guides"
+section: upgrades
+---
+
+## Upgrade Guides
+
+We strongly advise upgrading Spree incrementally, rather than in one big go. For example, if you're upgrading a Spree store from 0.60.x to 2.4.x, you would read through all of these guides, one by one.
+
+If there are any issues with these guides, please let us know by [filing an issue](https://github.com/spree/spree/issues/new).
+
+* [3.5.x to 3.6.x](/developer/three-dot-five-to-three-dot-six)
+* [3.4.x to 3.5.x](/developer/three-dot-four-to-three-dot-five)
+* [3.3.x to 3.4.x](/developer/three-dot-three-to-three-dot-four)
+* [3.2.x to 3.3.x](/developer/three-dot-two-to-three-dot-three)
+* [3.1.x to 3.2.x](/developer/three-dot-one-to-three-dot-two)
+* [3.0.x to 3.1.x](/developer/three-dot-oh-to-three-dot-one)
+* [2.3.x to 2.4.x](/developer/two-dot-three-to-two-dot-four)
+* [2.2.x to 2.3.x](/developer/two-dot-two-to-two-dot-three)
+* [2.1.x to 2.2.x](/developer/two-dot-one-to-two-dot-two)
+* [2.0.x to 2.1.x](/developer/two-dot-oh-to-two-dot-one)
+* [1.3.x to 2.0.x](/developer/one-dot-three-to-two-dot-oh)
+* [1.2.x to 1.3.x](/developer/one-dot-two-to-one-dot-three)
+* [1.1.x to 1.2.x](/developer/one-dot-one-to-one-dot-two)
+* [1.0.x to 1.1.x](/developer/one-dot-oh-to-one-dot-one)
+* [0.70.x to 1.0.x](/developer/point-seventy-to-one-dot-oh)
+* [0.60.x to 0.70.x](/developer/point-sixty-to-point-seventy)
diff --git a/guides/src/content/release_notes/0_10_0.md b/guides/src/content/release_notes/0_10_0.md
new file mode 100644
index 00000000000..0673e7f5c1e
--- /dev/null
+++ b/guides/src/content/release_notes/0_10_0.md
@@ -0,0 +1,449 @@
+---
+title: Spree 0.10.0
+section: release_notes
+order: 24
+---
+
+# Upgrade Notes
+
+## General upgrade process
+
+### Back up your database and code
+
+Always advisable!
+
+### Perform the standard upgrade command
+
+```bash
+spree —update
+```
+
+### Remove obsolete initializers
+
+```bash
+rm -rf config/initializers/compass.rb
+```
+
+h5. Remove defunct contents of public dirs
+
+````bash
+rm -rf public/javascripts/
+rm -rf public/stylesheets/
+rm -rf public/images/```
+
+### Take note of existing payment gateway settings
+
+The new payment gateway implementation will remove existing settings,
+and these need to be added again using the new interface.
+
+### Run the migrations
+```bash
+rake db:migrate```
+
+### Configure a payment method
+
+See the [additional
+information](#improvementsto-payment-gateway-configuration) later in
+the
+release notes.
+
+### Deprecation Warnings
+
+The newer version of Rails used by Spree generates a lot of deprecation
+warnings. You may see the following message in your console:
+
+```bash
+DEPRECATION: require "activerecord" is deprecated and will be removed
+in Rails 3. Use require "active_record" instead.
+````
+
+Remove all instances of `require 'activerecord'` from your Rakefiles
+
+#### API Changes
+
+##### Change to `taxonomies` variable
+
+`Taxonomies` used to be set in most shared views. Now, it is only set
+after calling `get_taxonomies` (inherited from Spree's base
+controller).
+
+### Spree Base Controller and Layouts
+
+`Spree::BaseController` inherits directly from `ActionController::Base`,
+rather than from `ApplicationController` (which itself is now an empty
+class to help interaction with other Rails apps).
+If you used `app/views/layout/application.html.erb` in an extension.
+e.g.
+
+```bash
+Spree::BaseController.class_eval { layout 'application' }
+```
+
+… then you will need to rename it to `spree_application.html.erb` and
+update the occurrences.
+
+### Adding admin tabs changed
+
+`extension_tabs` is no longer used, instead use theme "hooks":hooks.html.
+
+### Theme Support
+
+Spree now has basic support for theming. Themes in Spree are implemented as "extensions":extensions.html. All of the default views in Spree have now been abstracted into a default theme. You can override all or parts of the default theme in a new theme. Themes have their own generator and are created as follows:
+
+```bash
+script/generate theme foo
+```
+
+You can read more about themes in the very extensive [customization guide](http://guides.spreecommerce.org/legacy/0-11-x/customization_overview.html)
+
+---
+
+Don't panic if you have already customized Spree views in your site extension for a previous release of Spree. These view customizations should continue to work as long as they are loaded after the default theme extension.
+
+---
+
+### Named Scopes and Product Groups
+
+In various applications, we need to create lists of products according to various criteria, e.g. all products in taxon X, or all products costing less than $20, or composite criteria like all products costing more than $100 that have brand Y. Spree provides several so-called named scopes, which provide filtering by various aspects of the basic data, some of which can be chained or composed together for more complex filters.
+
+Named scopes can also be combined. The following chain of scopes lists the products that are active and with a price between $18 and $20.
+
+```bash
+Product.active.price_between(18,20)
+```
+
+Product groups allow for the defining and naming groups of products for various purposes in an application. For example, you can define a group called Ruby products which contains everything with Ruby in the product name, or another group called Ruby clothing which is the sub-list of the above limited to items in the Clothing taxon. Product group definitions can also take price ranges, product properties, and variant options into account. It is possible to add your own filters by defining new named scopes too.
+
+Please see the documentaiton for a more complete explanation of [named scopes and product groups](http://guides.spreecommerce.org/legacy/0-11-x/scopes_and_groups.html).
+
+### Improvements to Payment Gateway Configuration
+
+This release contains significant improvements to how payment gateways are configured. Gateways are no longer supported by database migrations, this scheme has been replaced by Active Record models that extend `Gateway`. The configuration of gateways is now done through standard Spree `preference configuration`. The [documentation](http://guides.spreecommerce.org/legacy/0-11-x/payment_gateways.html) has also been updated and contains a more detailed explanation.
+
+One major improvement is that it is now possible to configure multiple gateways for each of your Rails environments. Its also possible to use the live production server in development mode when previously, you were required to run in test mode. One unfortunate side effect of this improvement is that your existing gateway configuration information will be lost and you will need to reconfigure your gateway in the admin interface.
+
+!!!
+You should make a note of your gateway configuration setting before upgrading since you will need to reconfigure your gateway when you're done.
+!!!
+
+This approach to implementing and configuring gateways is extremely flexible. It makes it trivial to implement a new gateway that is already supported by Active Merchant. There are other useful benefits to this approach that a developer may be interested in knowing.
+
+#### Support of Non Active Merchant Gateways
+
+This architecture allows Spree to support gateways that are not officially supported by Active Merchant. Many times a new gateway is donated by someone in the community but its languishing in the queue waiting for someone to test and accept the patch. You have the option of taking that code (or writing your own from scratch) and implementing it within Spree. Instead of delegating to an Active Merchant class, you can simply implement that functionality yourself. You could also include the new gateway code from an Active Merchant fork inside your implementation and delegate the standard authorize, capture, etc operations to it.
+
+#### Ability to "Patch" Active Merchant Gateways
+
+We've noticed that sometimes it takes a while for a crucial Active Merchant patch to be applied. That's certainly understandable, the [Shopify](http://shopify.com) guys have a business to run and its probably not a high priority for them to make sure that the latest obscure gateway patch is applied in a timely fashion. Fortunately, the Spree approach to wrapping these gateways provides you with a convenient option.
+
+Lets say there is a bug with the +authorize+ method. You could simply provide an implementation of the gateway that has the patched version of the `authorize` method and then delegates to the Active Merchant class for everything else (since that works just fine.)
+
+#### Additional Functionality Beyond Active Merchant
+
+Another benefit of the architecture is that it makes it possible for Spree to provide additional common functionality that was not envisioned by Active Merchant. Specifically, it is possible to provide an abstraction for storing credit card profiles to be used with recurring payments. There's a good reason for Active Merchant to not care about this functionality. Its designed for people who just want to drop a single gateway provider into their application. Most programmers don't need three different gateways at once. Spree is a specialized use case. Its providing multiple gateways for you to choose from and so its desirable to have a standard method for operations such as this.
+
+---
+
+Recurring payments are not yet supported in Spree although there are plans to provide this in the near future.
+
+---
+
+### Multi Step Checkout
+
+#### Checkout Steps
+
+Spree has returned to a multi step checkout process. The following checkout steps are defined by default.
+
+- Registration (Optional)
+- Address Information
+- Delivery Options (Shipping Method)
+- Payment
+- Confirm
+
+There is also a default progress "train" which shows the current step and allows you to jump back to a previous step by clicking on it.
+
+!!!
+If you have a site running on a previous verison of Spree, your checkout process will likely need to be upgraded. The good news is the new approach is much easier to customize.
+!!!
+
+The checkout process is highly customizable - in fact, this is the reasoning behind moving away from the single step checkout. There is far less code hidden in javascript and each checkout step has its own partial. See the [checkout documentation](http://guides.spreecommerce.org/legacy/0-11-x/checkout.html) for mor information on how to customize the checkout.
+
+#### Countries Available for Shipping and Billing
+
+The mechanism for determining the list of billing and shipping countries has changed. Prior to this release, there was no way to limit the billing countries and shipping countries were limited by the countries included in the shipping zones that were configured. The new approach is to simply use all countries defined in the database by default.
+
+The list can be limited to a specific set of countries by configuring the new `:checkout_zone` preference and setting its value to the name of a [zone](http://guides.spreecommerce.org/legacy/0-11-x/zones.html) containing the countries you wish to use. This should handle most cases where the list of billing and shipping countries are the same. You can always customize the code via extension if this does not suit your needs.
+
+#### State Machine
+
+The Checkout model now has its own [state machine](https://github.com/pluginaweek/state_machine). This allows for easier customization of the checkout process. It is now much simpler to add or remove a step to the default checkout process. Here's an example which avoids the address step in checkout.
+
+```bash
+class SiteExtension < Spree::Extension
+ def activate
+ # customize the checkout state machine
+ Checkout.state_machines[:state] = StateMachine::Machine.new(Checkout, initial: 'payment') do
+ after_transition to: 'complete', do: :complete_order
+ before_transition to: 'complete', do: :process_payment
+ event :next do
+ transition to: 'complete', from: 'payment'
+ end
+ end
+
+ # bypass creation of address objects in the checkouts controller (prevent validation errors)
+ CheckoutsController.class_eval do
+ def object
+ return `object if `object
+ `object = parent_object.checkout
+ unless params and params[:coupon_code]
+
+`object.creditcard ||= Creditcard.new(month: Date.today.month, year: Date.today.year)
+ end
+ `object
+ end
+ end
+ end
+end
+```
+
+#### Controller Hooks
+
+The\*CheckoutController+ now provides its own "hook mechanism" (not to be
+confused with theme hooks) which allow for the developer to perform
+additional logic (or to change the default) logic that is applied during
+the edit and/or update operation for a particular step. The
+`Spree::Checkout::Hooks` module provides this additional functionality
+and makes use of methods provided by the `resource_controller` gem.
+See the [checkout documentation](http://guides.spreecommerce.org/legacy/0-11-x/checkout.html#controller-logic) for
+further details and examples.
+
+## Checkout Partials
+
+The default theme now contains several partials located within
+`vendor/extensions/theme_default/app/views/checkouts`. Each checkout
+step automatically renders the `edit.html.erb` view along with a
+corresponding partial based on the state associated with the current
+step. For example, in the delivery step the `_delivery.html.erb`
+partial is used.
+
+## Javascript
+
+Spree no longer requires javascript for checkout but the user experience
+will be slightly more pleasing if they have javascript enabled in their
+browser. Spree automatically includes the `checkout.js` file located in
+the default theme. This file can be replaced in its entirety through use
+of a site extension.
+
+## Payment Profiles
+
+The default checkout process in Spree assumes a gateway that allows for
+some form of third party support for payment profiles. An example of
+such a service would be [Authorize.net
+CIM](http://www.authorize.net/solutions/merchantsolutions/merchantservices/cim/).
+Such a service allows for a secure and PCI compliant means of storing
+the users credit card information. This allows merchants to issue
+refunds to the credit card or to make changes to an existing order
+without having to leave Spree and use the gateway provider's website.
+More importantly, it allows us to have a final "confirmation" step
+before the order is processed since the number is stored securely on the
+payment step and can still be used to perform the standard
+authorization/capture via the secure token provided by the gateway.
+
+Spree provides a wrapper around the standard active merchant API in
+order to provide a common abstraction for dealing with payment profiles.
+All `Gateway` classes now have a `payment_profiles_supported?` method
+which indicates whether or not payment profiles are supported. If you
+are adding Spree support to a `Gateway` you should also implement the
+`create_profile` method. The following is an example of the
+implementation of `create_profile` used in the `AuthorizeNetCim` class:
+
+````ruby
+# Create a new CIM customer profile ready to accept a payment
+def create_profile(creditcard, gateway_options)
+ if creditcard.gateway_customer_profile_id.nil?
+ profile_hash = create_customer_profile(creditcard,
+gateway_options)
+ creditcard.update_attributes(:gateway_customer_profile_id =\>
+profile_hash[:customer_profile_id], :gateway_payment_profile_id
+=\> profile_hash[:customer_payment_profile_id])
+ end
+end```
+
+!!!
+Most gateways do not yet support payment profiles but the
+default checkout process of Spree assumes that you have selected a
+gateway that supports this feature. This allows users to enter credit
+card information during the checkout withou having to store it in the
+database. Spree has never stored credit card information in the database
+but prior to the use of profiles, the only safe way to handle this was
+to post the credit card information in the final step. It should be
+possible to customize the checkout so that the credit card information
+is entered on the final step and then you can authorize the card before
+Spree automatically discards the number while saving the `Creditcard`
+object.
+!!!
+
+# Seed and Sample Data in Extensions
+
+Seed data is data that is needed by the application in order for it to
+work properly. Seed data is not the same as sample data. Instead of
+loading this type of data in a migration it is handled through the
+standard rails task through `rake db:seed`. The rake task will first
+load the seed data in the spree core (ex. `db/default/countries.yml`.)
+Spree will then load any fixtures found in the `db/default` directory of
+your extensions. If you wish to perform a seeding function other than
+simply loading fixtures, you can still do so in your extension's
+`db/seeds.rb` file.
+
+Sample data is data that is convenient to have when testing your code.
+Its loaded with the `rake db:sample` task. The core sample data is
+loaded first, followed by any fixtures contained in the `db/sample`
+directory of your extensions.
+
+If you have fixtures in your extension with the same filename as those
+found in the core, they will be loaded instead of the core version. This
+applies to both sample and seed fixtures. This allows for fine grained
+control over the sample and seed data. For example, you can create your
+own custom sample order data in your site extension instead of relying
+on the version provided by Spree.
+
+!!!
+You should remove all `db:bootstrap` tasks from your
+extensions. The new bootstrap functionality in the core will
+automatically load any fixtures found in `db/sample` of your extension.
+Failing to remove this task from your extension will result in an
+attempt to create the fixtures twice.
+!!!
+
+# RESTful API
+
+The REST API is designed to give developers a convenient way to access
+data contained within Spree. With a standard read/write interface to
+store data, it is now very simple to write third party applications (ex.
+iPhone) that can talk to Spree. The API currently only supports a
+limited number of resources.
+The list will be expanded soon to cover additional resources. Adding
+more resources is simply a matter of making the time for testing and
+investigating possible security implications.
+See the [REST API section](http://guides.spreecommerce.org/legacy/0-11-x/rest.html) for full details.
+
+# Inventory
+
+Inventory modeling has been modified to improve performance. Spree now
+uses a hybrid approach where on-hand inventory is stored as a count in
+`Variant#on_hand`, but back-ordered, sold or shipped products are
+stored as individual `InventoryUnits` so they can be tracked.
+
+This improves the performance of stores with large inventories. When the
+`on_hand` count is increased using `Variant#on_hand=`, Spree will
+first fill back-orders, converting them to `InventoryItems`, then place
+the remaining new inventory as a count on the `Variant` model. A
+migration is in place that will convert on-hand `InventoryItems` to a
+simple count during upgrade. Due to an issue with the sample data, demo
+stores cannot be upgraded in this fashion and should be re-bootstrapped.
+
+# Miscellaneous improvements
+
+## Sample Product Images in Extensions
+
+For some time now you've been able to write sample data fixtures in
+extensions
+that will get run when you load sample data with the `rake db:bootstrap`
+task.
+
+Now you can also add sample product image files in your extensions in
+the
+extensions own `lib/tasks/sample/products` directory. These images will
+be
+copied to the `public/assets/products` directory when the sample data is
+loaded.
+
+***
+Additional information on the release can be found in the
+`CHANGELOG` file as well as the [official ticket
+system](http://railsdog.lighthouseapp.com/projects/31096-spree/milestones/45833-10).
+***
+
+## Ruby 1.9 Support
+
+Spree is now 100% Ruby 1.9 compatible. There are a few workarounds
+needed to achieve this and those are consolidated in a custom
+initializer appropriately named `workarounds_for_ruby19`.
+
+## Sales Overview
+
+The default admin screen now shows a series of tables and graphs related
+to recent sales activity. By consulting this screen you can now see the
+following information
+
+- Best Selling Products
+- Top Grossing Products
+- Best Selling Taxons
+- Information on the Last 5 Orders
+- Biggest Spenders
+- Out of Stock Products
+- Order Count by Day
+
+## Extension Load Order
+
+It is now recommended to define the extension load order outside of the
+`environment.rb` file. This makes it easier for you to use the standard
+`environment.rb` file that comes with Spree and thus easier to upgrade.
+To define the extension load order inside of an initializer you can use
+the following line of code:
+
+```bash
+SPREE_EXTENSIONS_LOAD_ORDER = [:theme_default, :all, :site]```
+
+## SEO Improvements
+
+Products and taxons are now available by a single URL only. Prior to
+this release both of these URL's returned the same result:
+
+- http://localhost:3000/products/ruby-on-rails-ringer-t-shirt/
+- http://localhost:3000/products/ruby-on-rails-ringer-t-shirt
+
+Now we are returning a `301` redirect for the version of the URL without
+the trailing '/' character. Some SEO experts seem to feel that
+inconsistent links and [links without a trailing slash can be
+penalized](http://www.ragepank.com/articles/68/that-trailing-slash-does-matter/)
+We've been asked by one of our clients to fix this. We're passing on the
+SEO improvements to you!
+
+## Multiple Forms of Payment
+
+Spree now supports multiple forms of payment. This support is in the
+early stages but the basic build blocks are now present so that it
+should be quite easy to allow additional forms of payment. More
+documentation and improvements in this area are coming.
+
+## Refunds and Credits
+
+Spree now has explicit support for refunds and credits. More details to
+follow.
+
+# Known Issues
+
+The [ticket
+system](http://railsdog.lighthouseapp.com/projects/31096-spree/) lists
+all known
+outstanding issues with the Spree core. Some issues have a release
+target (*milestone*)
+attached: this is an indication of how soon an issue will be tackled.
+
+!!!
+There are some problems which we have traced to other projects.
+We list a few significant ones here.
+!!!
+
+## Ruby 1.9 and Sqlite3
+
+This combination doesn't work with Rails 2.3.5: the `change_column`
+calls make all fields into `NOT NULL`.
+See [the related
+ticket](http://railsdog.lighthouseapp.com/projects/31096-spree/tickets/1265-sqlite3sqlexception-adjustmentsposition-may-not-be-null)
+for more info.
+
+Workaround: apply the Rails patch by hand, or use MySQL instead if you
+want to try Ruby1.9
+````
diff --git a/guides/src/content/release_notes/0_30_0.md b/guides/src/content/release_notes/0_30_0.md
new file mode 100644
index 00000000000..cc23b9fc1f9
--- /dev/null
+++ b/guides/src/content/release_notes/0_30_0.md
@@ -0,0 +1,232 @@
+---
+title: Spree 0.30.0
+section: release_notes
+order: 23
+---
+
+# Summary
+
+Spree 0.30.0 is the first official release to support Rails 3.x. It has
+been several months in the making but we're finally here. Unfortunately
+we haven't had the time to write up detailed release notes and the
+documentation is still a work in progress. We'll try to mention the
+highlights here and we'll continue to update the documentation in the
+coming weeks.
+
+---
+
+We're always looking for more help with the Spree documentation.
+If you'd like to offer assistance please contact us on the spree-user
+mailing list and we can give you commit access to the
+[spree-guides](https://github.com/spree/spree-guides) documentation
+project.
+
+---
+
+# Rails Engines
+
+Spree is now heavily reliant upon the concept of Rails Engines. This
+represents a significant architectural shift from previous versions of
+Spree. This will likely be the most time consuming upgrade of Spree
+you'll ever have to make. The change is the result of a major change in
+Rails itself so the difficulties are unavoidable. The good news is that
+Rails has adopted many of the ideas used in Spree (Engines are now
+equivalent to Spree Extensions and visa versa.) This means that there is
+very little non-standard Rails behavior left in Spree.
+
+## No More Site Extension
+
+Previous versions of Spree required a [site
+extension](http://spreecommerce.com/legacy/0-30-x/extensions.html#thesiteextension)
+in order to customize the look and feel of the site. One major
+improvement in Spree is that this is no longer necessary. All of the
+content that normally goes in your site extension can now be moved to to
+_Rails.root_.
+
+## Extensions are Now Gems
+
+Extensions are now installed as Rubygems. They are also no longer
+deployed to _vendor/extensions_. You need to add the required extensions
+to you _Gemfile_. There is a comprehensive [Extension Guide](/developer/extensions_tutorial) in the
+online documentation which can assist you.
+
+As of the time of this release there are only a limited number of
+extensions that are currently compatible with Spree 0.30.x. It is
+suggested that you check the [Extension
+Registry](https://github.com/spree-contrib) for more information on
+which extensions are 0.30.x compatible. Check back often because the
+Spree core team will be working on updating the more critical ones
+immediately after the release.
+
+---
+
+Its relatively easy to convert an existing extension into a gem.
+Its suggested you find a 0.30.x compatible extension and study the
+source code for a better idea on how to do this.
+
+---
+
+# Improvements to Payments
+
+Payments have been significantly improved in this version of Spree. One
+of the most important changes is the addition of a [state
+machine](https://github.com/pluginaweek/state_machine) for payments.
+Payments that are submitted to a payment gateway for processing are in
+the "processing state." This will help to prevent additional attempts to
+process the payment through customer refreshing, etc. Failed payments
+are also recorded and given a "failed" state.
+
+We have abandoned the concept of payment transactions and now record
+most of the information directly in the payment record. When in comes
+time to calculate the payment total, only payments in the "completed"
+state are counted.
+
+# Simplification of Adjustments
+
+Adjustments have also been dramatically simplified. Instead of having
+the concept of _Charge_ and _Credit_ we just have the single
+_Adjustment_. What used to be called a _Credit_ is now just a negative
+adjustment. Adjustments also now have a _mandatory_ attribute. When this
+attribute is _true_ the adjustment is always shown when displaying the
+order total, even if the value is zero. All non-mandatory adjustments
+are removed from the order if their value is ever equal to zero.
+
+---
+
+Mandatory adjustments make it easy to show $0 for tax or shipping
+when those cases apply. The thinking is we don't want customers to
+wonder what the shipping cost because its not present - better to show a
+$0 value explicitly.
+
+---
+
+# New Promotion Functionality
+
+Promotion functionality in Spree has been greatly improved. There is a
+new _spree_promo_ gem which is included by default when you install
+Spree.
+
+## Creating a Promotion
+
+A new promotion requires a _name_ and _code_ attribute. The _code_
+attribute can be used by customers when checking out to "activate" a
+particular promotion.
+
+---
+
+This is standard "coupon code" functionality but you're not
+required to have customers enter codes in order to utilize promotions.
+
+---
+
+## Promotion Rules
+
+Once a new promotion is created you can create one or more rules for the
+promotion. You can require that all rules for the promotion be satisfied
+or just one of the rules.
+
+Each of the rules is based on a Ruby class that extends _PromotionRule_.
+There are four built in rule types for Spree but others can be added via
+extension or directly through your Spree application code.
+
+- **Item Total:** Limit to orders with an item total above a specified
+ amount
+- **Product:** Limit to orders containing one or all of the specified
+ products
+- **User:** Limit to orders made by specific users
+- **First Order:** Limit to the first order by a user
+
+# No More "Vendor Mode"
+
+Spree is deployed as a Rubygem now so the previous system of different
+"boot modes" has been simplified. Spree never needs to be deployed
+inside of your application, even if you're using edge or a custom fork.
+Thanks to Bundler you can reference any version of Spree source directly
+via _Gemfile_ and either a physical directory location or a git
+repository location.
+
+---
+
+See the [Source Guide](http://guides.spreecommerce.org/legacy/0-30-x/source_code.html) for a complete
+understanding of all the changes to the organization of the source code.
+
+---
+
+# Upgrading
+
+## Before You Upgrade
+
+### Upgrade to the Previous Version
+
+It is recommended that you upgrade to Spree 0.11.x (the previous latest
+stable version) first. The upgrade process should go much smoother if
+you upgrade incrementally.
+
+### Backup Your Database
+
+It is always recommended that you backup your database before upgrading.
+You should also test the upgrade process locally and/or on a staging
+server before attempting on your live production system.
+
+!!!
+The Spree 0.30.0 upgrade will delete any in progress orders
+which should generally considered to be a safe thing to do since these
+are typically just abandoned orders. There are also non trivial changes
+to payments and other tables. Hang on to your database backup until
+you're sure the upgrade has gone smoothly.
+!!!
+
+## Create a New Rails Application
+
+It is suggested that you create a brand new Rails 3.x application and
+then make the necessary changes to that application. We'll briefly walk
+you through the steps to do this.
+
+!!!
+There have been major changes to how Rails applications (and
+consequently Spree) are configured and initialized. You will have an
+easier time if you start with a new Rails application and migrate your
+stuff over to it rather than trying to make changes to an existing Spree
+application so that its Rails 3 compliant.
+!!!
+
+### Copy Your Legacy Files
+
+Spree no longer requires that you have a "site" extension. This means
+that you should copy all of the files in _vendor/extensions/site_ into
+the _app_ directory of your new Rails application. This includes the
+contents of the _public_ directory.
+
+### Add Spree to the _Gemfile_
+
+So now you have a new Rails 3.x application and you've moved over your
+custom files. Its time to add the Spree gem into the mix. Edit your
+_Gemfile_ and add the following entry:
+
+```ruby
+gem 'spree', '0.30.0'
+```
+
+Then install the Spree gem using the familiar Bundler approach:
+
+```bash
+$ bundle install
+```
+
+## Upgrade Migrations and Assets
+
+The gems that comprise Spree contain various public assets (images,
+stylesheets, etc.) as well as database migrations that are needed in
+order to run Spree. There is a Rake tasks designed to copy over the
+necessary files.
+
+```bash
+$ bundle exec rake spree:install
+```
+
+Once the migrations are copied over you can migrate using the familiar
+Rake task for this.
+
+```bash
+$ bundle exec rake db:migrate
+```
diff --git a/guides/src/content/release_notes/0_40_0.md b/guides/src/content/release_notes/0_40_0.md
new file mode 100644
index 00000000000..866de661727
--- /dev/null
+++ b/guides/src/content/release_notes/0_40_0.md
@@ -0,0 +1,190 @@
+---
+title: Spree 0.40.0
+section: release_notes
+order: 22
+---
+
+# Summary
+
+Spree 0.40.0 represents another step forward towards the eventual 1.0.0
+release. This version focuses heavily on authentication and
+authorization. Most sites running 0.30.x will be able to upgrade with
+very little difficulty. We're still working on identifying all of the
+Spree extensions that run 0.40.x but its a fairly safe bet that any
+extension running 0.30.x will work with this release.
+
+---
+
+We're always looking for more help with the Spree documentation.
+If you'd like to offer assistance please contact us on the spree-user
+mailing list and we can give you commit access to the documentation
+project.
+
+---
+
+# Database Migrations
+
+There are several new database changes in the 0.40.0 release. You will
+need to update your database migrations as follows:
+
+````bash
+$ bundle exec rake spree:install:migrations
+$ bundle exec rake db:migrate```
+
+!!!
+Always be sure to perform a complete database backup before
+performing any operations. It is also suggested that you examine the new
+migrations closely before running them so you are aware of what changes
+are being made to your database.
+!!!
+
+# Replacing Authlogic with Devise
+
+Spree has replaced the authlogic gem in favor of
+[Devise](https://github.com/plataformatec/devise) for all authentication
+methods. In part an effort to both simplify customization strategies,
+due to devise's modular nature, and allow for much simpler creation of
+extensions needing different authentication schemas and scopes. We have
+made every effort to maintain the backward compatibility required to
+upgrade an existing site. This means that naming conventions, and
+routing have all remained intact where possible as well as the use of
+deprecation notices.
+
+The database changes, described below, are made to offer as much
+flexibility as Devise itself offers and enabling you to implement,
+adjust the behavior, or remove features fairly effortlessly for those
+familiar with Devise.
+
+## Miscellaneous Clean Up
+
+Some of the biggest changes to the Spree authentication process is
+consolidation of most all the user management functions into the
+*spree_auth* gem. Prior versions of Spree still had small bits of code
+floating around in *spree_core* etc. So some files have simply been
+moved to where they should have been all along.
+
+## Upgrading Existing Sites
+
+We have tried to minimize changes and when possible, naming conventions
+have be maintained. The result is that only one controller has been
+moved and renamed. Routing and the named route conventions having been
+maintained as well.
+
+***
+The file *core/password_resets_controller.rb* has been renamed
+and moved to *auth/user_password_resets_controller.rb*
+***
+
+We have already set up devise to handle the existing encryption scheme
+that authlogic used so there is no need to make any changes and the
+current users will work "out of the box".
+
+# Changes to the REST API
+
+Spree 0.40.x introduces several minor but important changes to the REST
+API. If you are currently relying on the API you should be aware of a
+few important changes. Please also consult the detailed [REST
+Guide](http://guides.spreecommerce.org/legacy/0-40-x//rest.html) for more details.
+
+## New Authentication Mechanism
+
+The most significant change to the REST API is related to
+authentication. The recent adoption of
+[Devise](https://github.com/plataformatec/devise) for authentication in
+general has resulted in new opportunities to improve authentication for
+the API specifically.
+
+Prior to Spree 0.40.x the old method of authentication was to pass an
+authentication token in the header. This involved using the specially
+designated *X-SpreeAPIKey* header and passing a corresponding token
+value. The new approach is to use standard *HTTP_AUTHORIZATION* which
+is already nicely implemented by Devise.
+
+If you were using curl you could achieve this authentication as follows:
+
+```bash
+curl ~~u V8WPYgRdSZN1mSQG17sK:x \
+http://example.com/api/orders.json```
+
+Note that we are using the token as the "user name" and passing "x" as a
+password here. There is nothing special about "x", its just a
+placeholder since many HTTP Basic Authentication implementations require
+a password to be submitted. In our case the token is sufficient so we
+use a placeholder for the password.
+
+h4. Support for *.json* Suffix
+
+It is now recommended that you consider using a *.json* suffix in your
+URL when communicating via the REST API. This is technically not a new
+feature~~ it was always possible in older versions of the REST API.
+We've updated the documentation to suggest this simpler approach (which
+avoids the necessity of passing *Accept:application/json* in the
+header.)
+
+```bash
+curl -u V8WPYgRdSZN1mSQG17sK:x http://example.com/api/orders.json```
+
+# Tokenized Permissions
+
+There are situations where it may be desirable to restrict access to a
+particular resource without requiring a user to authenticate in order to
+have that access. Spree allows so-called "guest checkouts" where users
+just supply an email address and they're not required to create an
+account. In these cases you still want to restrict access to that order
+so only the original customer can see it. The solution is to use a
+"tokenized" URL.
+
+```bash
+http://example.com/orders/token/aidik313dsfs49d
+````
+
+Spree provides a _TokenizedPermission_ model used to grant access to
+various resources through a secure token. This model works in
+conjunction with the _Spree::TokenResource_ module which can be used to
+add tokenized access functionality to any Spree resource.
+
+The _Order_ model is one such model in Spree where this interface is
+already in use. The following code snippet shows how to add this
+functionality through the use of the _token_resource_ declaration:
+
+```ruby
+Order.class_eval do
+ token_resource
+end
+```
+
+If we examine the default CanCan permissions for _Order_ we can see how
+tokens can be used to grant access in cases where the user is not
+authenticated.
+
+```ruby
+can :read, Order do |order, token|
+ order.user user || order.token && token order.token
+end
+can :update, Order do |order, token|
+ order.user user || order.token && token order.token
+end
+can :create, Order
+```
+
+This configuration states that in order to read or update an order, you
+must be either authenticated as the correct user, or supply the correct
+authorizing token.
+
+The final step is to ensure that the token is passed to CanCan when the
+authorization is performed. Most controllers will do this automatically
+if they declare _resource_controller_.
+
+```ruby
+authorize! action, resource, session[:access_token]
+```
+
+---
+
+Since _OrdersController_ does not implement _resource_controller_
+this is done explicitly in the controller
+
+---
+
+For more information on tokenized permissions please read the detailed
+[security guide](http://guides.spreecommerce.org/legacy/0-40-x/security.html#tokenized-permissions).
diff --git a/guides/src/content/release_notes/0_50_0.md b/guides/src/content/release_notes/0_50_0.md
new file mode 100644
index 00000000000..6565cca3538
--- /dev/null
+++ b/guides/src/content/release_notes/0_50_0.md
@@ -0,0 +1,76 @@
+---
+title: Spree 0.50.0
+section: release_notes
+order: 21
+---
+
+# Summary
+
+Spree 0.50.0 represents a minor update to the 0.40.x release. Several
+important bugs in the 0.40.x release have been addressed. There are no
+crucial security fixes in this release but you are still encouraged to
+upgrade as soon as convenient. By making these small upgrades as they
+are released you will only need to focus on minor changes to each point
+release instead of a series of important changes covering several
+releases.
+
+This is a another step forward towards the eventual 1.0.0 release. We're
+still working on identifying all of the Spree extensions that run 0.50.x
+but its a fairly safe bet that any extension running 0.40.x will work
+with this release. Look for some major improvements to how extensions
+are certified against Spree versions in the very near future.
+
+INFO: We're always looking for more help with the Spree documentation.
+If you'd like to offer assistance please contact us on the spree-user
+mailing list and we can give you commit access to the
+[spree-guides](https://github.com/spree/spree-guides) documentation
+project.
+
+# Database Migrations
+
+There is only one minor database changes in the 0.50.0 release. You will
+need to update your database migrations as follows:
+
+````bash
+$ bundle exec rake spree:install:migrations
+$ bundle exec rake db:migrate```
+
+!!!
+Always be sure to perform a complete database backup before
+performing any operations. It is also suggested that you examine the new
+migrations closely before running them so you are aware of what changes
+are being made to your database.
+!!!
+
+# Significant Improvements in Test Coverage
+
+We have made drastic improvements to the level of test coverage. In
+particular, there are tons of new Cucumber features that perform
+automated testing in the browser for some of the most important
+features. This doesn't impact store owners in any way, but better test
+coverage means its safer to change the Spree code without breaking
+things so its a step towards more stability.
+
+***
+See the Testing Guide for more details on testing in Spree.
+***
+
+# Replace search_logic gem with meta_search
+
+We have also replaced the
+[search_logic](https://github.com/binarylogic/searchlogic) gem with
+[meta_search](https://github.com/ernie/meta_search). This is another
+one of those behind the scenes changes that you're not likely to notice.
+Our reason for making the switch was that search_logic is not supported
+for Rails3 and it took us several days to get it working for Spree
+0.30.x. We're not anxious to support it any longer and this is part of
+our ongoing effort to get to stable a stable release of Spree that is
+easier to support.
+
+!!!
+If you are using one of the search related extensions for Spree
+you may experience compatibility issues. Please report any troubles you
+have on the spree-user list and we'll see if we can help. Eventually
+we'll revisit these extensions and verify their compatibility.
+!!!
+````
diff --git a/guides/src/content/release_notes/0_60_0.md b/guides/src/content/release_notes/0_60_0.md
new file mode 100644
index 00000000000..c590450f2ec
--- /dev/null
+++ b/guides/src/content/release_notes/0_60_0.md
@@ -0,0 +1,159 @@
+---
+title: Spree 0.60.0
+section: release_notes
+order: 20
+---
+
+# Summary
+
+The 0.60.0 release contains a significant change to all controllers
+within Spree as they have been refactored to remove the use of the
+[resource_controller](https://github.com/jamesgolick/resource_controller)
+gem. This release may have an impact on existing extensions or site
+customizations that leverage some of resource_controllers features, so
+it's important to read the details on it's removal below and review any
+code which might be affected.
+
+While the removal of resource_controller is not most glamous or
+exciting change, it's another significant stepping stone to our 1.0
+release.
+
+---
+
+We're always looking for more help with the Spree documentation.
+If you'd like to offer assistance please contact us on the spree-user
+mailing list and we can give you commit access to the
+documentation project.
+
+---
+
+# Database Migrations
+
+There are no database migrations to worry about in this release
+(assuming you are already running Spree 0.50.x).
+
+!!!
+Always be sure to perform a complete database backup before
+performing any operations.
+!!!
+
+# Removal of resource_controller
+
+The resource_controller gem has been central to Spree's controllers
+from one it's earliest releases and was responsible for some of Spree's
+customizability features. It's removal has been discussed (endlessly)
+and worked on for quite some time. It's original lack of Rails 3.0
+support delayed Spree's 0.30.0 release for sometime while we forked and
+updated the gem to support Rails 3. However it was still felt that the
+library was too overbearing for Spree's needs and added unneeded
+complexity to it's controllers.
+
+Some earlier Spree releases removed its usage from the more complex
+front-end controllers (like OrdersController and CheckoutController) and
+this release extends this to all front-end controllers.
+
+## Supported Functionality
+
+The majority of backend (Admin) controllers now base off of
+_Admin::ResourceController_ class which provides a much simpler and
+slimmed down version of resource_controllers existing features, while
+attempting to maintain backwards compatibility for the majority commonly
+used extension points.
+
+_Admin::ResourceController_ provides several resource_controller style
+features as follows.
+
+### Standard CRUD Methods
+
+There are basic implementations of all the standard CRUD methods:
+
+- _:index_
+- _:show_
+- _:create_
+- _:read_
+- _:update_
+- _:destroy_
+
+### In Action Callbacks
+
+- _:update.before_
+- _:create.before_
+- _:update.after_
+- etc.
+
+### URL Helpers
+
+- _new_object_url_
+- _edit_object_url_
+- _object_url_
+- _collection_url_
+
+!!!
+Use of these generic helpers is discouraged in favor of the
+default Rails helper urls.
+!!!
+
+### Instance Variables
+
+- \*`object+
+ - +`collection\*
+
+!!!
+Use of these variables is strongly discouraged. Use the actual
+Rails standard variable names instead.
+!!!
+
+## Unsupported Functionality
+
+_Admin::ResourceController_ does **NOT** provide the following
+resource_controller features:
+
+### Custom Responses
+
+- _create.wants.js_
+- _update.wants.html_
+
+These have been replaced with a new method called _respond_override_
+
+---
+
+See the Customization guide for more details
+on _respond_override_
+
+---
+
+### Deprecated Methods
+
+These methods are deprecated and should either be removed or replaced
+with a custom _load_resource_ method (see _Admin::ResourceController_
+source for details).
+
+- _object_
+- _collection methods_
+
+The following method for custom flash messages has been removed entirely
+(with no replacement approach.)
+
+- _create.flash_
+
+Admin::ResourceController's use is encouraged in extensions and/or site
+customizations that require basic CRUD admin controllers. It should not
+be used on front-end or complex admin controllers, as you can see from
+the current source even certain core admin controllers do not use it
+(for example Admin::OrdersController).
+
+---
+
+While the use of resource_controller has been completely removed
+from all of Spree's core controllers, the dependency on
+resource_controller gem will remain for a while to allow extension
+authors to update their projects.
+
+---
+
+# Miscellaneous Changes
+
+There are also a series of minor bug fixes and improvments. Please see
+the [Github
+compare](https://github.com/spree/spree/compare/v0.50.2...v0.60.0) for
+more details.
diff --git a/guides/src/content/release_notes/0_70_0.md b/guides/src/content/release_notes/0_70_0.md
new file mode 100644
index 00000000000..c9057ff805a
--- /dev/null
+++ b/guides/src/content/release_notes/0_70_0.md
@@ -0,0 +1,462 @@
+---
+title: Spree 0.70.0
+section: release_notes
+order: 19
+---
+
+# Summary
+
+This 0.70.0 release is the first Spree release to support Rails 3.1 and
+contains several exciting new features including a complete overhaul of
+the theming system and major improvements to the Promotions system.
+
+---
+
+We're always looking for more help with the Spree documentation.
+If you'd like to offer assistance please contact us on the spree-user
+mailing list and we can give you commit access to the documentation
+project.
+
+---
+
+# Theming Improvments
+
+Theming support in this release has change significantly and all the new
+features are covered in detail in the following guides:
+
+- [Customization Overview](http://guides.spreecommerce.org/legacy/0-70-x/customization.html) - provides a high level
+ introduction to all the customization and theming options now
+ provided by Spree.
+- [View Customization](http://guides.spreecommerce.org/legacy/0-70-x/view_customization.html) - introduces and
+ explains how to use Deface to customize Spree's views.
+- [Asset Customization](http://guides.spreecommerce.org/legacy/0-70-x/asset_customization.html) - explains how Spree
+ uses Rails' new asset pipeline, and how you can use it to customize
+ the stylesheets, javascripts and images provided by Spree.
+
+---
+
+While the upgrade instructions that follow briefly cover the new
+asset pipeline features we suggest you review the guides above as part
+of your upgrade preparation.
+
+---
+
+## Themes as engines
+
+In previous versions (0.11.x and earlier) Spree encouraged the approach
+of bundling themes in their own extensions, with the advent of the asset
+pipeline in Rails 3.1 and the upcoming Theme Editor we're bringing back
+this approach as a suitable model for distributing and sharing themes.
+
+While the distinction between themes and extensions is covered in the
+[Extensions & Themes](http://guides.spreecommerce.org/legacy/0-70-x/extensions.html) guide they are both just Rails
+engines and can be treated as such.
+
+We've created two front-end themes to help show this new approach in
+action:
+
+- [Spree Blue](https://github.com/spree/spree_blue_theme) - Recreates
+ the original "blue" theme of 0.60.x as a stand alone theme.
+
+- [Rails Dog Radio](https://github.com/spree/spree_rdr_theme) - This
+ recreates some of the aspects of the Rails Dog Radio demo
+ application for a default Spree application.
+
+Both themes can be installed by just adding a reference to the git
+repository to your Gemfile, ie:
+
+```ruby
+gem 'spree_blue_theme', git: 'git://github.com/spree/spree_blue_theme.git'
+```
+
+# Experimental SCSS/SASS Theme
+
+LESS proves to be unpopular amongst Spree developers so it is decided to
+retire LESS stylesheets of `spree_blue_theme` in favor for all-in-one
+`screen.css`.
+
+Yet with the recently adopted SCSS/SASS in Rails 3.1, we believe this
+technology could become de-facto standard for CSS someday. Thus, we
+create the experimental SCSS/SASS version of
+[spree_blue_sass_theme]("https://github.com/spree/spree_blue_sass_theme")
+to collect feedbacks before we could come up with a decision to opt for
+SCSS/SASS in future version of Spree.
+
+# The Asset Pipeline
+
+One of the more interesting new features included in Rails 3.1 is the
+asset pipeline, and a lot of the customization improvements in Spree are
+enabled by this feature. While the asset pipeline provides excellent
+flexibility and improved organization for all of Spree's assets (images,
+javascripts and stylesheets) it does add a significant overhead as all
+asset requests are now being handled by Rails itself (and not being
+handed off to the web server).
+
+This can be especially noticeable in development mode when using a
+single process application server like Webrick. This release of Spree
+includes a small tweak to the standard pre-compiling rake task that
+allows pre-compiling of assets for development mode.
+
+## Pre-compiling in production
+
+Rails supports pre-compiling of assets which is intended to offload the
+overhead of generating and serving assets from the application server in
+production environments.
+
+Pre-compiling is not required for the asset pipeline to function
+correctly in production, if you choose to not pre-compile Rails will
+generate each asset only once and serve each subsequent request using
+Rack::Cache.
+
+Rack::Cache is generally sufficient for lower traffic sites, but
+pre-compiling will provide some additional speed increases by allowing
+the web server to serve all assets, including gzipped versions of
+javascript and css files.
+
+To pre-compile assets for production you would normally execute the
+following rake task (on the production server).
+
+```bash
+$ bundle exec rake assets:precompile
+```
+
+This would write all the assets to the `public/assets` directory while
+including an MD5 fingerprint in the filename for added caching benefits.
+
+---
+
+In production all references to assets from views using
+image_tag, asset_path, javascript_include_tag, etc. will
+automatically include this fingerprint in the file name so the correct
+version will be served.
+
+---
+
+## Pre-compiling for development
+
+Spree alters the behaviour of the precompile rake task as follows:
+
+````bash
+$ bundle exec rake assets:precompile:nondigest```
+
+It will still output the assets to `public/assets` but it will not
+include the MD5 fingerprint in the filename, hence the files will be
+served in development directly by the web server (and not processed by
+Rails).
+
+!!!
+Using the precompile rake task in development will prevent any
+changes to asset files from being automatically included in when you
+reload the page. You must re-run the precompile task for changes to
+become available.
+!!!
+
+Rail's also provides the following rake task that will delete the entire
+`public/assets` directory, this can be helpful to clear out development
+assets before committing.
+
+```bash
+$ bundle exec rake assets:clean```
+
+It might also be worthwhile to include the `public/assets` directory in
+your `.gitignore` file.
+
+# Promotions
+
+Promotions have been extended well beyond their coupon roots to include
+new `activator` support, which can now fire or activate a promotion for
+a variety of different user triggered events, including:
+
+- Adding items to a cart
+- Visiting specific pages
+- Adjusting cart contents
+- Using a coupon code
+
+Promotions also includes a new `Actions` feature which can perform an
+action as a result of a promotion being actived, these actions currently
+include creating an adjustment or adding additional items to a cart.
+
+For more details on these new promotions feature please refer to the
+[Promotions](http://guides.spreecommerce.org/legacy/0-70-x/promotions.html) guide.
+
+# New Extension Generator
+
+There is a new extension generator available as part of this release.
+The generator is utilized by a new executable contained within the gem.
+
+You can generate new extensions inside an existing rails application or
+as a standalone git repository using the following command
+
+```bash
+$ spree extension foofah
+````
+
+One of the most important advances in this new generator is that you can
+now easily run specs for extensions in their own standalone repository.
+You just need to create a test application (one time only) as a context
+before running your specs.
+
+```bash
+$ bundle exec rake test_app
+$ bundle exec rspec spec
+```
+
+You can get more information on the extension generator in the [Creating
+Extensions](http://guides.spreecommerce.org/legacy/0-70-x/creating_extensions.html) guide.
+
+---
+
+You must install the spree gem in order to use the new extension
+generator.
+
+---
+
+# Upgrade Instructions
+
+These instructions are mainly concerned with upgrading Spree
+applications, extension developers should jump straight to the "Asset
+Customization"asset_customization.html and [View
+Customization](http://guides.spreecommerce.org/legacy/0-70-x/view_customization.html) guides for details on the
+changes that are required for extensions to fulling support 0.70.0.
+
+## Before you begin
+
+To prevent problems later please ensure that you've copied all the
+migrations from your current version of Spree and all the extensions
+that you are running. While this is normal practice when setting up
+Spree and extensions any missing migrations will cause issues later in
+the upgrade process.
+
+You can check that all Spree migrations are copied by running:
+
+```bash
+$ bundle exec rake spree:install:migrations
+```
+
+Each extension will provide it's own rake task (or generator) for
+copying migrations so please refer to the extensions README for
+instructions.
+
+!!!
+It's vital that you confirm this first before starting the
+upgrade process as Rails 3.1 has altered how engine migrations are
+handled and incorrectly copying migrations later could result in data
+loss.
+!!!
+
+## Changes required for Rails 3.1
+
+Most of the changes required to upgrade a Spree application to 0.70.0 is
+same for any Rails 3.0.x application upgrading to 3.1.
+
+### Gemfile changes
+
+You'll need to make several additions and changes to your Gemfile:
+
+- Update spree to 0.70.0
+- Update rails to 3.1.1
+- Update mysql2 to 0.3.6 - if your using it.
+- Ensure the assets group is present:
+
+```ruby
+group :assets do
+ gem 'sass-rails', "~> 3.1.1"
+ gem 'coffee-rails', "~> 3.1.1"
+ gem 'uglifier'
+end
+```
+
+### Update config/boot.rb
+
+Rails 3.1 simplifies the `config/boot.rb` file significantly, so update
+yours to match:
+
+```ruby
+require 'rubygems'
+
+# Set up gems listed in the Gemfile.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', *FILE*)
+
+require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+```
+
+### Enable the asset pipeline
+
+Add the following line to `config/application.rb`, this will enable the
+Asset Pipeline feature (required by 0.70.0):
+
+```ruby
+config.assets.enabled = true
+```
+
+### Remove deprecated configuration
+
+Remove the following line from `config/environments/development.rb`.
+
+```ruby
+config.action_view.debug_rjs = true
+```
+
+### Retire lib/spree_site.rb
+
+The spree_site.rb file is no longer required so it's important to
+migrate any existing code out of this file and into the correct
+location.
+
+- Model, controller or helper evals should be relocated to a suitable
+ \_decorator.rb file.
+- Configuration settings should be moved to initializers.
+- If you are activating custom Abilities or Calculators, you should
+ remove then for now. You can re-add them to config/application.rb
+ after you've ran the spree:site generator below. They will need to
+ be inside the `config.to_prepare` block.
+
+---
+
+The decorator initialization block will be automatically included
+in config/application.rb for you, when you run the spree:site generator
+below.
+
+---
+
+### Remove spree_site from config/application.rb
+
+Also ensure the following line is **not** present in
+config/application.rb:
+
+```ruby
+require 'spree_site'
+```
+
+## Update gems & generate Spree files
+
+Once you've completed all the Rails 3.1 steps above, you can now update
+your dependencies and start generating the new asset pipeline
+placeholders.
+
+### Install dependencies
+
+Install all the required gems, and lock your dependencies by running:
+
+````bash
+ $ bundle update```
+
+### Generate & copy required files
+
+Running the `spree:site` generator will create the skeleton structure
+for the asset pipeline, and also copy missing migrations.
+
+```bash
+ $ rails g spree:site```
+
+After running the generator above, it's best to check to make sure you
+do not have multiple copies of the following line, in
+`config/application.rb`:
+
+```ruby
+config.middleware.use "RedirectLegacyProductUrl"
+config.middleware.use "SeoAssist"
+````
+
+### Update your database
+
+If you encounter any issues with this step please ensure you've
+completed the [Before you
+begin](#before-you-begin) section above.
+
+!!!
+Make sure you've taken a backup of the database before
+attempting this step.
+!!!
+
+````bash
+$ bundle exec rake db:migrate```
+
+## Migrating your assets
+
+Cleaning up your `/public` directory is one major advantage of using
+Rails 3.1. The main task required here is to separate your application
+specific assets (javascript, stylesheets and images) from all of Spree's
+(and all those belonging to all the extensions installed) which have
+been mingled together in your public directory.
+
+### Asset Organization
+
+Assets should be placed inside your application in one of three
+locations: `app/assets`, `lib/assets` or `vendor/assets`.
+
+`app/assets` is for assets that are owned by the application, such as
+custom images, JavaScript files or stylesheets.
+
+`lib/assets` is for your own libraries' code that doesn't really fit
+into the scope of the application or those libraries which are shared
+across applications.
+
+`vendor/assets` is for assets that are owned by outside entities, such
+as code for JavaScript plugins.
+
+***
+If you are using a library like Jammit solely for concatenating
+and minifying your javascript and stylesheet files then we suggest you
+remove it from your application as part of the Rails 3.1 upgrade.
+***
+
+For a full explanation of how Spree uses Rails' asset pipeline and how
+to update your site to use these new features please refer to the [Asset
+Customization](http://guides.spreecommerce.org/legacy/0-70-x/asset_customization.html) guide.
+
+### Cleaning up /public
+
+Once you've relocated all your applications assets, the only
+remaining directories and files in `/public` should be images that
+you've uploaded for your Products (or taxonomies). If you are using the
+default Spree configuration these will be in `/public/assets`.
+
+!!!
+Leaving any javascript, stylesheet or image files in the public
+directory will override those provided by Spree or it's extensions. So
+it's vital that you delete all remaining files from the `/public`
+directory, EXCEPT YOUR PRODUCT IMAGES!.
+!!!
+
+***
+If you are using S3 (or another cloud storage system) for your
+Product images then your `/public` directory should be completly empty.
+***
+
+## Including old style theming hooks
+
+With the introduction of Deface the old style theming hooks have been
+deprecated, the old hooks will continue to function after the upgrade as
+they are automatically upgraded to Deface overrides, we strongly suggest
+you upgrade them as soon as possible.
+
+To include your previously defined old style theming hooks from
+`lib/site_hooks.rb` add the following to the bottom of
+`config/application.rb`
+
+```ruby
+require 'lib/site_hooks'
+````
+
+---
+
+The development log will include suggested replacement Deface
+overrides anytime Rails encounters a old style hook call. These
+suggestions are a best effort replacement, but might need some tweaks as
+some elements have been moved while removing the old hook calls from
+Spree's view files.
+
+---
+
+For more information about using Deface overrides, please refer to the
+[View Customization](http://guides.spreecommerce.org/legacy/0-70-x/view_customization.html) guide.
+
+# New way to register Calculators
+
+Calculators no longer can be registered with _register_ method. The
+register method is refactored to take advantage of default Rails
+application configuration _Rails.application.config.spree.calculators_.
+
+For more information about this new change, please refer to the
+[Ajustments](http://guides.spreecommerce.org/legacy/0-70-x/adjustments.html) guide.
diff --git a/guides/src/content/release_notes/0_9_0.md b/guides/src/content/release_notes/0_9_0.md
new file mode 100644
index 00000000000..028d5a6398d
--- /dev/null
+++ b/guides/src/content/release_notes/0_9_0.md
@@ -0,0 +1,487 @@
+---
+title: Spree 0.9.0
+section: release_notes
+order: 25
+---
+
+!!!
+Some of the information here has been made redundant by later
+changes.
+!!!
+
+# Improved Layout Customization
+
+Work has been done to reduce the likelihood of new projects needing to
+override the default Spree layout template _application.html.erb_. The
+title, stylesheets, and logo now can all be customized without creating
+your own copy of the layout.
+
+## New title methods
+
+There are some new methods for manipulating the page title: the _title_
+and _set_title_ helper methods in Spree::BaseController.
+
+Use _set_title_ to set a page title either from a controller method, or
+a view template. You can also override the _default_title_ and _title_
+methods in Spree::BaseController for further control.
+
+The _title_ method is used in _application.html.erb_ of the new release,
+however if you are upgrading and want to take advantage, use this in
+between your _<title>_ tags of your layout template
+
+And to set the title in a view template:
+
+## Customize default stylesheets
+
+*Spree::Config+ is a new config option for customizing the stylesheets
+used by the default application layout. The value
+of*Spree::Config[:stylesheets]+ is a comma-separated string of
+stylesheet names without the file extensions. See the [customization
+guide](http://guides.spreecommerce.org/legacy/0-11-x/customization_overview.html) for more information.
+
+If you are upgrading, to take advantage of this use the new
+_stylesheet_tags_ helper method.
+
+## Customize logo
+
+\*Spree::Config+ is a new config option for customizing the logo image
+path.
+
+If you are upgrading, take advantage of this by using the new\*logo+
+helper method.
+
+# Polymorphic Calculators
+
+There has been significant refactoring to the implementation of
+calculators. Calculators are now polymorphic and belong to _calculable_.
+This will have a non trivial impact on your existing store
+configuration. After upgrading to Spree 0.9.0 you are likely going to
+have to make several manual adjustments to the existing tax and shipping
+configurations. Ultimately we feel this is outweighed by the superior
+design of the new calculator system which will allow for a more modular
+design.
+
+!!!
+Many of the existing calculator extensions are not yet updated
+to support Spree 0.9.0. Please check the [extension
+registry](https://github.com/spree-contrib) to see which versions are
+supported. Our goal is to back port most of the useful calculators out
+there shortly after the release.
+!!!
+
+All calculators need to implement the following method
+
+```bash
+ def compute(something=nil)
+ …
+ end
+```
+
+The calculator is passed an optional "target" on which to base their
+calculation. This method is expected to return a single numeric value
+when the calculation is complete. A value of _nil_ should be returned in
+the event that a charge is not applicable.
+
+Since calculators are now instances of _ActiveRecord::Base_ they can be
+configured with preferences. Each instance of _ShippingMethod_ is now
+stored in the database along with the configured values for its
+preferences. This allows the same calculator (ex.
+_Calculator::FlatRate_) to be used with multiple _ShippingMethods_, and
+yet each can be configured with different values (ex. different amounts
+per calculator.)
+
+Calculators are configured using Spree's flexible [preference
+system](http://guides.spreecommerce.org/legacy/0-11-x/preferences.html). Default values for the preferences are
+configured through the class definition. For example, the flat rate
+calculator class definition specifies an amount with a default value of 0.
+
+```bash
+ class Calculator::FlatRate < Calculator
+ preference :amount, :decimal, :default =\> 0
+ …
+ end
+```
+
+Spree now contains a standard mechanism by which calculator preferences
+can be edited. The screenshot below shows how the amounts for the flat
+rate calculator are now editable directly in the admin interface.
+
+Calculators are now stored in a special _calculator_ directory located
+within _app/models_. There are several calculators included that meet
+many of the standard store owner needs. Developers are encouraged to
+write their own [extensions](http://guides.spreecommerce.org/legacy/0-11-x/extensions.html) to supply additional
+functionality or to consider using a [third party
+extension](https://github.com/spree-contrib) written by members of the Spree
+community.
+
+Calculators need to be "registered" with Spree in order to be made
+available in the admin interface for various configuration options. The
+recommended approach for doing this is via an extension. Custom
+calculators will typically be written as extensions so you need to add
+some registration logic to the extension containing the calculator. This
+will allow the calculator to do a one time registration during the
+standard extension activation process.
+
+The _CalculatorExtenion_ that is included in the Spree core is a good
+example of how you can achieve this in your own custom extensions.
+
+````bash
+ def activate
+ [
+ Calculator::FlatPercent,
+ Calculator::FlatRate,
+ Calculator::FlexiRate,
+ Calculator::PerItem,
+ Calculator::SalesTax,
+ Calculator::Vat,
+ ].each(&:register)
+ end```
+
+This calls the *register* method on the calculators that we intend to
+register. Spree provides a mechanism for extension authors to specify
+the operations for which the calculator is intended. For example, a flat
+rate calculator might be useful for all operations but another
+calculator may be appropriate only for coupons and not shipping or
+taxes.
+
+Models that are declared with *has_calculator* maintains their own set
+of registered calculators. Currently this includes *Coupons*,
+*ShippingMethods*, *ShippingRates* and *TaxRates*. The following example
+shows how to configure a calculator to make it available for use with
+*Coupons*.
+
+```bash
+ def self.register
+ super
+ Coupon.register_calculator(self)
+ end```
+
+***
+Spree automatically configures your calculators for you when using
+the basic install and/or third party extensions. This discussion is
+intended to help developers and others interested in understanding the
+design behind calculators.
+***
+
+Once your calculators have been registered correctly by your extensions,
+then they will become available as options in the appropriate admin
+screens.
+
+
+# Simplified Tax Configuration
+
+There are also minor changes to how taxes are configured. You no longer
+need to specify sales tax or VAT but you do need to choose a calculator
+type. Tax rates are configured as preferences for the calculator itself.
+
+!!!
+Your tax rates will be lost when you run the migrations. You
+will have to recreate them manually in the admin interface.
+!!!
+
+# Unified Adjustment Model
+
+Spree 0.9.0 provides a new flexible system of adjustments associated
+with orders. The *orders* table no longer has separate columns for
+*tax_total*, *ship_total*, etc. This information is now captured more
+generically as an *Adjustment*. This allows a Spree application to add
+more then one tax or shipping charge per order as well as to support new
+types of charges that might be required. For instance, some products for
+sale (like cell phones) require a separate activation fee.
+
+Adjustments come in two basic flavors: *Charges* and *Credits*. From an
+implementation perspective, they are both modeled in a single database
+table called *adjustments* and use the single table inheritance
+mechanism of Rails. Charges add to the order total, and credits work in
+the opposite direction.
+
+Orders have one or more adjustments associated with them and each
+adjustment also belongs to an adjustment source. This allows charges and
+credits to recalculate themselves when requested. Adjustments are always
+recalculated before they are saved which includes every time and order
+is updated. This provides a very flexible system by which an adjustment
+can determine that it is no longer relevant based on changes in the
+order.
+
+Consider a coupon that takes $5 off all orders over $20. If the order
+exceeds the required amount during checkout the coupon will create the
+proper adjustment. If the customer then decides to edit their cart
+(before completing checkout) then you will want to make sure that the
+coupon still qualifies. If the order total drops below the required
+amount the source of the adjustment (in this case the coupon) will have
+the ability to remove the adjustment based on its own internal logic.
+
+!!!
+There are significant changes to the database to support the
+new adjustment system. The migrations should update your legacy data and
+create the necessary tax and shipping adjustments for existing orders
+but you should backup your database before running.
+!!!
+
+# Coupons and Discounts
+
+Spree now supports a flexible coupon system. Coupons in an online store
+are virtual and can be thought of as "codes" that must be entered during
+the checkout process. Coupons serve two important functions. First, they
+determine whether or not they are eligible to be used for the offer in
+question. Second, they calculate the actual credit/discount that should
+be applied to the specific order (assuming that the eligibility
+requirement is satisfied.)
+
+## Eligibility
+
+Coupon eligibility is completely customizable on a per coupon basis.
+Eligibility is determined by the following factors.
+
+- **Start Date** - coupons can be configured to be invalid before a
+ specific date
+- **Expiration Date** - coupons can be configured so that they are not
+ usable passed a certain date
+- **Expiration Date** - coupons can be configured so that they are not
+ usable passed a certain date
+- **Number of Uses** - coupons can be restricted to an arbitrary
+ number of uses (typically a single use if there's a limit at all)
+- **Combination** - there is an option to restrict specific coupons so
+ that they cannot be combined with other coupons in the same order.
+
+Any other restriction on eligibility is intended to be provided by
+custom calculators. The *compute* method has access to the complete
+order (including shipping and other related information) and can simply
+return *nil* if the coupon is not to be applied in a specific situation
+
+***
+The next version of Spree will also provide built in filtering for
+coupons based on product properties and taxon information. This will
+provide a standard way to restrict coupons to certain types of products.
+As a workaround, you can accomplish this by hard coding restrictions in
+your calculator.
+***
+
+## Discount Calculation
+
+The *create_discount* method in *Coupon* is responsible for the actual
+calculation of the credit to be applied for the cooupon. By default,
+Spree will not allow the credit amount to exceed the item total. The
+credit adjustment associated with a coupon is subject to recalculation
+based on changes in the order. This is no different then any other
+adjustment to the order total (such as shipping or tax charges.)
+
+# RESTful Checkout
+
+There have been several minor but crucial changes to the checkout
+process. The primary motivation for these changes was to improve
+maintenance of the checkout process and to simplify checkout
+customization.
+
+## Checkout Module has been Replaced by Controller
+
+Prior to the refactoring, much of the checkout logic was contained in
+*lib/checkout.rb*. The idea was to isolate this logic from the
+*OrdersController* and to make it easier to extend. In this release we
+have just taken this another step further and made the checkout its own
+resource.
+
+## Changes to the Checkout Partials
+
+The views have been shuffled around a bit due to this refactoring. This
+shouldn't affect you too much unless you have an existing Spree
+application in which you customized some of the checkout partials. For
+instance, *app/views/orders/_billing.html.erb* has been moved to
+*app/views/checkouts/_billing.html.erb*. So you may need to
+rename your custom partials if you have any.
+
+## Additional Details
+
+For more detailed information on the nature of these changes, please see
+the [relevant
+commit](https://github.com/spree/spree/commit/ce1aad7bc25c15a794f8f5689efcdbf8c3311b7b)
+in Github.
+
+# Variant Images
+
+Some changes have been made to allow you to attach images to both the
+Product model and each individual variant for a product. The Images
+administration has been relocated from the main product details form to
+it's own tab accessible via the right hand side bar on the product
+details screen.
+
+This new admin interface enables you to select from a drop-down list
+which object (product or variant) the image represents. Note if a
+product does not contain any variants then the drop-down is not
+displayed to ensure that basic implementations are not cluttered with
+unnecessary administration options.
+
+The front-end product details interface has also been updated to filter
+the displayed images depending on which variant is selected, and the
+cart view now displays the image of the selected variant.
+
+# Improvements to image handling
+
+We've upgraded the paperclip gem to take advantage of recent changes.
+Paperclip is the library which handles creation of and access to the
+various formats of image. On top of this, we're explicitly catching
+errors in the image creation stage and returning these via the validation
+mechanism - also adding a more meaningful message in the *errors* list. This will avoid the silent
+failures that some people have experienced when they don't have image
+magick installed correctly.
+
+Another change is to store the original image's width and height: this
+info is sometimes useful when working with a set of images with different 'shapes', e.g. where your
+images might all have a width of 240 but (minor) variation on height.
+Knowing the height of the original allows you to calculate the max
+height of your images and thus to create a suitable bounding box.
+
+Finally, note that the processing tools behind paperclip can do many
+transformations on the images, such as cropping, color adjustment, … - and these can be requested by
+passing the options to paperclip, or you can run the conversions on a batch of images in
+advance of loading into Spree. Automatic cropping is particularly useful to make best use of screen
+area.
+
+# Update to SearchLogic
+
+Spree now runs with version 2.1.13 of SearchLogic. It has meant some
+minor recoding of how searches are set up and paginated, and allowed some of the existing forms to be
+simplified (by taking advantage of new functionality) and opened the door to more sophistication in
+selecting products, e.g. for handling faceted search or product groups.
+
+There's an overview of what the new SearchLogic offers on the
+[Spree
+blog](http://spreecommerce.com/blog/2009/07/30/updating-searchlogic/),
+and full documentation
+is at [rdoc.info](http://rdoc.info/projects/binarylogic/searchlogic).
+
+# Some new named scopes for products
+
+To make it easier to construct sets of products for various uses, we've
+added some more named scopes whichhelp with taxon, property, and availability of option values. The first
+kind (*taxons_id_in_tree* and
+*taxons_id_in_tree_any*) allows restriction to a set of taxons and
+their combined descendents. The property
+scope *with_property* takes a property object (or its id - the
+definition uses Rails' automatic conversion)
+and an optional argument for uniquifying the table names in complex
+queries, eg where you are filtering by
+two distinct properties. This scope does not take a property value: the
+design is that you add further
+condition(s) on the value in a subsequent scope. It will handle cases
+where the property is absent or null
+for a product. There is a simpler scope *with_property_value* for
+simpler cases.
+The option type scope (*with_option*, with its prerequisite
+*with_variant_options*) follows the pattern
+of option type object or id, and an optional table name, and is intended
+as a basis for further conditions
+on the value of that option type.
+See *lib/product_scopes.rb* for the definitions, and see
+*lib/product_filters.rb* for examples of
+their use.
+
+# Basic support for filtering of results
+
+It is often useful to cut down the results in a taxon via certain
+criteria, such as products in a price
+range or with certain properties, and sometimes you want a set of
+restrictions selectable via checkboxes etc.
+Using ideas from SearchLogic version 2, Spree now contains a basic
+framework for this kind of filtering.
+You can some basic filtering by visiting */products?taxon=1000* (unless
+you have overridden the products
+controller), where it allows you to select zero or more of a taxon's
+children and to select some price ranges and product brands.
+
+File *lib/product_filters.rb* explains the mechanism in detail, with
+several concrete examples. Basically,
+a filter definition associates a named scope with a mapping of human
+readable labels to internal labels.
+The named scope should be defined to test the relevant product
+attribute(s), and to convert a set of these
+internal labels into tests on the attributes. For example, you may want
+to filter by price range, so
+should set up labels for price ranges like 0-20, 20-50, 50-100, 100 or
+more; then define a named scope
+which maps these into a combined test on the (master) price attribute of
+products.
+
+The partial *app/views/shared/_filters.html.erb* displays a checkbox
+interface for the filters
+returned by the method *applicable_filters* for the selected taxon.
+This method allows you to control
+which filters are used for some taxon, eg a filter on fabric type may be
+required for clothing taxons,
+but not suitable for mugs etc.
+
+To use this framework, you should override and extend
+*lib/product_filters.rb* and define a suitable
+*applicable_filters* method for taxons.
+The new named scopes (above) are useful building blocks for adding
+application-specific filters.
+
+# Miscellaneous improvements
+
+## Default ship and bill addresses
+
+Spree now saves the last used bill and ship addresses for logged in
+users and uses these as the defaults
+in their next checkout. If the ship or bill addresses are edited in
+checkout, then the old addresses are
+left unchanged and new addresses saved as the defaults. This is a very
+simple form of address book.
+
+## Extension initializers
+
+It is now possible to include initializers in your extensions. This
+makes it a lot easier to
+configure extensions and to make site-specific customizations, and to
+keep them with the relevant
+extension code.
+
+## Improved handling of requests for invalid objects
+
+If a method *object_missing* for a controller, Spree will pass all
+requests for invalid objects to
+this method. This provides an easy way for applications to add specific
+handlers for invalid requests.
+For example, you may wish to direct customers back to the front page.
+If no method has been defined, Spree will use its default 404 response.
+
+## Reduced silent failures in checkout
+
+The checkout code is now more careful about returning and checking
+results from key operations, and
+a few more handlers for exceptions and invalid responses have been
+added. In normal use these should
+not occur, but they may sometimes occur if you have an error in your
+database or configuration.
+
+## Improvements to Upgrade Process
+
+The *rake spree:upgrade* task has been eliminated. It turns out there
+were some crucial flaws that caused issues when the older version of
+Spree used a different version of Rails or a different version of
+*upgrade.task* than the newer version of Spree. The rake task has been
+replaced by a new gem command:
+
+```bash
+ spree —update```
+
+
+You can also use the abbreviated form:
+
+```bash
+ spree —u```
+
+After installing a new version of the Spree gem, simply run either one
+of these commands from inside *RAILS_ROOT* (your application directory)
+and your application will be upgraded.
+
+The update process is also now less "destructive" than in previous
+versions of Spree. Instead of silently replacing crucial files in your
+application, Spree now checks the content of files it needs to replace,
+and if the old version differs, it will be saved with a *\~* suffix.
+
+This makes it easier to see when and how some file has changed - which
+is often useful if you need to update a customized version. The update
+command will also no longer copy the *routes.rb* file - the original
+version just loads the core Spree routes file, so has no need to change.
+(Recall that you can define new routes in your extensions.)
+````
diff --git a/guides/src/content/release_notes/1_0_0.md b/guides/src/content/release_notes/1_0_0.md
new file mode 100644
index 00000000000..2c6a0714364
--- /dev/null
+++ b/guides/src/content/release_notes/1_0_0.md
@@ -0,0 +1,509 @@
+---
+title: Spree 1.0.0
+section: release_notes
+order: 18
+---
+
+# Summary
+
+This is the official 1.0 Release of Spree. This is a **major** release
+for Spree, and so backwards compatibility with extensions and
+applications is not guaranteed. Please consult the [extension
+registry](https://github.com/spree-contrib) to see which extensions
+are compatiable with this release. If your extension is not yet
+compatible you should check back periodically since the community will
+be upgrading various extensions over time.
+
+!!!
+If you are upgrading from older versions of Spree you should
+perform a complete backup of your database before attempting. It is also
+recommended that you perform a test upgrade on a local development or
+staging server before attempting in your production environment.
+!!!
+
+# Namespacing
+
+A difficulty in previous versions of Spree was using it with existing
+applications, as there may have been conflicting class names between the
+Spree engines and the host application. For example, if the host
+application had a _Product_ class, then this would cause Spree's
+_Product_ class to not load and issues would be encountered.
+
+A major change within the 1.0 Release is the namespacing of all classes
+within Spree. This change remedies the above problem in the cleanest
+fashion possible.
+
+Classes such as _Product_, _Variant_ and _ProductsController_ are now
+_Spree::Product_, _Spree::Variant_ and _Spree::ProductsController_.
+Other classes, such as _RedirectLegacyProductUrl_, have undergone one
+more level of namespacing to more clearly represent what areas of Spree
+they are from. This class is now called
+_Spree::Core::RedirectLegacyProductUrl_.
+
+Constants such as _SpreeCore_ and _SpreeAuth_ are now _Spree::Core_ and
+_Spree::Auth_ respectively.
+
+## Referencing Spree routes
+
+In previous versions of Spree, due to the lack of namespacing, it was
+possible to reference routing helpers such as _product_url_ as-is in
+the controllers and views of your application and send them to the
+_ProductsController_ for Spree.
+
+Due to the namespacing changes, these references must now be called on
+the _spree_ routing proxy, so that Rails will route to Spree's
+_product_url_, rather than a _potential_ _product_url_ within an
+application. Routing helpers referencing Spree parts must now be written
+like this:
+
+```ruby
+spree.product_url
+```
+
+Conversely, to reference routes from the main application within a
+controller, view or template from Spree, you must use the _main_app_
+routing proxy like this:
+
+```ruby
+main_app.root_url
+```
+
+If you encounter errors where routing methods you think should be there
+are not available, ensure that you aren't trying to call a Spree route
+from the main application within the proxy prefix, or a main application
+route from Spree without the proxy as well.
+
+## Mounting the Spree engine
+
+When _rails g spree:install_ is run inside an application, it will
+install Spree, mounting the _Spree::Core::Engine_ component by inserting
+this line automatically into _config/routes.rb_:
+
+```ruby
+mount Spree::Core::Engine, at: "/"
+```
+
+By default, all Spree routes will be available at the root of your
+domain. For example, if your domain is http://shop.com, Spree's
+/products URL will be available at http://shop.com/products.
+
+You can customize this simply by changing the _:at_ specification in
+_config/routes.rb_ to be something else. For example, if your domain is
+http://bobsite.com and you would like Spree to be mounted at /shop, you
+can write this:
+
+```ruby
+mount Spree::Core::Engine, at: "/shop"
+```
+
+The different parts of Spree (Auth, API, Dash & Promo) all extend the
+Core's routes, and so they will be mounted as well if they are available
+as gems.
+
+# Spree Analytics
+
+The admin dashboard has been replaced with Spree Analytics. This new
+service will provide deep insight\
+into your store's ecommerce performance and sales conversion funnel.
+
+You will have to [register your
+store](http://spreecommerce.com/stores/new) with Spree Commerce. Then
+configure the Analytics Add On to generate your token. The token should
+be entered on the Admin Overview page.
+
+The original dashboard has been extracted into the [spree_dash
+gem](https://github.com/spree/spree_simple_dash) .
+
+# Command line tool
+
+We have moved the 'spree' command line tool to its own gem. This is the
+new recommended way for adding Spree to an existing Rails application.
+The tool will add the Spree gem, copy migrations, initializers and
+generate sample data.
+
+To add Spree to a Rails application you do the following:
+
+````bash
+$ gem install spree
+$ rails new my_store
+$ cd my_store
+$ spree install```
+
+
+The extension generator has also been moved to this new tool.
+
+```bash
+$ gem install spree
+$ spree extension my_ext```
+
+# Default Payment Gateways
+
+The new Spree Command Line Tool prompts you to install the default
+gateways. This adds the
+[spree\skrill](https://github.com/spree/spree_skrill) and
+[spree_usa_epay](https://github.com/spree/spree_usa_epay) gems. These
+are the Spree Commerce supported gateways for stores in the United
+States (USA ePay) and Internationally (Skrill formally Moneybookers).
+
+```bash
+ $ rails new my_store
+ $ spree install my_store
+ Would you like to install the default gateways? (yes/no) [yes]```
+
+We have moved all the gateways out of core (except bogus) to the [Spree
+Gateway Gem](https://github.com/spree/spree_gateway). You can add this
+gem to your Gemfile if you need one of those gateways.
+
+```ruby
+gem 'spree'
+
+# add to your Gemfile after the Spree gem
+gem 'spree_gateway'
+````
+
+The gateways available in the [Spree Gateway
+Gem](https://github.com/spree/spree_gateway) are community supported.
+These include Authorize.net, Stripe and Braintree and many other
+contributed gateways.
+
+# Preferences
+
+We have refactored Spree Preferences to improve performance and
+simplify code for applications and extensions. The previous interfaces have been\
+maintained so no code changes should be required. The underlying
+classes have been completely rewritten.
+
+Please see the [Spree
+blog](http://spreecommerce.com/blog/2011/12/08/spree-preferences-refactored)
+for notes on this release.
+
+# Deprecated functions
+
+## Middleware
+
+The lines for middleware in _config/application.rb_ within a host
+application are now deprecated. When upgrading to Spree 1.0 you must
+remove these two lines from _config/application.rb_:
+
+```ruby
+config.middleware.use "SeoAssist"
+config.middleware.use "RedirectLegacyProductUrl"
+```
+
+These two pieces of middleware are now automatically included by the
+`Spree::Core::Engine`.
+
+## Product
+
+_master_price_, _master_price=_, _variants?_, _variant_ are now
+officially retired. Please use _Spree::Product#price_,
+_Spree::Product#price=_, _Spree::Product#has_variants?_ and
+_Spree::Product#master_ respectively instead.
+
+## Spree::Config[:stylesheets]
+
+`Spree::Config` and `stylesheet_tags` are removed in favor for the Rails
+3.1 Asset Pipeline. See the [Asset
+Customization](http://guides.spreecommerce.org/legacy/1-0-x/asset_customization.html) for more information.
+
+Extensions looking to add stylesheets to the application should do so
+through the Asset Pipeline by making the extension an engine.
+
+## General deprecations
+
+- _Gateway.current_ is now deprecated. Use
+ _order.payment_method.gateway_ now.
+ [#747](https://github.com/spree/spree/pull/747)
+
+# Calculator
+
+## Calculator::PriceBucket is now renamed to Calculator::PriceSack
+
+The _PriceBucket_ contains Bucket keyword that conflicts with _AWS::S3_
+library which has caused few issues with Heroku deployment. If you used
+this calculator in your application, then you will need to rename it to
+_PriceSack_.
+
+# Taxation
+
+There have been several major changes to how Spree handles tax
+calculations. If you are migrating from an older version of Spree your
+previous tax configurations will not function properly and will need to
+be reconfigured before you can resume processing orders.
+
+!!!
+Be sure to backup your database before migrating. Your tax
+configuration will likely break after upgrading. You have been warned.
+!!!
+
+## Zone#match now only returns the best possible match.
+
+Previously the method would return an array of zones as long as the zone
+included the address. Now only the narrowest match is returned.
+
+## New `Order#tax_zone` method
+
+Will return the zone to be used for computing tax on the order. Returns
+the best possible zone match given the order address. In the absence of
+an order address it will return the default tax zone (if one is
+specified.)
+
+## Adjustments are now polymorphic
+
+Previously the `Adjustment` class belonged to just `Order`. Now the
+`LineItem` class can have adjustments as well. This allows Spree to
+store the amount of tax included in the price when prices include tax
+(such as VAT case.)
+
+## New `Order#price_adjustments` method
+
+Convenience method for listing all of the price adjustments in the
+order. Price adjustments are tax calculations equivalent to the amount
+of tax included in a price. These adjustments are not counted against
+the order total but are sometimes useful for reporting purposes.
+
+---
+
+You don't need to worry about price adjustments unless your prices
+include tax (such as the case with Value Added Tax.)
+
+---
+
+## New `Order#price_adjustment_totals` method
+
+Convenient method for show the price adjustment totals grouped by
+similar tax categories and rates.
+
+## Removed helpers and javascript related to VAT
+
+Prior to this version of Spree there were several helpers designed to
+show prices including tax before Spree was changed so that prices were
+expected to already include tax (when applicable.) We've removed a lot
+of stuff related to the old (more complicated) way of doing things.
+
+!!!
+One unfortunate byproduct of prices now including tax is that
+you will need to change the prices on your products if you are in a
+region that requires prices to include tax and you were not already
+including the tax in your prices.
+!!!
+
+## Removed sales tax and VAT calculators
+
+Both of these calculators have been replaced by the single calculator
+`Calculator::DefaultTax`.
+
+## Tax rates can now be included in a product price
+
+There is now a boolean checkbox for indiciating if a tax rate is
+included in the product price. The tax rate will only be considered as
+part of the product price if the product has a matching tax category.
+You can also have multiple tax rates with this designation.
+
+## New `TaxRate#adjust` method
+
+This method is responsible for calculating the price. This is basically
+an internal change but some developers may be interested to know this.
+
+---
+
+Marking a tax rate as including price is the new way to handle
+Value Added Tax (VAT) and other similar tax schemes.
+
+---
+
+# Zones
+
+There is one major change related to zones in this release. Zones can no
+longer have zone members that are themselves a zone. All zone members
+must now be a either a country or state.
+
+# Testing
+
+## The demise of Cucumber testing
+
+Cucumber is a great testing tool however it doesn't bring more values
+for testing but overhead. It is decided to opt for a light-weight
+practice of RSpec + Capybara.
+
+# Upgrading
+
+This section aims to walk you through upgrading to the newest version of
+Spree.
+
+---
+
+This steps in this guide were written while upgrading from 0.70.x
+to 1.0.0. Upgrading older versions of Spree may require some additional
+steps.
+
+---
+
+## Upgrading the Spree Gem
+
+You will want to begin the update process by updating the Spree gem in
+your Gemfile to reference version 1.0.0.
+
+```ruby
+gem 'spree', '1.0.0'
+```
+
+Next, you will need to update this gem using this command:
+
+````bash
+$ bundle update spree```
+
+***
+If you run `bundle update` instead of `bundle update spree`,
+you run the risk of having all your application dependencies updated to
+their latest version. It is recommended to only update spree during the
+upgrade process.
+***
+
+## Extensions
+
+Any Spree extensions being used will also need to be updated to a 1.0.0
+compatible version. If there is not a 1.0.0 compatible extensions
+release yet, you will need to disable that extension in order to
+continue the upgrade process.
+
+## Routes
+
+You will need to update your routes file in order for Spree's routes to
+be correctly loaded. You will need to add `mount Spree::Core::Engine,
+at: '/'` as shown below.
+
+```ruby
+#config/routes.rb
+YourStore::Application.routes.draw do
+ mount Spree::Core::Engine, at: '/'
+
+ # your application's custom routes
+ …
+end
+````
+
+If you're mounting Spree at the default root path, it is recommended to
+place your application's custom routes beneath Spree's mounted routes as
+shown in the above example. This will ensure you don't override any of
+Spree's defined routes.
+
+You may choose to mount Spree at a custom location by changing the _:at_
+option to something different, such as _at: '/shop'_.
+
+## Update config/application.rb
+
+Remove the following two lines from **config/application.rb** in your
+application:
+
+```ruby
+config.middleware.use "SeoAssist"
+config.middleware.use "RedirectLegacyProductUrl"
+```
+
+These two pieces of middleware are now automatically included by Spree.
+If you have no desire to use these pieces of middleware, you can now
+remove them by placing these two lines into your
+**config/application.rb**:
+
+```ruby
+config.middleware.delete "Spree::Core::Middleware::SeoAssist"
+config.middleware.delete
+"Spree::Core::Middleware::RedirectLegacyProductUrl"
+```
+
+## Migrations
+
+````bash
+$ bundle exec rake railties:install:migrations```
+
+Run the above command to copy over all the migrations from all the
+engines included in your application. This may also include any
+migrations from extensions or other engines.
+
+Then it is time to run any new migrations copied to your application.
+
+```bash
+$ bundle exec rake db:migrate
+````
+
+## Asset Manifest Files
+
+Remove the line requiring spree_dash from
+**app/assets/stylesheets/store/all.css**,
+**app/assets/stylesheets/admin/all.css**,
+**app/assets/javascripts/store/all.js**, and
+**app/assets/javascripts/store/all.js**
+
+## Other Tips for Upgrading
+
+- If your application defines any class decorators, you will need to
+ update these files to decorate Spree's new namespace classes. This
+ means _Product_ becomes _Spree::Product_, _Country_ becomes
+ _Spree::Country_, and so on.
+- Correct the paths to any templates you are overriding to include the
+ Spree namespace. Things such as **app/views/products/show.html.erb**
+ have now become **app/views/spree/products/show.html.erb**.
+
+# Bug fixes
+
+- Fixed issue caused by using _&:upcase_ syntax inside the _tab_
+ helper provided by _Admin::NavigationHelper_.
+ [#693](https://github.com/spree/spree/pull/693) and
+ [#704](https://github.com/spree/spree/pull/704).
+- Fixed issue where non-ASCII characters were not being correctly
+ titleized in the _tab_ helper provided by _Admin::NavigationHelper_.
+ [#722](https://github.com/spree/spree/pull/722)
+- When Thinking Sphinx was being used, a conflict would occur with its
+ _Scopes_ module and the one inside Spree.
+ [#740](https://github.com/spree/spree/pull/740)
+- Added _script/rails_ to core to allow things such as _rails
+ generate_ and _rails console_ to be used. [commit
+ b0903ea](https://github.com/spree/spree/commit/b0903ea477b63bd36c9940b5e0386e29e55f6189)
+- Performance improvements for the _best_selling_variants_ and
+ _top_grossing_variants_ methods in _Admin::OverviewController_.
+ [#718](https://github.com/spree/spree/pull/718)
+- If an admin user already exists, _rake db:admin:create_ will now ask
+ if you want to override.
+ [#752](https://github.com/spree/spree/pull/752)
+- Making a request to a URL such as
+ _/admin/products/non-existant/edit_ no longer shows a status 500
+ page. [#538](https://github.com/spree/spree/issues/538)
+- _rails g spree:install_ output is now not so excessive. [commit
+ ca4db30](https://github.com/spree/spree/commit/ca4db301e773da4ebc9d2a13e24c5d0e86dd0108)
+- The _Spree::Core::Engine_ is automatically mounted inside your
+ application when you run _rails g spree:install_. [commit
+ ba67b51](https://github.com/spree/spree/commit/ba67b514af41918bf892323c9fd685689c74667a)
+- Product _on_hand_ now takes all variants into account.
+ [#772](https://github.com/spree/spree/issues/772)
+- The translation for "Listing Products" in admin/products now is more
+ easily translatable into different languages. [commit
+ c0d5cb5](https://github.com/spree/spree/commit/c0d5cb5316715ec8aa886fab5bc0820be616d302)
+- Removed POSIX-only system calls, replaced with Ruby calls instead to
+ enable Windows compatibility.
+ [#711](https://github.com/spree/spree/issues/711) and [commit
+ ce00172](https://github.com/spree/spree/commit/ce001721a32dd84523d9504feec074db72ef3efb)
+- Improved _bundle exec rake test_app_ performance. [commit
+ 6a2d367](https://github.com/spree/spree/commit/ce001721a32dd84523d9504feec074db72ef3efb)
+- Improved permalink code, removed reliance on the
+ _rd-find_by_param_ gem.
+ [#444](https://github.com/spree/spree/issues/444) and
+ [#847](https://github.com/spree/spree/issues/847)
+- Master variant is now deleted when a product is deleted. Performance
+ with this action has also been improved.
+ [#801](https://github.com/spree/spree/issues/801)
+- An invalid coupon code on the payment screen will now show an error.
+ [#717](https://github.com/spree/spree/issues/717)
+- Products are now restocked when an order is canceled, and unstocked
+ when the order is resumed.
+ [#729](https://github.com/spree/spree/issues/729)
+- The _ffaker_ gem is now used in favor of the _faker_ gem.
+ [#826](https://github.com/spree/spree/issues/826)
+- _Spree::Config.set_ should no longer be used, please use
+ _Spree.config_ with a block: [commit
+ 5590fb3](https://github.com/spree/spree/commit/5590fb3)
+ [#801](https://github.com/spree/spree/issues/801)
+- Fix calculator dropdown bug for creating a shipping method in the
+ admin interface. [#825](https://github.com/spree/spree/issues/825)
+- Fix escaping of _order_subtotal_ in view.
+ [#852](https://github.com/spree/spree/issues/852)
diff --git a/guides/src/content/release_notes/1_1_0.md b/guides/src/content/release_notes/1_1_0.md
new file mode 100644
index 00000000000..61286f8cd46
--- /dev/null
+++ b/guides/src/content/release_notes/1_1_0.md
@@ -0,0 +1,308 @@
+---
+title: Spree 1.1.0
+section: release_notes
+order: 17
+---
+
+# Summary
+
+This is the official 1.1 Release of Spree. This is a minor release, and
+so backwards compatibility with extensions and applications is mostly
+guaranteed. There may still be some changes required for your extensions
+or applications, and so please read the changelog below to know if you
+are affected.
+
+If any particular extension your store uses is not yet compatible you
+are encouraged to alert the Spree team about it by filing an issue on
+that extension project if it's an official extension, or to submit a
+patch to that project to upgrade compatibility.
+
+# Major changes (backwards incompatibility)
+
+## Support for Rails 3.2.x only
+
+Support for Rails 3.1.x is dropped. Rails 3.2.x offers performance boost
+in development
+mode and is the first-class supported platform for 1.1.x release cycles.
+It is recommended
+that you use the latest version 3.2.3. Please upgrade your Rails before
+bumping Spree gem by modifying your Gemfile:
+
+````bash
+gem 'rails', '3.2.3'
+ group :assets do
+ gem 'sass-rails', '~> 3.2.3'
+ gem 'coffee-rails', '~> 3.2.1'
+ gem 'uglifier', '>= 1.0.3'
+ gem 'jquery-rails'
+ end```
+
+## ransack replaced meta_search
+
+`ransack` replaced `meta_search` as the primary object-based
+searching mechanism.
+Be warned that `ransack` is not fully backward-compatible with
+`meta_search` query.
+Make sure you port and test all `meta_search` queries to `ransack`
+after upgrade.
+
+## spree_product_groups now as standalone extension
+
+Product Groups component has been extracted to a standalone spree
+extension. It is recommended that if you are using this functionality to
+add the new extension to your Gemfile:
+
+gem 'spree_product_groups', git:
+'git@github.com:spree/spree_product_groups.git'
+
+## Old Theme Hook
+
+`theme_support` files are now deprecated in favor of Deface. Make sure
+you port
+all your old style hooks to Deface.
+
+## Major rewrite of Creditcard model
+
+Prior to 1.1, the `Creditcard` model contained a lot of payment
+processing code. This has [since been
+moved](https://github.com/spree/spree/commit/0e684e01b5a15ec21b34263699004ebd78692f0d)
+into the `Payment` model. If you have customized the
+`Spree::Admin::PaymentsController` or depend on any of the payment
+processing methods inside the `CreditCard` model such as `authorize`,
+`credit`, or `void`, this change may affect you.
+
+## API rewrite
+
+The API component of Spree has undergone a major rewrite in order to
+provide better support for applications wishing to interact with it
+using tools such as Backbone, or for people wishing to just generally
+access the individual components of Spree.
+
+This is currently a work-in-progress and we would appreciate feedback on
+the API on the [mailing
+list](https://groups.google.com/group/spree-user).
+
+As a part of this, the `spree/spree_api` assets are no longer
+available, and so these should be removed from the `store/all.css`,
+`store/all.js`, `store/admin/all.css` and `store/admin/all.js` assets
+located in your application.
+
+# Minor changes
+
+## Introduce Spree::Product#master_images and Spree::Product#variant_images
+
+`Spree::Product#master_images` and its alias `Spree::Product#images`
+only returns images belongs to master variant.
+
+`Spree::Product#variant_images` behaviour is changed, it is no longer
+return only images belongs to master variants but now also include all
+variants' images.
+
+## Stronger mass assignment protection
+
+As per the [Rails 3.2.3 release
+notes](https://groups.google.com/forum/?fromgroups#!topic/rubyonrails-core/X-zNKaPOVJw)
+, there is a stronger enforcement of attribute protection within Rails.
+This **may** affect your Spree application, and if it does we would
+advise [filing an issue](https://github.com/spree/spree/issues) so that
+it can be promptly fixed. While the tests for Spree itself are
+extensive, there may still be edge cases where your application goes
+that we do not have covered.
+
+## Removed images association of `Spree::Product`
+
+`Spree::Product` association with `viewable` has been moved `master`
+variant of
+the product. The change is backward compatible and require no upgrade
+modification.
+
+A call to the `images` method on a `Spree::Product` will return all the
+images associated with all the variants of this product. If you want
+just the `master` variant's images, use `product.master.images`.
+
+The version of Paperclip required by Spree 1.1 is now any version of the
+2.7.x branch of Paperclip.
+
+## Clearer separation of Spree components
+
+It was [brought to our
+attention](https://github.com/spree/spree/issues/1292) that the Core
+component of Spree depended upon things from other components, primarily
+Auth. The purpose of the Core component is that it should be usable in
+complete isolation from all other components of Spree. This feature was
+regressed during the 1..0 branch, but has [now been
+fixed](https://github.com/spree/spree/issues/1296) in the 1.1 branch.
+
+From 1.1 onwards, you will once again be able to use the Core component
+of Spree in isolation from the other components if you choose so.
+
+## Allow for easier Spree controller spec testing
+
+We have added a `Spree::Core::TestingSupport::ControllerRequests` module
+to aid in the testing of Spree controllers within not only the Spree
+components, but also within your application. [The documentation at the
+top of this
+module](https://github.com/spree/spree/blob/1-1-stable/core/lib/spree/core/testing_support/controller_requests.rb)
+should adequately describe how this works.
+
+## Deprecated functions
+
+### Spree::Zone
+
+`Spree::Zone#in_zone?` is retired, please use `Spree::Zone#include?`
+instead.
+
+### Spree::PaymentMethod
+
+`Spree::PaymentMethod.current` is retired, please use
+`current_order.payment_method` instead.
+
+Additionally, `current_gateway` is also removed.
+
+### Spree::ProductsHelper
+
+`product_price` is retired, please use `number_to_currency` instead.
+
+### Spree::HookHelper
+
+`hook` is retired, please use Deface instead.
+
+### Spree::Variant
+
+`Spree::Variant.additional_fields` has been deprecated in favour of
+using decorators and Deface. Please see
+[#1406](https://github.com/spree/spree/issues/1406) for more
+information.
+
+# Patches
+
+## Version bumps
+
+- Paperclip version has been bumped to 2.7.0.
+ [#1148](https://github.com/spree/spree/issues/1148) +
+ [#1152](https://github.com/spree/spree/issues/1152)
+- Stringex version has been bumped to 1.3.2 to prevent 1.3.1 from
+ being used, as that release contained a bug.
+- nested_set version has been bumped to 1.7.0.
+ [9b7eda3](https://github.com/spree/spree/commit/9b7eda361dcb001ffa5ad20cd124428d95da21d6)
+- jquery-rails version has been bumped to \~> 2.0.0.
+ [d4b3d7](https://github.com/spree/spree/commit/d4b3d76491)
+- deface version has been bumped to 0.8.0.
+ [e571ed](https://github.com/spree/spree/commit/e571edd86dfadab48e4243caf4fd850a3fd10553)
+- highline version has been bumped to 1.6.11.
+ [45f5e2](https://github.com/spree/spree/commit/45f5e2)
+
+## Other fixes
+
+- Added `admin/orders/address_states.js` to precompile list.
+ [#754](https://github.com/spree/spree/issues/754)
+- Added initializer to warn about orphaned preferences. [commit
+ 4f2669](https://github.com/spree/spree/commit/4f2669)
+- Address.default will no longer provide a nil value if the default
+ country is deleted.
+ [#1142](https://github.com/spree/spree/issues/1142) +
+ [#997](https://github.com/spree/spree/issues/997)
+- Fix undefined_method to_d for PriceSack Shipping.
+ [#1156](https://github.com/spree/spree/issues/1156)
+- Fixed rounding calculation bug for VAT.
+ [#1128](https://github.com/spree/spree/issues/1128) +
+ [#1172](https://github.com/spree/spree/issues/1172)
+- Allow `:error` key to be passed to `link_to_delete` .[#1169](https://github.com/spree/spree/issues/1169)
+- Fix issue where assigning a price such as"$5" to a variant
+ caused it to set the price to 0.
+ [#1173](https://github.com/spree/spree/issues/1173)
+- Product names longer than 50 characters are now truncated.
+ [#1171](https://github.com/spree/spree/issues/1171)
+- Fix issue where preferences set to `false` were not being saved.
+ [#1177](https://github.com/spree/spree/issues/1177)
+- Fix incorrect variable name in `script/rails` file inside extension
+ generator. [#1135](https://github.com/spree/spree/issues/1135)
+- Acknowledge Spree's own locale settings before `Rails.application.config.i18n.default_locale` for Spree's
+ locale details. [#1184](https://github.com/spree/spree/issues/1184)
+- Fix issue where preferences set to an empty string were not being
+ saved. [#1187](https://github.com/spree/spree/issues/1187)
+- Set `default_url_options` in `mail_settings.rb` so that it doesn't
+ need to be set manually for each environment or mailer.
+ [#1188](https://github.com/spree/spree/issues/1188)
+- Correctly fire events for content paths and actions.
+ [#1141](https://github.com/spree/spree/issues/1141)
+- Allow preferences with a type of `:text`.
+- Image settings (such as width & height) are now configurable via the
+ Admin interface.
+ [7d987fe](https://github.com/spree/spree/commit/7d987fe0e86d799d0896e123e638745201e7adb8)
+- Fix bug where `Payment#build_source` would fail dependent on the
+ ordering of the hash passed in.
+ [#981](https://github.com/spree/spree/issues/981)
+- Fix issue where states javascript include would be prefixed with
+ asset host when alternate asset host was configured.
+ [#1213](https://github.com/spree/spree/issues/1213)
+- Fix issue where `Promotion#products` would return no products due
+ to incorrect class specification.
+ [#1237](https://github.com/spree/spree/issues/1237)
+- Order email addresses are now validated with the Mail gem.
+ [#1238](https://github.com/spree/spree/issues/1238)
+- Attempt to access `/admin/products/{id}` will now redirect to
+ `/admin/products/{id}`.
+ [#1239](https://github.com/spree/spree/issues/1239)
+- Show 'N/A' for tax category on `/admin/tax_rates` if a tax rate
+ doesn't have a tax category.
+ [#535](https://github.com/spree/spree/issues/#535)
+- Fix issue where incorrect order assignment was breaking return
+ authorization creation.
+ [#1107](https://github.com/spree/spree/issues/1107)
+ [#1109](https://github.com/spree/spree/issues/1109)
+ [#1149](https://github.com/spree/spree/issues/1149)
+- Fix issue where under certain circumstances users were able to view
+ other people's order information.
+ [#1243](https://github.com/spree/spree/issues/1243)
+- Fix issue where searching for orders by SKU was broken in admin
+ backend. [#1259](https://github.com/spree/spree/issues/1259)
+- Logout when "Remember me" was checked for login now will actually
+ log a user out. [#1257](https://github.com/spree/spree/issues/1257)
+- `Spree::UrlHelpers` has moved to `Spree::Core::UrlHelpers`
+ [3bf5df](https://github.com/spree/spree/commit/3bf5df57e3474322dc484eb57ca5ee9098bd9454)
+- Preview buttons on the admin dashboard are now hidden once the
+ dashboard has been configured.
+ [#1271](https://github.com/spree/spree/issues/1271)
+- Slightly alter permalink code so that it does not conflict on
+ similar names. Current permalinks will not be affected.
+ [#1254](https://github.com/spree/spree/issues/1254)
+- Don't allow payments to be created in admin backend unless payment
+ methods have been defined.
+ [#1269](https://github.com/spree/spree/issues/1269)
+- `Address#full_name` will now return a string with no extra spaces
+ around it. [#1298](https://github.com/spree/spree/issues/1298)
+- Ensure `StatesController` always returns JS response.
+ [#1304](https://github.com/spree/spree/issues/1304)
+- Fix issue where checkbox for "Use Billing Address" was not being
+ checked for an order in admin backend when it was in the frontend.
+ [#1290](https://github.com/spree/spree/issues/1290)
+- Fix issue where a shipping method could not be updated.
+ [#1331](https://github.com/spree/spree/issues/1331)
+- Allow layout to be customized based on a configuration setting.
+ [#1355](https://github.com/spree/spree/issues/1355)
+- `Rails.application.config.assets.debug` is no longer hardcoded to
+ `false` in Spree. Set this variable at your discretion inside your
+ `config/application.rb` from now on.
+ [#1356](https://github.com/spree/spree/issues/1356)
+- Added `gem_available?` method to `BaseHelper` to be able to check
+ if an extension is available.
+ [#1241](https://github.com/spree/spree/issues/1241)
+- Fixed potential bug where `Activator.active` scope may have not been
+ returning activators that were currently active.
+ [#1343](https://github.com/spree/spree/issues/1343)
+- Fix incorrect route issue when updating a return authorization.
+ [#1343](https://github.com/spree/spree/issues/1343)
+- Fixed issue where "Add to Cart" button may not work on IE7.
+ [#1397](https://github.com/spree/spree/issues/1397)
+- Fixed issue where non-price characters were being stripped from
+ prices. [#1392](https://github.com/spree/spree/issues/1392)
+ [#1400](https://github.com/spree/spree/issues/1400)
+- Limit ProductsController#show to only show active products.
+ [#1390](https://github.com/spree/spree/issues/1390)
+- Spree::Calculator::PerItem will now calculate per-item, rather than
+ per-line-item. This means that if you have a per-item calculator
+ costing $1 and 3 line items with quantity of 5 that would be $15,
+ rather than $3.
+ [#1414](https://github.com/spree/spree/issues/1414)
+````
diff --git a/guides/src/content/release_notes/1_1_4.md b/guides/src/content/release_notes/1_1_4.md
new file mode 100644
index 00000000000..f1793ff4a3d
--- /dev/null
+++ b/guides/src/content/release_notes/1_1_4.md
@@ -0,0 +1,126 @@
+---
+title: Spree 1.1.4
+section: release_notes
+order: 16
+---
+
+# Summary
+
+This is the official 1.1.4 Release of Spree. This is a trivial release,
+and so backwards compatibility with extensions and applications is
+guaranteed.
+
+This will be the final release of the 1.1.x branch for Spree. We would
+recommend upgrading to 1.2.x as soon as possible.
+
+# Major fixes
+
+## Migrations
+
+This new version of Spree most likely contains new migrations. Please
+install them and run them with this command:
+
+````bash
+bundle exec rake railties:install:migrations
+bundle exec rake db:migrate```
+
+## Rails dependency upgraded
+
+Due to a [security
+bug](http://weblog.rubyonrails.org/2012/11/12/ann-rails-3-2-9-has-been-released/)
+within Rails, we have upgraded the dependency of Rails to be 3.2.9. You
+will not be able to use Rails 3.2.8 with Spree 1.1.4. It's highly
+encouraged you upgrade your Rails version.
+
+## JavaScript changes
+
+If you are upgrading from an older version of Spree 1.1, please make
+sure that your JavaScript requires are correct within
+`app/assets/javascripts`.
+
+Inside `app/assets/javascripts/admin/all.js`, the directives should be
+this:
+
+```bash
+//= require jquery
+//= require jquery_ujs
+
+//= require admin/spree_core
+//= require admin/spree_auth
+//= require admin/spree_promo
+
+//= require_tree .
+````
+
+Inside `app/assets/javascripts/store/all.js`, the directives should be
+this:
+
+```bash
+//
+//= require jquery
+//= require jquery_ujs
+
+//= require store/spree_core
+//= require store/spree_auth
+//= require store/spree_promo
+
+//= require_tree .
+```
+
+These changes are important, as they fix [Issue
+#1854](https://github.com/spree/spree/issues/1854).
+
+# Minor fixes
+
+- Ensure there is a valid state during the checkout.
+ [#1770](https://github.com/spree/spree/issues/1770)
+- Use GET request for /user/logout.
+ [#1663](https://github.com/spree/spree/issues/1663) and
+ [#1812](https://github.com/spree/spree/issues/1812)
+- Use product description if no meta description is provided.
+ [#1811](https://github.com/spree/spree/issues/1811)
+- Don't update _product.count_on_hand_ if track inventory labels is
+ disabled. [#1820](https://github.com/spree/spree/issues/1820)
+- Set payment state to _credit_owed_ if order is cancelled and
+ nothing is shipped.
+ [#1513](https://github.com/spree/spree/issues/1513)
+- Added _ignore_types_ option for _flash_messages_ to ignore
+ specific types of flash messages.
+ [#1835](https://github.com/spree/spree/issues/1835)
+- Added large image placeholder.
+ [#1828](https://github.com/spree/spree/issues/1828)
+- Assets are no longer precompiled during the installation process.
+ [#1854](https://github.com/spree/spree/issues/1854)
+- API responses now return the correct content type.
+ [#1866](https://github.com/spree/spree/issues/1866)
+- I18n-ify order cancel, confirmation and shipment emails.
+ [#1884](https://github.com/spree/spree/issues/1884)
+- Preferences are now checked for in database before falling back to
+ their defined-defaults.
+ [#1500](https://github.com/spree/spree/issues/1500)
+- Deleted shipping methods are no longer visible in the admin backend.
+ [#1847](https://github.com/spree/spree/issues/1847) and
+ [#1967](https://github.com/spree/spree/issues/1967).
+- Moved admin JS translations out to their own partial
+ (_core/app/views/admin/shared/\_translations.html.erb_)
+ [#1906](https://github.com/spree/spree/issues/1906)
+- Fixed error where admin page for tax rates would not load if a tax
+ rate didn't have a valid zone.
+ [#2019](https://github.com/spree/spree/issues/2019)
+- Fixed incorrect PriceSack calculation
+ [#2055](https://github.com/spree/spree/issues/2055)
+- InventoryUnit#restock_variant will no longer alter
+ _variant.count_on_hand_ if inventory tracking is disabled.
+- Orders with all return authorizations received are now marked as
+ returned. [#1714](https://github.com/spree/spree/issues/1714) and
+ [#2097](https://github.com/spree/spree/issues/2097)
+- _Product.in_taxons_ will now only return unique products.
+ [#1917](https://github.com/spree/spree/issues/1917) and
+ [#1974](https://github.com/spree/spree/issues/1974) and
+ [#1962](https://github.com/spree/spree/issues/1962)
+- _Product.on_hand_ no longer sums deleted variants.
+ [#2112](https://github.com/spree/spree/issues/2112)
+- EXIF data is now stripped from uploaded JPEGs
+ [#2145](https://github.com/spree/spree/issues/2145)
+- OrdersController#show now requires SSL, as it contains sensitive
+ data. [#2164](https://github.com/spree/spree/issues/2164).
diff --git a/guides/src/content/release_notes/1_2_0.md b/guides/src/content/release_notes/1_2_0.md
new file mode 100644
index 00000000000..c2829d87f26
--- /dev/null
+++ b/guides/src/content/release_notes/1_2_0.md
@@ -0,0 +1,234 @@
+---
+title: Spree 1.2.0
+section: release_notes
+order: 15
+---
+
+Spree 1.2.0 introduces some fairly major changes in the basic
+architecture of Spree, as well as minor alterations and bugfixes.
+
+Due to the long development cycle of Spree 1.2 in parallel with
+continuing development of the 1.1 branch, there may be features released
+in 1.2 that are already present in 1.1.
+
+# Summary
+
+There were two major topics addressed within this release of Spree:
+custom authentication and better checkout customization.
+
+The first was the ability to use Spree in conjunction with an
+application that already provided its own way to authenticate users. Due
+to how Spree was architected in the past, this was not as easy as it
+could have been. In this release of Spree, the auth component of Spree
+has been removed completely and placed into a separate extension called
+[spree_auth_devise](https://github.com/spree/spree_auth_devise). If
+you wish to continue using this component of Spree, you will need to
+specify this extension as a dependency in your Gemfile. See [Issue
+#1512](https://github.com/spree/spree/pull/1512) for more detail about
+the customization.
+
+The checkout process has always been hard to customize within Spree, and
+that has generated complaints in the past. We are pleased to report in
+the 1.2 release of Spree that this has been substaintially easier with a
+new checkout DSL that allows you to re-define the checkout steps in a
+simple manner. For more information about this, please see [Issue
+#1418](https://github.com/spree/spree/pull/1418) and [Issue
+#1743](https://github.com/spree/spree/pull/1743).
+
+Along with these two major issues, there were also a ton of minor
+improvements and bug fixes, explained in detail below.
+
+# Major changes (backwards incompatibility)
+
+## spree_auth removal
+
+Authentication is disabled by default within Spree as of this release,
+with the application supposed to be providing its own authentication. If
+you are upgrading an existing Spree installation or just want it to
+work, you can achieve the behaviour of a 1.1 installation by adding
+"spree_auth_devise" to your Gemfile.
+
+````ruby
+gem 'spree_auth_devise', git:
+"git://github.com/spree/spree_auth_devise"```
+
+For more information on how to customize authentication, please see the
+[Authentication
+guide](http://guides.spreecommerce.org/authentication.html).
+
+## State machine customizations
+
+Customizing the state machine within Spree now does not require you to
+override the entire *state_machine* definition within Spree's *Order*
+model. Instead, you are provided with the ability to define the "next"
+events for *Order* objects like this:
+
+```ruby
+Spree::Order.class_eval do
+ checkout_flow do
+ go_to_state :address
+ go_to_state :delivery
+ go_to_state :payment, if: lambda { payment_required? }
+ go_to_state :confirm, if: lambda { confirmation_required? }
+ go_to_state :complete
+ remove_transition from: :delivery, to: :confirm
+ end
+end```
+
+For more information about customizing the checkout process within
+Spree, please see the [Checkout
+guide](http://guides.spreecommerce.org/checkout.html).
+
+# Minor changes
+
+## has_role?, api_key and roles methods now namespaced
+
+On a usr object, the *has_role?* method is now called
+*has_spree_role?*, the *api_key* method is called *spree_api_key*
+and the *roles* association is now called *spree_roles*. This allows
+for applications to define their own *has_role?*, *api_key* and
+*roles* methods without them conflicting with the methods defined within
+Spree.
+
+## Introduce Spree::Product#master_images and Spree::Product#variant_images
+
+*Spree::Product#master_images* and its alias *Spree::Product#images*
+only returns images belongs to master variant.
+
+*Spree::Product#variant_images* behaviour is changed, it is no longer
+return only images belongs to master variants but now also include all
+variants' images.
+
+## Spree::Zone#country_list renamed to #zone_member_list
+
+Please be noted that the underlying logics remain intact.
+
+## Spree::Creditcard renamed to Spree::CreditCard
+
+All occurences of Creditcard are changed to CreditCard to better follow
+Rails naming conventions.
+
+## Assert that ImageMagick is installed during Spree installation
+
+When installing Spree, if the ImageMagick library was not installed on
+the system, then the *identify* command that Paperclip uses would fail
+and sample product images would not appear.
+
+There is now a [check in
+place](https://github.com/spree/spree/commit/a6deb62) to ensure that
+ImageMagick is installed before Spree is.
+
+## Sass variables can now be overriden
+
+The variable definitions for the sass files of Spree have been moved to
+a separate file within Core called
+*app/assets/stylesheets/store/variables.css.scss*. You can override this
+file within your application in order to re-define the colors and other
+variables that Spree uses for its stylesheets.
+
+## Added support for serialized preferences
+
+Preferences can now be defined as serialized. See
+[7415323](https://github.com/spree/spree/commit/7415323) for more
+information.
+
+## New UI for defining taxons and option types for a product
+
+Rather than having the taxons and option types related to a product on
+two completely separate pages, they are now included on the product edit
+form. This functionality is provided by the Select2 JavaScript plugin,
+and will fall back to a typical select box if JavaScript is not
+available.
+
+## Using the Money gem to display currencies
+
+In earlier versions of Spree, we used *number_to_currency* to display
+prices for products. This caused a problem when somebody selected a
+different I18n locale, as the prices would be displayed in their
+currency: 20 Japanese Yen, rather than 20 American Dollars, for
+instance.
+
+To fix this problem, we're now parsing the prices through the Money gem
+which will display prices consistently across all I18n locales. To now
+change the currency for your site, go to Admin, then Configuration, then
+General Settings. Changing the currency will only change the currency
+symbol across all prices of your store.
+
+Note: After the 1.2.0 release, more options to format the currency
+output have been introduced. Specifically the position of the currency
+symbol which is fixed to :before in 1.2.0 can be adjusted. To achieve
+this in the 1.2.0, you may wish to override the Spree::Money.to_s
+function.
+
+# Tiny changes / bugfixes
+
+## General
+
+- Replaced uses of deprecated jQuery method live with on
+ ([273987d](https://github.com/spree/spree/commit/273987d))
+
+## API
+
+- Added ability to make shipments ready and declare them shipped
+ ([e933f36](https://github.com/spree/spree/commit/e933f36))
+- Added payments and shipments information to order data returned from
+ the API ([8c2aaef](https://github.com/spree/spree/commit/8c2aaef))
+- Add ability to credit payments through the order
+ ([320599a](https://github.com/spree/spree/commit/320599a))
+- Added the ability to update an order
+ ([ab1e23b](https://github.com/spree/spree/commit/ab1e23b))
+- Added the ability to search for products
+ ([024b22a](https://github.com/spree/spree/commit/024b22a))
+- Added zones
+ ([b522703](https://github.com/spree/spree/commit/b522703)
+ [#1615](https://github.com/spree/spree/issues/1615))
+- Allow orders to be paginated
+ ([873e9f8](https://github.com/spree/spree/commit/873e9f8))
+
+## Core
+
+- Added more products in sample data
+ ([6b66b3a](https://github.com/spree/spree/commit/6b66b3a))
+- Sample products now have product properties associated with them
+ ([c71ef3a](https://github.com/spree/spree/commit/c71ef3a))
+- Product images are now sorted by their position
+ ([5115377](https://github.com/spree/spree/commit/5115377))
+- The configuration menu/sidebar will now display on
+ */admin/shipping_categories/index*
+ ([459b5d0](https://github.com/spree/spree/commit/459b5d0))
+- Datepickers are now localized in the admin backend
+ ([e5f1680](https://github.com/spree/spree/commit/e5f1680))
+- You can now click on a product name in */admin/products/* list to go
+ to that product
+ ([f2e9cc3](https://github.com/spree/spree/commit/f2e9cc3))
+- Defining of Spree::Image helper methods is done on the fly now
+ ([ff0c837](https://github.com/spree/spree/commit/ff0c837))
+- Account for purchased units in stock validation
+ ([7c4cd77](https://github.com/spree/spree/commit/7c4cd77))
+- Sort all order adjustments by their created_at timestamp
+ ([aef2fd9](https://github.com/spree/spree/commit/aef2fd9))
+- Use *ransack* method on classes rather than *search*, as this may be
+ defined by an extension
+ ([4fabc52](https://github.com/spree/spree/commit/4fabc52))
+- Fix issue where redeeming a coupon code for a free product did not
+ apply the coupon
+ ([2459f75](https://github.com/spree/spree/commit/2459f75)
+ [#1589](https://github.com/spree/spree/issues/1589))
+- Specify class name for all *belongs_to* associations
+ ([94a6859](https://github.com/spree/spree/commit/94a6859))
+- Don't rename users table if *User* constant is defined
+ ([c77c822](https://github.com/spree/spree/commit/c77c822))
+- Removed all references to link_to_function throughout Spree,
+ replace with 100% JavaScript
+- Moved *get_taxonomies* helper method out of
+ *Spree::Core::ControllerHelpers* into *Spree::ProductsHelper*
+ ([980348a](https://github.com/spree/spree/commit/980348a))
+
+## Promo
+
+- Coupon code input is now displayed on the cart page
+ ([c030671](https://github.com/spree/spree/commit/c030671))
+- Only acknowledge coupon codes for promos that have the
+ 'spree.checkout.coupon_code_added' event
+ ([4158979](https://github.com/spree/spree/commit/4158979))
+````
diff --git a/guides/src/content/release_notes/1_2_2.md b/guides/src/content/release_notes/1_2_2.md
new file mode 100644
index 00000000000..b080e70a6e7
--- /dev/null
+++ b/guides/src/content/release_notes/1_2_2.md
@@ -0,0 +1,112 @@
+---
+title: Spree 1.2.2
+section: release_notes
+order: 14
+---
+
+Spree 1.2.2 is the latest Spree release in the 1.2.x branch. This
+release contains minor improvements and bug fixes. Compatibility with
+extensions is mostly guaranteed, however there may be edge cases. If you
+find one of these, please [file an
+issue](https://github.com/spree/spree/blob/master/.github/CONTRIBUTING.md).
+
+## Major changes
+
+### Migrations
+
+This new version of Spree contains new migrations. Please install them
+and run them with this command:
+
+```bash
+bundle exec rake railties:install:migrations
+bundle exec rake db:migrate
+```
+
+### API changes
+
+We have changed some aspects of the API component in Spree. For a
+detailed list of these changes, please refer to the [Changes page on our
+API site](http://api.spreecommerce.com/changes/)
+
+## Other changes
+
+- Switched from using the `acts_as_nested_set` gem to using
+ `awesome_nested_set_gem`, which is an optimized version of the
+ same gem. [#1927](https://github.com/spree/spree/issues/1927)
+- Renamed InventoryUnit.backorder to InventoryUnit.backordered
+ [commit](https://github.com/spree/spree/commit/6cc3da52daa3ef57423c0ddbeb4211980ea3103d)
+- Fix issue with installer when running on `i386-mingw32` platform
+ PCs. [#1903](https://github.com/spree/spree/issues/1903)
+- All adjustments, not just optional ones, are locked on an order
+ completion.
+ [commit](https://github.com/spree/spree/commit/1a9b25c0a4232f02f25ab0d7bc80250e045bf8fa)
+- Fix issue where currently selected currency wasn't displayed
+ correctly on the "General Settings" page.
+ [commit](https://github.com/spree/spree/commit/a46455afd8e4691aaf789b4639da8967277f1916)
+- Added `:currency_symbol_position` configuration option, to
+ configure if currency symbol goes before or after amount.
+ [commit](https://github.com/spree/spree/commit/575af696f39f9ea408fc9f4082bccff4e7fa4e05)
+ [#1911](https://github.com/spree/spree/issues/1911)
+- Allow for calling `save_permalink` manually to recalculate a
+ permalink. [#1920](https://github.com/spree/spree/issues/1920)
+- Disable double-clicking delete links on line items.
+ [#1934](https://github.com/spree/spree/issues/1934)
+- Add `per_page` parameter for API orders.
+ [#1949](https://github.com/spree/spree/issues/1949)
+- Fixed payment banner not being dismissed when asked.
+ [#1952](https://github.com/spree/spree/issues/1952)
+- Prevent double-submit on checkout confirm step.
+ [commit](https://github.com/spree/spree/commit/84f91aa875d41fa1e77646c9cc25b321dab050cc)
+- Allow an order with no shipments to be canceled.
+ [#1989](https://github.com/spree/spree/issues/1989)
+- Added `Product#available?`
+ [#2002](https://github.com/spree/spree/issues/2002)
+- Allow case-insensitive coupon codes
+ [#2009](https://github.com/spree/spree/issues/2009) and
+ [#2012](https://github.com/spree/spree/issues/2012)
+- Fix problem with currencies whose sub-units are not in hundreds.
+ [#2030](https://github.com/spree/spree/issues/2030)
+- Payments are no longer processed if `Order#payment_required?`
+ returns false. [#2028](https://github.com/spree/spree/issues/2028)
+- Expired promotions are now excluded from
+ `Product#possible_promotions`
+ [#2058](https://github.com/spree/spree/issues/2058)
+- Load `Spree::AuthenticationHelpers` during a `to_prepare` hook
+ [#2076](https://github.com/spree/spree/issues/2076)
+- Updating a line item's quantity and then clicking the checkout
+ button will now persist the update.
+ [#2086](https://github.com/spree/spree/issues/2086)
+- `with_option_value` and `taxons_name_eq` scopes on `Product`
+ will now return `Product` objects, rather than IDs.
+ [#2082](https://github.com/spree/spree/issues/2082)
+- Searcher class instances in `HomeController`, `ProductsController`
+ and `TaxonsController` will now have access to the current user
+ object. [#2089](https://github.com/spree/spree/issues/2089)
+- `spree_products.count_on_hand` and
+ `spree_variants.count_on_hand` columns can now be set to `NULL`
+ to indicate an infinite supply.
+ [#2096](https://github.com/spree/spree/issues/2096)
+- Orders are marked "returned" if all return authorizations are
+ received [#1714](https://github.com/spree/spree/issues/1714)
+ [#2099](https://github.com/spree/spree/issues/2099)
+- Unique products are returned from `Product.in_taxon` scope
+ [#1917](https://github.com/spree/spree/issues/1917)
+ [#1974](https://github.com/spree/spree/issues/1974)
+ [#1962](https://github.com/spree/spree/issues/1962)
+- `Product.on_hand` scope no longer sums deleted variants.
+ [#2112](https://github.com/spree/spree/issues/2112)
+- `Payment#capture` now does nothing for payments marked as
+ "completed" [#2119](https://github.com/spree/spree/issues/2119)
+- I18nify "Order Adjustments" text
+ [#2123](https://github.com/spree/spree/issues/2123)
+- EXIF data is now stripped from JPEGs
+ [#2142](https://github.com/spree/spree/issues/2142)
+ [#2145](https://github.com/spree/spree/issues/2145)
+- Only eligible promotions are now included in `promo_total` on
+ `Order` objects.
+ [commit](https://github.com/spree/spree/commit/74a7914903b9d7dac77e0cbd38b1919fb3396254)
+- Promotion usage count is now visible on a promotion's edit page.
+ [#2193](https://github.com/spree/spree/issues/2193)
+- The user picker for the promotions backend is now functional again,
+ after being broken accidentally in the previous release.
+ [#1890](https://github.com/spree/spree/issues/1890)
diff --git a/guides/src/content/release_notes/1_3_0.md b/guides/src/content/release_notes/1_3_0.md
new file mode 100644
index 00000000000..a262817046f
--- /dev/null
+++ b/guides/src/content/release_notes/1_3_0.md
@@ -0,0 +1,170 @@
+---
+title: Spree 1.3.0
+section: release_notes
+order: 13
+---
+
+Spree 1.3.0 is the first release of the 1.3.x branch of Spree. This release contains some major non-breaking changes, which are covered in the release notes below.
+
+Due to the long development cycle of Spree 1.3 in parallel with continuing development of the 1.1 branch, there may be bug fixes released in 1.3 that are already present in the latest release of 1.2.
+
+Here's a quick summary of the major features in this release:
+
+- Admin redesign
+- Currency support for variants
+
+## Major changes
+
+### Admin redesign
+
+Alexey Topolyanskiy has done some amazing work performing a makeover for the admin backend for Spree, something that has been long overdue!
+
+
+
+### Currency support
+
+Thanks to work by Gregor MacDougall and the team at Free Running Technologies, Spree's Variant model now is able to keep track of a different price for different currencies.
+
+## Minor changes
+
+### Remove child node from API responses
+
+The API has previously returned data with a child node within its responses. Take this example from `/api/products`:
+
+```ruby
+{
+ [products]() [
+ {
+ [product]() {
+ [id]() 1,
+ …
+ }
+ }]
+}
+```
+
+This response will now be returned without the child nodes, like this:
+
+```ruby
+{
+ [products]() [
+ {
+ [id]() 1,
+ …
+ }
+ ]
+}
+```
+
+### API requests can now ask for different Rabl templates
+
+If you would like to make a request to the API use a different Rabl template, pass the template's name within the request as an `X-Spree-Template` header or _template_ parameter, and Spree will automatically use that template to render the response.
+
+For instance, if you have a template at `app/views/spree/api/products/special_show.v1.rabl`, to render that template the `X-Spree-Template` header or `template` parameter would need to be simply "special_show". This will allow you to customize the responses from Spree's API extremely easily.
+
+### Jirafe false positive conversions
+
+We've had a number of reports of Jirafe false positive conversions within Spree
+([#2273](https://github.com/spree/spree/issues/2273)
+[#2211](https://github.com/spree/spree/issues/2211) and
+[#2157](https://github.com/spree/spree/issues/2157))
+
+This issue should now be fixed based on [this commit](https://github.com/spree/spree/commit/50bc65f78d07453fea85ae034748007946bd27bd)
+
+## Other changes
+
+- Fix issue where return authorization form would crash if a variant
+ had an ID
+ with a large value
+ [commit](https://github.com/spree/spree/commit/820a1c023d915f9d2c972c04c5641b5d823ab508)
+- Don't process payments if payments are not required [#2025](https://github.com/spree/spree/issues/2025)
+- Payments are now applied one at a time until the order total is met,
+ rather
+ than processing all payments at the same time.
+ [#1954](https://github.com/spree/spree/issues/1954)
+ [#2008](https://github.com/spree/spree/issues/2008)
+- Exclude expired promotions from Product#possible_promotions
+ [#2058](https://github.com/spree/spree/issues/2058)
+- Pass all changes to Variant#count_on_hand to Variant#on_hand=
+ to ensure
+ backorders are processed correct
+ [commit](https://github.com/spree/spree/commit/d6c1183095125a946e8f6f1078ce0ee7487687b9)
+- Use select2 for properties and option types on prototype form to
+ display
+ options better. [#2077](https://github.com/spree/spree/issues/2077)
+- Clicking 'Checkout' on the cart page will now update the order and
+ redirect to
+ the address form, rather than just redirecting to the address form.
+ [#2086](https://github.com/spree/spree/issues/2086)
+- The searcher class now has access to the current user.
+ [#2089](https://github.com/spree/spree/issues)
+- Allow anonymous requests to the API.
+ [commit](https://github.com/spree/spree/commit/456cadf5ff858ecac75646ca6b592be384a07396)
+- Don't clear mail method or payment method passwords if they're not
+ included in
+ a request. [#2094](https://github.com/spree/spree/issues/2094)
+- An order is marked as returned automatically if all return
+ authorizations are
+ received. [#1714](https://github.com/spree/spree/issues/1714)
+ [#2099](https://github.com/spree/spree/issues/2099)
+- Added _on_demand_ field for variants, indicating that the variant
+ is an "on
+ demand" item. [#1940](https://github.com/spree/spree/issues/1940)
+ [#2080](https://github.com/spree/spree/issues/2080)
+- Product.in_taxons does not return duplicate products
+ [commit](https://github.com/spree/spree/commit/75fa3623b61e22fcde395b7f9900e23038361df9)
+- Spree::Product.on_hand no longer sums with deleted variants
+ [#2112](https://github.com/spree/spree/issues/2112)
+- Payment#capture! will no longer work on completed payments.
+ [#2119](https://github.com/spree/spree/issues/2119)
+- Fix "Order adjustments" translation
+ [#2123](https://github.com/spree/spree/issues/2123)
+- Order#create_tax_charge! is called whenever a line item is added
+ or removed
+ from an order. [#1418](https://github.com/spree/spree/issues/1418)
+- Don't allow
+ void_transaction! to operate on a payment which is already void.
+ [#2119](https://github.com/spree/spree/issues/2119)
+- Strip EXIF data from images [#2142](https://github.com/spree/spree/issues/2142)
+- Display promotion usage data in admin
+ [#2193](https://github.com/spree/spree/issues/2193)
+- Remove display_on option for Payment Methods.
+ [#1918](https://github.com/spree/spree/issues/1981)
+- Add Order#variants, to get a list of variants associated with an order.
+ [#2195](https://github.com/spree/spree/issues/2195)
+- Fix issue when trying to move taxon to the bottom of the tree
+ [#2180](https://github.com/spree/spree/issues/2180)
+- Show only one validation message for an order's email if left blank on the
+ checkout [#2214](https://github.com/spree/spree/issues/2214)
+- Taxonomies can now be reordered
+ [#2237](https://github.com/spree/spree/issues/2237)
+- Order#merge no longer uses Order#add_variant. For an
+ explanation, [see this
+ commit](https://github.com/spree/spree/commit/8569ed5d98e354285ad6ccbd366444fd31e773f8)
+- Orders with promotions that "zero" the order total will no longer
+ skip
+ delivery step if that step is required.
+ [#2191](https://github.com/spree/spree/issues/2191)
+- Jirafe analytics can now be edited after registration
+ [#2238](https://github.com/spree/spree/issues)
+- awesome_nested_set version has been bumped to 2.1.5
+ [commit](https://github.com/spree/spree/commit/3bdd22fedda456308f20f0817155590fab231e96)
+- Order details page no longer errors if a payment's credit card type
+ is blank
+ [#2282](https://github.com/spree/spree/issues/2282)
+- No longer transition to complete if payment is required and there
+ are payments
+ due.
+ [commit](https://github.com/spree/spree/commit/8639bbcc3b1909a339b0a60da239a49b95baa760)
+- Refactored preference fetching from the preference store
+ [commit](https://github.com/spree/spree/commit/bfcb5b29b3e29c3d451b14ab39e2b502ea93f6a4)
+- Order#checkout_steps will now always include the "Complete" step.
+ [commit](https://github.com/spree/spree/commit/227f86ff57735e0e0637a0896006ff79fe8e0a6d)
+- Allow "first order for user" promotion to work with guest users as
+ well
+ [#2306](https://github.com/spree/spree/issues/2306)
+- Always show "resend" (email confirmation) button when viewing an
+ order in
+ admin backend. [#2318](https://github.com/spree/spree/issues/2318)
+- Made sure that shipment for resumed order can be set to "ready"
+ [#2317](https://github.com/spree/spree/issues/2317)
diff --git a/guides/src/content/release_notes/2_0_0.md b/guides/src/content/release_notes/2_0_0.md
new file mode 100644
index 00000000000..69ddd3dae5e
--- /dev/null
+++ b/guides/src/content/release_notes/2_0_0.md
@@ -0,0 +1,321 @@
+---
+title: Spree 2.0.0
+section: release_notes
+order: 12
+---
+
+## Major/new features
+
+### General
+
+#### Removing support for Ruby 1.8.7
+
+Support for Ruby 1.8.7 is going away in this major release. If you are still using
+1.8.7, it is time to upgrade.
+[Ruby 1.8.7 is End of Life'd at the end of June](https://blog.engineyard.com/2012/ruby-1-8-7-and-ree-end-of-life)
+
+Upgrading to Ruby 1.9.3 or higher is highly encouraged. Spree 2.0 and above supports Ruby 2.
+
+#### Splitting up core
+
+A lot of people have requested the ability to use either the backend or the
+frontend separately from the other. We did a lot of work toward this goal as part of
+<%= issue 2225 %> and now Spree is split up
+into the following components:
+
+- API
+- **Backend**
+- Core
+- Dash
+- **Frontend**
+- Sample
+
+The **Backend** component provides the admin interface for Spree and the
+**Frontend** component provides the frontend user-facaing checkout interface. These
+components were extracted out of Core to allow for users of Spree to override the frontend
+or backend functionality of Spree as they choose. Core now contains just the very basic
+needs for Spree.
+
+Along with this work, the Promo engine has now been merged with Spree core. We
+saw that there was a lot of hackery going on to get promos to work with Core,
+and a lot of stores want promos anyway, and so merging them made sense.
+
+Additionally, as part of this work, the spree_core assets have been renamed.
+In `store/all.css` and `store/all.js`, you will need to rename the references
+from `spree_core` to `spree_frontend`. Similarly to this, in `admin/all.css`
+and `admin/all.js`, you will need to rename the references from `spree_core`
+to `spree_backend`.
+
+#### Split shipments
+
+Complex Spree stores require sophisticated shipping and warehouse logic that Spree hasn't had a general solution for until now. Split shipments in Spree allows for multiple shipments per order and for those shipments to be shipped from multiple locations.
+
+There are 4 main components that make up split shipments: Stock Locations, Stock Items, Stock Movements and Stock Transfers.
+
+##### Stock locations
+
+Stock locations are the locations where your inventory is shipped from. Each stock location can have many stock items. When creating a new stock location, stock items for that location are automatically created for each variant in your store.
+
+Having multiple stock locations allows for more robust shipping options. For example, if an item in an order is out of stock at the location of the other items in a order, a new shipment may be created if that item is found to be in stock at another location.
+
+You are also able to create and manage orders that have items from multiple locations by using the improved admin interface.
+
+##### Stock items
+
+Stock Items represent the inventory at a stock location for a specific variant. Stock item count on hand can be increased or decreased by creating stock movements. Because these are created automatically for each location you create, there is no need to manually manage these.
+
+##### Stock movements
+
+Stock movements allow you to manage the inventory of a stock item for a stock location. Stock movements are created in the admin interface by first navigating to the product you want to manage. Then, follow the Stock Management link in the sidebar.
+
+For more information on split shipments and how they pertain to inventory management, read the [Inventory Guide](http://edgeguides.spreecommerce.com/developer/inventory.html).
+
+For more information on the classes introduced by split shipments and how to work with them programmatically, see the [Shipments Guide](http://edgeguides.spreecommerce.com/developer/shipments.html#split-shipments).
+
+##### Stock transfers
+
+Stock transfers allow you to move inventory in bulk from one stock location to another stock location.
+
+Stock transfers generally consist of a source location, a destination location, one or more variants and an optional reference number. Stock transfers can also be used as a way to track new stock, in which case only a stock location destination and variant are required.
+
+#### I18n
+
+Spree 2.0 now comes with namespaced translations so that translations in your application
+will no longer conflict with those within Spree. It's recommended that if you have extension
+that uses Spree to move its translations into the Spree namespace to avoid the same problem.
+
+Translations within Spree views should now use the `Spree.t` helper, rather than the `t`
+helper so that they are namespaced correctly.
+
+### API
+
+#### New API endpoints
+
+API clients can now manage the following resources through the API:
+
+- Option Types
+- Option Values
+- Inventory Units
+- Shipments
+- Stock Items
+- Stock Locations
+- Stock Movements
+
+The documentation for these endpoints hasn't been written yet, but will be shortly.
+
+#### Instance level permissions
+
+The API now can enforce instance-level permissions on objects. This means that
+some users would be able to access a single item within a resource, rather than
+an "all or none" approach to the API.
+
+<%= commit "548dc0c58e4400501bc67cddea942fda1c7dbad3" %>
+
+#### Custom API templates
+
+If you wish to use a custom template for an API response you can do this by
+passing in a `template` parameter to API requests.
+
+[Read the documentation for more
+information](http://edgeguides.spreecommerce.com/api/summary.html#customizing-responses).
+
+### Core
+
+#### Adjustment state changes
+
+Adjustments can now be open, closed or finalized, allowing for a more flexible
+adjustments system. An 'open' adjustment can be modified, whereas a 'closed'
+adjustment cannot. Finalized adjustments are never altered.
+
+<%= commit "43a3cca49180b1572e41bc3638d3ca0f0e9116d9" %>
+
+#### OrderPopulator
+
+Order population responsibility has been moved out to its own class. This has
+been done so that the API, Core and any other extensions that wish to use the
+order population logic have an easy way to do so.
+
+See <%= issue 2341 %> and <%= commit "432d129c86e03597347cd223507d9386e9613d62" %> for more information.
+
+#### CouponApplicator
+
+Coupon application responsibility has been moved out to its own class too. This
+has been done so that the API, Core and any other extensions that wish to apply
+coupons have an easy way to do so.
+
+<%= commit "8ac9ac1c56fe1e471dd5b0124edbe383a8c70c48" %>
+
+#### ProductDuplicator
+
+Product duplication code has been moved out to its own class as well.
+
+<%= issue 2641 %>
+
+#### New helpers to modify checkout flow steps
+
+To add or remove steps to the checkout flow, you can now use the `insert_checkout_step`
+and `remove_checkout_step` helpers respectively. This patch has been backported
+to 1-3-stable as well, and will be available in Spree releases >= 1.3.3.
+
+The `insert_checkout_step` takes a `before` or `after` option to determine where to
+insert the step:
+
+````ruby
+insert_checkout_step :new_step, before: :address
+# or
+insert_checkout_step :new_step, after: :address```
+
+The `remove_checkout_step` will remove just one checkout step at a time:
+
+```ruby
+remove_checkout_step :address
+remove_checkout_step :delivery```
+
+### Dash
+
+## Minor changes
+
+### API
+
+#### CheckoutsController
+
+The Spree API now has support for "checking out" an order. This API
+functionality allows an existing order to be updated and advanced until
+it is in the complete state.
+
+For instance, if you have an order in the "confirm" state that you would
+like to advance to the "complete" state, make the following request:
+
+```bash
+PUT /api/checkouts/ORDER_NUMBER```
+
+For more information on using the new CheckoutsController, please see
+the [Checkouts API Documentation](/api/checkouts).
+
+#### Versioned templates
+
+API responses can now be versioned by the [versioncake](https://github.com/bwillis/versioncake) gem. While this is not used in Spree at the moment, it is future-proofing the API.
+
+### Core
+
+#### Auto-rotation of images
+
+Images will now be auto-rotated to their correct orientation based on EXIF data from the original image. All EXIF data is then stripped from the image, resulting in a smaller final image size.
+
+<%= issue 2338 %>
+
+#### Sample data
+
+The sample data now exists within straight Ruby code. The previous YAML-backed
+configuration was confusing and led to invalid data being inserted for sample data.
+
+<%= commit "cc2f55a27a154c0bf9d67e1dbef3c4761c68f8b8" %>
+
+#### Unique payment identifier
+
+Some payment gateways require payments to have a unique identifier. To solve this problem in Spree, each payment now has an `identifier` attribute which is generated when the payment is created.
+
+<%= issue 1998 %>
+<%= commit "b543fd105c2d511cdc98f27223cd0f5b1f663e72" %>
+
+#### Removal of `CheckoutController#state_callback`
+
+The `state_callback` method in `CheckoutController` has been removed. Instead of this method, please use transition callbacks on the `Order.state_machine` instance instead.
+
+#### Tracking URL for shipments
+
+Shipping methods now have the ability to have tracking URLs. These can be used to track the shipments on external shipping providers' websites.
+
+<%= issue 2644 %>
+
+#### Mailers now can take IDs or model objects
+
+To help with potential background processing of mailers, all mailer actions can now take the ID of their respective object, or the object itself.
+
+<%= issue 2808 %>
+
+#### SSLRequirement deprecated in favour of ForceSSL
+
+Spree will now use the `config.force_ssl` setting of Rails to determine whether or not to use SSL.
+
+<%= issue 2410 %>
+
+#### MailMethod model no longer exists
+
+The `MailMethod` model no longer exists. Instead, it is a class and all configuration is now done through that class or through `Spree::Config` settings.
+
+<%= issue 2643 %>
+
+## Trivial changes
+
+Some of these changes may have made it into 1-3-stable or 1-2-stable as well. You may wish to check that branch for commits with the same message to make sure of this.
+
+* `ShippingMethod` labels can now be overriden by overriding the `adjustment_label` method. <%= issue(2222) %>
+* Promotion rules that respond to `#products` will have their products considered in promotion adjustments. <%= issue 2363 %>
+* Taxons and products are now joined with a `Classification` model. <%= issue 2532 %> <%= commit "0c594923a457d2f6050c936498d31df312d6153a" %>
+* Fix call to `select_month` and `select_year` <%= issue 2259 %>
+* Fix issue where `params[:keyword]`, not `params[:keywords]` was acknowledged on taxons/show.html.erb. <%= issue 2258 %> <%= issue 2270 %>
+* Allow overriding of a shipping address's label <%= issue 2222 %>
+* Guard against false positive Jirafe conversions <%= issue 2273 %> <%= issue 2211 %> <%= issue 2157 %>
+* Add first_name and last_name aliases for Addresses <%= commit "ad119f9e21af9ba6f6ada96944fc758a6144c61c" %>
+* Escape JavaScript within ecommerce tracking code <%= issue 2289 %>
+* Slight refactoring of how preferences are fetched from preference store <%= commit "bddc49a5cf261ca9fcee45e4234bdb076eaa3feb" %>
+* Fix issue where "New State" button on country page would not link to correct country. <%= issue 2281 %>
+* Order#checkout_steps will now always include complete <%= commit "5110e12127840fb9b2e22f8b4f4c4fbee594de23" %>
+* Fix issue where `flash[:commerce_tracking]` was hanging around too long. <%= issue 2287 %>
+* Remove colons from translations in mail templates <%= issue 2278 %>
+* A payment method can now control its `auto_capture?` method. <%= issue 2304 %>
+* Added a preference to display a product without a price. <%= issue 2302 %>
+* Improve 'out of stock' error message. <%= issue 1821 %>
+* Track IP addresses for orders, for payment gateway reasons. <%= issue 2216 %> <%= issue 2257 %>
+* Use protocol-specific URL for font. <%= issue 2316 %>
+* First order promotion will now work with guest users. <%= issue 2306 %>
+* Always show resend button on admin order page. <%= issue 2318 %>
+* Allow a shipment to be made 'ready' once order has been made 'resumed' <%= issue 2317 %>
+* PerItem calculator no longer fails if a rule doesn't respond to the `products` method. <%= issue 2322 %>
+* `attachment_url` can now be configured from the admin backend. <%= issue 2344 %>
+* Scale and precision have been corrected in split_prices_from_variants migration. <%= issue 2358 %>
+* Localize error message for email validator. <%= issue 2364 %> <%= issue 2729 %> <%= issue 2730 %>
+* Remove duplicate thumbnails on products/show. <%= issue 2361 %>
+* Use line item price, not variant price, in return authorization price calculation JS. <%= issue 2342 %>
+* Allow for a product's description to be put to the page raw <%= issue 2323 %> <%= issue 2874 %> <%= commit "30fdf083" %>. See also <%= commit "b616d84a78e426bdeb3e8e6251aaa9ce8757996d" %> and <%= issue 2518 %>.
+* Promotions can now apply to orders which were created before the promotion. <%= issue 2388 %> <%= issue 2395 %>
+* Allow Address#phone validation to be overridden. <%= issue 2394 %>
+* Sort properties by alphabetical order on prototype form. <%= issue 2389 %>
+* Introduce datepicker_field_value for displaying datepicker field values. <%= issue 2405 %>
+* The 'New Product' button now appears on the edit product page. <%= issue 2407 %>
+* option values in `Variant#options_text` are now sorted in a predictable order. <%= issue 2432 %>
+* link_to_cart can now be used outside of Spree contexts. <%= issue 2441 %>
+* Promotion adjustments will now be removed on orders which are not complete when the promotion is deleted. <%= issue 1046 %> <%= issue 2453 %>
+* Adjustments are now displayed on the orders/show template. <%= issue 2449 %>
+* Variant images are displayed in place of product images in admin/images <%= issue 2228 %>
+* Product properties are now sortable. <%= issue 2464 %>
+* Returned items can now be re-shipped. <%= issue 2475 %>
+* LogEntry records are now saved for failed payments <%= issue 1767 %>
+* Show full address in order confirmation <%= issue 2136 %> <%= issue 2511 %>
+* Retrieve a list of variants from an order by calling `Order#variants`. <%= issue 2195 %>
+* Fix group_by_products_id sometimes not being available as a scope. <%= issue 1247 %>
+* Allow `meta_description` on `Spree::Product` to be as long as `text` will allow, rather than `string`. <%= issue 2611 %>
+* Fix currency display issues when using Euro. <%= issue 2634 %>
+* Fix issue where orders would become "locked" when initial payment had failed. <%= issue 2616 %> <%= issue 2570 %> <%= issue 2678 %> <%= issue 2585 %> <%= issue 2652 %>
+* Check for unprocessed payments before transitioning to complete state <%= issue 2694 %>
+* Added check to prevent skipping of checkout steps. <%= issue 2280 %>
+* Improve the look of the coupon code on the checkout. <%= issue 2720 %>
+* Admin tabs are now only displayed if user is authorized to see them. <%= issue 2626 %>
+* Reduce minimum characters required for variant autocomplete <%= issue 2774 %>
+* Added helper methods to shipment to calculate shipment item and total costs <%= issue 2843 %>
+* Allow product scopes to be added to from an extension <%= issue 2608 %>
+* Fix routing error caused by routing-filter and previous ghetto implementation of taxon autocomplete. <%= issue 2248 %>
+* ActionMailer settings will no longer be re-configured if they're already set. <%= issue 2855 %>
+* Fix issue where tax calculator computed taxes incorrectly for non-VAT taxes. <%= issue 2870 %>
+* The coupon code field is now hidden when there are no possible coupon codes. <%= issue 2835 %>
+* The `position` attribute for variants is now computed once the variant is saved. <%= issue 2744 %>
+* Account for situation where `current_order` might return `nil` in an `OrdersController#update` call. <%= issue 2750 %>
+* Fix case where ActiveMerchant would incorrectly process currencies without 2 decimal places. <%= issue 2930 %>
+* Add US military states to default states. <%= issue 2769 %>
+* Allow for specially overridden attributes for line items in the API. <%= issue 2916 %>
+* Transition order as far to complete as it will go after editing customer details. <%= issue 2950 %> <%= issue 2433 %>
+* Fix issue where going back a step in a cart could cause a `undefined method run_callbacks` error to be raised. <%= issue 2959 %> <%= issue 2921 %>
+* Use DISTINCT ON to make product `in_taxon` scope really distinct in PostgreSQL <%= issue 2851 %>
+* Run order update hooks when order is finalized too <%= issue 2986 %>
+````
diff --git a/guides/src/content/release_notes/2_1_0.md b/guides/src/content/release_notes/2_1_0.md
new file mode 100644
index 00000000000..059176adf42
--- /dev/null
+++ b/guides/src/content/release_notes/2_1_0.md
@@ -0,0 +1,286 @@
+---
+title: Spree 2.1.0
+section: release_notes
+order: 11
+---
+
+## Major/new features
+
+### Rails 4 compatibility
+
+Spree 2.1.0 is the first Spree release which is Rails 4 compatible. Go ahead, try it out!
+
+### Breaking API changes
+
+Spree's API component has undergone some work to make it easier to build JavaScript-backed frontends. For example, our [experimental Spree+Marionette project](https://github.com/radar/spree-marionette).
+
+As a result, we have altered some parts of the API to make this process easier. Please check the API changelog below to see if anything in there affects you.
+
+### Better Spree PayPal Express extension
+
+We now have a [better Spree PayPal Express](https://github.com/radar/better_spree_paypal_express) extension which is fully compatible with this release. If you are looking for PayPal Express Checkout integration for your new Spree store, check out this extension.
+
+## API
+
+- The Products API endpoint now returns an additional key called `shipping_category_id`, and also requires `shipping_category_id` on create.
+
+ _Jeff Dutil_
+
+- The Products API endpoint now returns an additional key called `display_price`, which is the proper rendering of the price of a product.
+
+ _Ryan Bigg_
+
+- The Images API's `attachment_url` key has been removed in favour of keys that reflect the current image styles available in the application, such as `mini_url` and `product_url`. Use these now to references images.
+
+ _Ryan Bigg_
+
+- Fix issue where calling OrdersController#update with line item parameters would _always_ create new line items, rather than updating existing ones.
+
+ _Ryan Bigg_
+
+- The Orders API endpoint now returns an additional key called `display_item_total`, which is the proper rendering of the total line item price of an order.
+
+ _Ryan Bigg_
+
+- Include a `per_page` key in Products API end response so that libraries like jQuery.simplePagination can use this to display a pagination element on the page.
+
+ _Ryan Bigg_
+
+- Line item responses now contain `single_display_amount` and `display_amount` for "pretty" versions of the single and total amount for a line item, as well as a `total` node which is an "ugly" version of the total amount of a line item.
+
+ _Ryan Bigg_
+
+- /api/orders endpoints now accept a `?order_token` parameter which should be the order's token. This can be used to authorize actions on an order without having to pass in an API key.
+
+ _Ryan Bigg_
+
+- Requests to POST /api/line_items will now update existing line items. For example if you have a line item with a variant ID=2 and quantity=10 and you attempt to create a new line item for the same variant with a quantity of 5, the existing line item's quantity will be updated to 15. Previously, a new line item would erroneously be created.
+
+ _Ryan Bigg_
+
+- /api/countries now will a 304 response if no country has been changed since the last request.
+
+ _Ryan Bigg_
+
+- The Shipments API no longer returns inventory units. Instead, it will return manifest objects. This is necessary due to the split shipments changes brought in by Spree 2.
+
+ _Ryan Bigg_
+
+- Checkouts API's update action will now correctly process line item attributes (either `line_items` or `line_item_attributes`)
+
+ _Ryan Bigg_
+
+- The structure of shipments data in the API has changed. Shipments can now have many shipping methods, shipping rates (which in turn have many zones and shipping categories), as well as a new key called "manifest" which returns the list of items contained within just this shipment for the order.
+
+ _Ryan Bigg_
+
+- Address responses now contain a `full_name` attribute.
+
+ _Ryan Bigg_
+
+- Shipments responses now contain a `selected_shipping_rate` key, so that you don't have to sort through the list of `shipping_rates` to get the selected one.
+
+ _Ryan Bigg_
+
+- Checkouts API now correctly processes incoming payment data during the payment step.
+
+ _Ryan Bigg_
+
+- Fix issue where `set_current_order` before filter would be called when CheckoutsController actions were run, causing the order object to be deleted. #3306
+
+ _Ryan Bigg_
+
+- An order can no longer transition past the "cart" state without first having a line item. #3312
+
+ _Ryan Bigg_
+
+- Attributes other than "quantity" and "variant_id" will be added to a line item when creating along with an order. #3404
+
+ _Alex Marles & Ryan Bigg_
+
+- Requests to POST /api/line_items will now update existing line items. For example if you have a line item with a variant ID=2 and quantity=10 and you attempt to create a new line item for the same variant with a quantity of 5, the existing line item's quantity will be updated to 15. Previously, a new line item would erroneously be created.
+
+ - Ryan Bigg
+
+- Checkouts API's update action will now correctly process line item attributes (either `line_items` or `line_item_attributes`)
+
+ - Ryan Bigg
+
+- Taxon attributes from `/api/taxons` are now returned within `taxons` subkey. Before:
+
+```json
+[{ name: 'Ruby' ... }]
+```
+
+Now:
+
+```json
+{ "taxons": [{ "name": "Ruby" }] }
+```
+
+ * Ryan Bigg
+
+## Backend
+
+- layouts/admin.html.erb was broken into partials for each section. e.g.
+ header, menu, submenu, sidebar. Extensions should update their deface
+ overrides accordingly
+
+ _Washington Luiz_
+
+- No longer requires all jquery ui modules. Extensions should include the
+ ones they need on their own manifest file. #3237
+
+ _Washington Luiz_
+
+- Symbolize attachment style keys on ImageSettingController otherwise users
+ would get _undefined method `processors' for "48x48>":String>_ since
+ paperclip can't handle key strings. #3069 #3080
+
+ _Washington Luiz_
+
+- Split line items across shipments. Use this to move line items between
+ existing shipments or to create a new shipment on an order from existing
+ line items.
+
+ _John Dyer_
+
+- Fixed display of "Total" price for a line item on a shipment. #3135
+
+ _John Dyer_
+
+- Fixed issue where selecting an existing user in the customer details step would not associate them with an order.
+
+ _Ryan Bigg and dan-ding_
+
+- We now use [jQuery.payment](https://stripe.com/blog/jquery-payment) (from Stripe) to provide slightly better formatting on credit card number, expiry and CVV fields.
+
+ _Ryan Bigg_
+
+- "Infinite scrolling" now implemented for products taxon search to prevent loading all taxons at once. Only 50 taxons are loaded at a time now.
+
+ _Ryan Bigg_
+
+## Cmd
+
+No changes.
+
+## Core
+
+- Product requires `shipping_category_id` on create #3188.
+
+ _Jeff Dutil_
+
+- No longer set ActiveRecord::Base.include_root_in_json = true during install.
+ Originally set to false back in 2011 according to convention. After
+ https://groups.google.com/forum/#!topic/spree-user/D9dZQayC4z, it
+ was changed. Applications should now decide their own setting for this value.
+
+ _Weston Platter_
+
+- Change `order.promotion_credit_exists?` api. Now it receives an adjustment
+ originator (PromotionAction instance) instead of a promotion. Allowing
+ multiple adjustments being created for the same promotion as the current
+ PromotionAction / Promotion api suggests #3262
+
+- Remove after_save callback for stock items backorders processing and
+ fixes count on hand updates when there are backordered units #3066
+
+ _Washington Luiz_
+
+- InventoryUnit#backordered_for_stock_item no longer returns readonly objects
+ neither return an ActiveRecored::Association. It returns only an array of
+ writable backordered units for a given stock item #3066
+
+ _Washington Luiz_
+
+- Scope shipping rates as per shipping method display_on #3119
+ e.g. Shipping methods set to back_end only should not be displayed on frontend too
+
+ _Washington Luiz_
+
+- Add `propagate_all_variants` attribute to StockLocation. It controls
+ whether a stock items should be created fot the stock location every time
+ a variant or a stock location is created
+
+ _Washington Luiz_
+
+- Add `backorderable_default` attribute to StockLocation. It sets the
+ backorderable attribute of each new stock item
+
+ _Washington Luiz_
+
+- Removed `t()` override in `Spree::BaseHelper`. #3083
+
+ _Washington Luiz_
+
+- Improve performance of `Order#payment_required?` by not updating the totals every time. #3040 #3086
+
+ _Washington Luiz_
+
+- Fixed the FlexiRate Calculator for cases when max_items is set. #3159
+
+ _Dana Jones_
+
+- Translation for admin tabs are now located under the `spree.admin.tab` key. Previously, they were on the top-level, which led to conflicts when users wanted to override view translations, like this:
+
+```yml
+en:
+ spree:
+ orders:
+ show:
+ thank_you: 'Thanks, buddy!'
+```
+
+See #3133 for more information.
+
+ * Ryan Bigg*
+
+- CreditCard model now validates that the card is not expired.
+
+ _Ryan Bigg_
+
+- Payment model will now no longer provide a vague error message for when the source is invalid. Instead, it will provide error messages like "Credit Card Number can't be blank"
+
+ _Ryan Bigg_
+
+- Calling #destroy on any PaymentMethod, Product, TaxCategory, TaxRate or Variant object will now no longer delete that object. Instead, the `deleted_at` attribute on that object will be set to the current time. Attempting to find that object again using something such as `Spree::Product.find(1)` will fail because there is now a default scope to only find _non_-deleted records on these models. To remove this scope, use `Spree::Product.unscoped.find(1)`. #3321
+
+ _Ryan Bigg_
+
+- Removed `variants_including_master_and_deleted`, in favour of using the Paranoia gem. This scope would now be achieved using `variants_including_master.with_deleted`.
+
+ _Ryan Bigg_
+
+- You can now find the total amount on hand of a variant by calling `Variant#total_on_hand`. #3427
+
+ _Ruben Ascencio_
+
+- Tax categories are now stored on line items. This should make tax calculations slightly faster. #3481
+
+ _Ryan Bigg_
+
+- `update_attribute(s)_without_callbacks` have gone away, in favour of `update_column(s)`
+
+ _Ryan Bigg_
+
+## Frontend
+
+- Fix issue where "Use Billing Address" checkbox was unticked when certain
+ browsers autocompleted the checkout form. #3068 #3085
+
+ _Washington Luiz_
+
+- Switch to new Google Analytics analytics.js SDK from ga.js SDK for custom dimensions & metrics.
+
+ _Jeff Dutil_
+
+- We now use [jQuery.payment](https://stripe.com/blog/jquery-payment) (from Stripe) to provide slightly better formatting on credit card number, expiry and CVV fields.
+
+ _Ryan Bigg_
+
+## Spree::ActiveShipping
+
+- Origin address fields (e.g., origin_country) have been removed from the Spree::ActiveShipping preferences.
+
+ _Ryan Bigg_
diff --git a/guides/src/content/release_notes/2_2_0.md b/guides/src/content/release_notes/2_2_0.md
new file mode 100644
index 00000000000..0d52910ac0f
--- /dev/null
+++ b/guides/src/content/release_notes/2_2_0.md
@@ -0,0 +1,217 @@
+---
+title: Spree 2.2.0
+section: release_notes
+order: 10
+---
+
+## Major/new features
+
+### Adjustments Refactoring
+
+The adjustments system in Spree has undergone a large portion of work. Adjustments (typically originating from promotions and taxes) can now be applied at a line item, shipment or order level.
+
+**This system has been designed to be backwards-compatible with older versions of Spree, so that an upgrade path is relatively easy. If you encounter any issues during an upgrade, please [file an issue](https://github.com/spree/spree/issues/new).**
+
+Along with this, taxes are now split into two groupings: "additional" and "included". Additional taxes are those which increase the price of the item they're attached to. Included taxes are those which are already included in the cost of the item. It is still necessary to track these included taxes due to tax reporting requirements in many countries.
+
+Shipments no longer have a linked adjustment. Instead, the shipment itself has a "cost" attribute which is used in the calculation of shipping costs for an order.
+
+Also worth noting is that the number of callbacks triggered when any aspect of an order is updated has been greatly reduced, which should lead up to speed-ups in stores. An example of this would be in prior versions of Spree, an order would trigger an update on all its adjustments when it updated. With the new system, only line items or shipments that change will have their adjustments updated.
+
+For more information about this, [Ryan Bigg wrote up a long explanation about it](http://ryanbigg.com/2013/09/order-adjustments/), and there is further discussion on #3567.
+
+### Fragment caching
+
+In certain places in the frontend, the following changes have been applied:
+
+- Fragment caching for each product.
+- Fragment caching for the lists of products in home/index and products/index.
+- Fragment caching for a taxon's children.
+
+This can lead to significant speedups in the frontend of a Spree store.
+
+See more about this in [this comment on spree/spree#2913](https://github.com/spree/spree/issues/2913#issuecomment-34946007).
+
+### Asset renaming
+
+An issue was brought up in #4050 where a user showed us that a `require_tree` use inside `app/assets` would also require the Spree assets that were placed in `app/assets/store` and app/assets/admin` respectively. This would happen in areas of the application where Spree wasn't even used.
+
+To fix this bug, we have moved the location of the assets to `vendor/assets`. Frontend's assets are now placed in `vendor/assets/spree/frontend` and Backend's are in `vendor/assets/spree/backend`.
+
+Similar changes to this have also been made to extensions, where their assets are now placed in `app/assets/spree/[extension_name]`. Ultimately, these changes fix the bug and now we're using the same names to refer to the same components (store -> frontend, admin -> backend) on assets as we do internally to Spree.
+
+You will need to manually rename asset requires within your application:
+
+- `admin/spree_backend` => `spree/backend`
+- `store/spree_frontend` => `spree/frontend`
+
+### Risk analysis
+
+The AVS and CVV response codes for payments are now checked to determine the possibility that an order is considered risky. If the order is considered risky, then it will transition to a 'considered risky' state upon finalize rather than 'complete'. The order must be approved in the admin backend in order for it to proceed to the 'complete' state.
+
+Stores may choose to override `Order#is_risky` to implement their own risk analysis for orders.
+
+See issues #4021 and #4298 for further information.
+
+### Paperclip settings have been removed
+
+The ability to configure Paperclip settings for `Spree::Image` has been removed from Spree. The alternative to this is to configure the Paperclip settings for `Spree::Image` in an initializer:
+
+ Paperclip::Attachment.default_options[:s3_protocol] = "https"
+ Spree::Image.attachment_definitions[:attachment][:styles] = ""
+ Spree::Image.attachment_definitions[:attachment][:path] = ""
+
+These settings are for the Paperclip gem, and hence more information about them can be found in [Paperclip's documentation](http://rubydoc.info/gems/paperclip/Paperclip/ClassMethods).
+
+You may wish to use S3, in which case you can configure it using code [like this Gist](https://gist.github.com/radar/e414c49579b393e4aafe).
+
+## Minor changes
+
+### Core
+
+- Switched to using friendly_id for permalink generation. This meant that we needed to rename `Spree::Product`'s `permalink` field to `slug`.
+
+ Ryan Bigg
+
+- Add a `name` column to spree_payments. That should hold the _Name on card_
+ option in payment checkout step.
+
+ Washington Luiz
+
+- Associate line item and inventory units for better extensibility with
+ product assemblies. Migration was added to set line_item_id for existing
+ inventory units.
+
+- A _channel_ column was added to the spree_orders table. Users can set
+ it when importing orders from other stores. e.g. amazon
+
+ Washington Luiz
+
+- Introduce `Core::UserAddress` module. Once included on the store user class
+ the user address can be rememembered on checkout
+
+ Washington Luiz
+
+- Added tax_category to variants, to allow for different variants of a product to have different tax categories. #3946
+
+ Peter Rhoades
+
+- Removed `Spree::Activator`. Promotions are now activated using the `Spree::PromotionHandler` classes.
+
+ Ryan Bigg
+
+- Promotion#event_name attribute has been removed. A promotion's event now depends on the fields that are filled out during its creation.
+
+ Ryan Bigg
+
+- Simplified OrderPopulator to take only a variant_id and a quantity, rather than a confusing hash of product/variant ids.
+
+ Ryan Bigg
+
+- lib/ is no longer in autoload paths. You'll have to manually require what
+ you need in that dir. See https://github.com/spree/spree/commit/b3834a1542e350034c1e9c5a8b13c00b2415e63b
+
+- Introduce Spree::Core::MailMethod to manage mail settings at each delivery.
+ This allows changes to mail settings to be applied without a server restart.
+ See https://github.com/spree/spree/commit/95df1aa7832912f73e34302d31b0abbbea3af709
+
+ John Hawthorn
+
+- Create Spree::Migrations to warn about missing migrations. See #4080
+
+ Washington Luiz
+
+- Variant#in_stock? now no longer takes a quantity. Call can_supply? instead.
+ see #4279
+
+ Ryan Bigg / Peter Berkenbosch
+
+- PromotionRule#activator_id column has been renamed to promotion_id.
+
+ Ryan Bigg
+
+### API
+
+- Api requires authentication by default now
+
+ Peter Berkenbosch
+
+- Improve products_controller #create and #update for better support to create
+ and update variants, option types and option values.
+ See #4172 and #4240
+
+ Bruno Buccolo / Washington Luiz / John Dyer
+
+- ApiHelpers attributes can now be extended without overriding instance
+ methods. By using the same approach in PermittedAttributes. e.g.
+
+ Spree::Api::ApiHelpers.order_attributes.push :locked_at
+
+ Washington Luiz
+
+- Admin users can set the order channel when importing orders. By sing the
+ channel attribute on Order model
+
+ Washington Luiz
+
+- Cached products/show template, which can lead to drastically (65x) faster loading times on product requests. 806319709c4ce9a3d0026e00ec2d07372f51cdb8
+
+ Ryan Bigg
+
+- The parts that make up an order's response from /api/orders/:num are cached, which can lead to a 5x improvement of speed for this API endpoint. 80ffb1e739606ac02ac86336ac13a51583bcc225
+
+ Ryan Bigg
+
+- Cached variant objects which can lead to slightly faster loading times (4x) for each variant.
+
+ Ryan Bigg
+
+- Added a route to allow for /api/variants/:id requests
+
+ Ryan Bigg
+
+- Products response now contains a master variant separately from all the other variants. Previously all variants were grouped together.
+
+ Ryan Bigg
+
+- Added API endpoint to retrieve a user's orders: /api/orders/mine. #4022
+
+ Richard Nuno
+
+- Order token can now be passed as a header: `X-Spree-Order-Token`. #4148
+
+ Lucjan Suski
+
+### Frontend
+
+- Payment step displays a name input so that users can enter _Name on card_
+ Previously we had a first_name and last_name hidden input instead.
+
+ Washington Luiz
+
+- Checkout now may remember user address
+
+ Washington Luiz
+
+### Backend
+
+- Don't serve JS to non XHR requests. Prevents sentive data leaking. Thanks to
+ Egor Homakov for pointing that out in Spree codebase.
+ See http://homakov.blogspot.com.br/2013/05/do-not-use-rjs-like-techniques.html
+ for details.
+
+- 'Only show completed orders' checkbox status will now persist when paging through orders.
+
+ - darbs + Ryan Bigg
+
+- Implemented a basic Risk Assessment feature in Spree Backend. Viewing any Order's edit page now shows the following, with a status indicator:
+
+ Payments; link_to new log feature (ie. Number of multiple failed authorization requests)
+ AVS response (ie. Billing address not matching credit card)
+ CVV response (ie. code not matching)
+
+ - Ben Radler (aka lordnibbler)
+
+- Moved 'Taxonomies' out from under 'Configuration' menu. It now is a sub-menu item on the products.
+
+ - Ryan Bigg
diff --git a/guides/src/content/release_notes/2_3_0.md b/guides/src/content/release_notes/2_3_0.md
new file mode 100644
index 00000000000..a001ee87130
--- /dev/null
+++ b/guides/src/content/release_notes/2_3_0.md
@@ -0,0 +1,246 @@
+---
+title: Spree 2.3.0
+section: release_notes
+order: 9
+---
+
+## Major/new features
+
+### Rails 4.1 support
+
+Rails 4.1 is now supported by Spree 2.3. If you wish to use Rails 4.1, Spree 2.3 is the release for you.
+
+### Preferences serialized on records
+
+Preferences are now stored on their records, rather than being stored in `spree_preferences`. This means that to fetch a preference for say, a calculator, one query needs to be done to the database for that row, as that row has the `preferences` column which contains all preferences.
+
+Previously, there would be a single DB call for the record itself, and then any number of database calls thereafter to fetch the required preference values for that record. What happens now is that there's only one database call, which means there should be some minor speedups.
+
+### Better multi-store support
+
+A `Spree::Store` model for basic multi-store/multi-domain support has been added.
+
+This provides a basic framework for multi-store/multi-domain, based on the
+spree-multi-domain extension. Some existing configuration has been moved to
+this model, so that they can have different values depending on the site
+being served:
+
+- `Spree::Config[:site_name]` is moved to `name`
+- `Spree::Config[:site_url]` is moved to `url`
+- `Spree::Config[:default_meta_description]` is moved to `meta_description`
+- `Spree::Config[:default_meta_keywords]` is moved to `meta_keywords`
+- `Spree::Config[:default_seo_title]` is moved to `seo_title`
+
+A migration will move existing configuration onto a new default store.
+
+A new `ControllerHelpers::Store` concern provides a `current_store` helper
+to fetch a helper based on the request's domain.
+
+### Better guest user tracking
+
+Now we are using a signed cookie to store the guests unique token
+in the browser. This allows customers who close their browser to
+continue their shopping when they visit again. More importantly
+it allows you as a store owner to uniquely identify your guests orders.
+Since we set `cookies.signed[:guest_token]` whenever a vistor comes
+you may also use this cookie token on other objects than just orders.
+For instance if a guest user wants to favorite a product you can
+assign the `cookies.signed[:guest_token]` value to a token field on your
+favorites model. Which will then allow you to analyze the orders and
+favorites this user has placed before which is useful for recommendations.
+
+## Core
+
+- Drop first_name and last_name fields from spree_credit_cards. Add
+ first_name & last_name methods for now to keep ActiveMerchant happy.
+
+ Jordan Brough
+
+- Replaced cookies.signed[:order_id] with cookies.signed[:guest_token].
+
+ Now we are using a signed cookie to store the guests unique token
+ in the browser. This allows customers who close their browser to
+ continue their shopping when they visit again. More importantly
+ it allows you as a store owner to uniquely identify your guests orders.
+ Since we set cookies.signed[:guest_token] whenever a vistor comes
+ you may also use this cookie token on other objects than just orders.
+ For instance if a guest user wants to favorite a product you can
+ assign the cookies.signed[:guest_token] value to a token field on your
+ favorites model. Which will then allow you to analyze the orders and
+ favorites this user has placed before which is useful for recommendations.
+
+ Jeff Dutil
+
+- Order#token is no longer fetched from another table.
+
+ Both Spree::Core::TokenResource and Spree::TokenizedPermission are deprecated.
+ Order#token value is now persisted into spree_orders.guest_token. Main motivation
+ here is save a few extra queries when creating an order. The TokenResource
+ module was being of no use in spree core.
+
+ NOTE: Watch out for the possible expensive migration that come along with this
+
+ Washington L Braga Jr
+
+- Replaced session[:order_id] usage with cookies.signed[:order_id].
+
+ Now we are using a signed cookie to store the order id on a guests
+ browser client. This allows customers who close their browser to
+ continue their shopping when they visit again.
+ Fixes #4319
+
+ Jeff Dutil
+
+* Order#process_payments! no longer raises. Gateways must raise on failing authorizations.
+
+ Now it's a Gateway or PaymentMethod responsability to raise a custom
+ exception any time an authorization fails so that it can be rescued
+ during checkout and proper action taken.
+
+* Assign request headers env to Payment when creating it via checkout.
+
+ This might come in handy for some gateways, e.g. Adyen, actions that require
+ data such as user agent and accept header to create user profiles. Previously
+ we had no way to access the request headers from within a gateway class
+
+* More accurate and simpler Order#payment_state options.
+
+ Balance Due. Paid. Credit Owed. Failed. These are the only possible values
+ for order payment_state now. The previous `pending` state has been dropped
+ and order updater logic greatly improved as it now mostly consider total
+ values rather than doing last payment state checks.
+
+ Huge thanks to dan-ding. See https://github.com/spree/spree/issues/4605
+
+* Config settings related to mail have been removed. This includes
+ `enable_mail_delivery`, `mail_bcc`, `intercept_email`,
+ `override_actionmailer_config`, `mail_host`, `mail_domain`, `mail_port`,
+ `secure_connection_type`, `mail_auth_type`, `smtp_username`, and
+ `smtp_password`.
+
+ These should instead be [configured on actionmailer directly](http://api.rubyonrails.org/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Configuration+options).
+ The existing functionality can also be used by including the [spree_mail_settings](https://github.com/spree-contrib/spree_mail_settings) gem.
+
+ John Hawthorn
+
+* refactor the api to use a general importer in `lib/spree/importer/order.rb`
+
+ Peter Berkenbosch
+
+* Ensure transition to payment processing state happens outside transaction.
+
+ Chris Salzberg
+
+* Increase the precision of the amount/price columns in order for support other currencies. See https://github.com/spree/spree/issues/4657
+
+ Gonzalo Moreno
+
+* Preferences on models are now stored in a serialized `preferences` column instead of the `Spree::Preferences` table.
+
+ `Spree::Preferences` are still used for configuration (like `Spree::Config`).
+ For models with preferences (`Calculator`, `PromotionRule`, and
+ `PaymentMethod` in spree core) they are now serialized using
+ `ActiveRecord::Base.serialize`, storing the preferences as YAML in the
+ `preferences` column.
+
+ ```
+ > c = Spree::Calculator.first
+ => #
+ > c.preferred_amount
+ => 5
+ > c.preferred_amount = 10
+ => 10
+ > c
+ => #
+ ```
+
+ John Hawthorn
+
+* Add Spree::Store model for basic multi-store/multi-domain support
+
+ This provides a basic framework for multi-store/multi-domain, based on the
+ spree-multi-domain extension. Some existing configuration has been moved to
+ this model, so that they can have different values depending on the site
+ being served:
+
+ - `Spree::Config[:site_name]` is moved to `name`
+ - `Spree::Config[:site_url]` is moved to `url`
+ - `Spree::Config[:default_meta_description]` is moved to `meta_description`
+ - `Spree::Config[:default_meta_keywords]` is moved to `meta_keywords`
+ - `Spree::Config[:default_seo_title]` is moved to `seo_title`
+
+ A migration will move existing configuration onto a new default store.
+
+ A new `ControllerHelpers::Store` concern provides a `current_store` helper
+ to fetch a helper based on the request's domain.
+
+ Jeff Dutil, Clarke Brunsdon, and John Hawthorn
+
+## API
+
+- Support existing credit card feature on checkout.
+
+ Checkouts_controller#update now uses the same Order::Checkout#update_from_params
+ from spree frontend which help us to remove a lot of duplicated logic. As a
+ result of that `payment_source` params must be sent now outsite the `order` key.
+
+ Before you'd send a request like this:
+
+ ```ruby
+ api_put :update, id: order.to_param, order_token: order.guest_token,
+ order: {
+ payments_attributes: [{ payment_method_id: @payment_method.id.to_s }],
+ payment_source: { @payment_method.id.to_s => { name: "Spree" } }
+ }
+ ```
+
+ Now it should look like this:
+
+ ```ruby
+ api_put :update, id: order.to_param, order_token: order.guest_token,
+ order: {
+ payments_attributes: [{ payment_method_id: @payment_method.id.to_s }]
+ },
+ payment_source: {
+ @payment_method.id.to_s => { name: "Spree" }
+ }
+ ```
+
+ Josh Hepworth and Washington
+
+- api/orders/show now display credit cards as source under payment
+
+ Washington Luiz
+
+- refactor the api to use a general importer in core gem.
+
+ Peter Berkenbosch
+
+- Shipment manifests viewed within the context of an order no longer return variant info. The line items for the order already contains this information. #4498
+
+ - Ryan Bigg
+
+## Frontend
+
+- The api key that was previously placed in the dom for ajax requests has been
+ removed since the api now uses the session to authenticate the user.
+
+- Mostly inspired by Jeff Squires' extension spree_reuse_credit card, checkout
+ now can remember user credit card info. Make sure your user model responds
+ to a `payment_sources` method and customers will be able to reuse their
+ credit card info.
+
+ Washington Luiz
+
+- Use settings from current_store instead of Spree::Config
+
+ Jeff Dutil, John Hawthorn, and Washington Luiz
+
+## Backend
+
+- The api key that was previously placed in the dom for ajax requests has been
+ removed since the api now uses the session to authenticate the user.
diff --git a/guides/src/content/release_notes/2_4_0.md b/guides/src/content/release_notes/2_4_0.md
new file mode 100644
index 00000000000..029936d7ef5
--- /dev/null
+++ b/guides/src/content/release_notes/2_4_0.md
@@ -0,0 +1,133 @@
+---
+title: Spree 2.4.0
+section: release_notes
+order: 8
+---
+
+## Major/new features
+
+### Return Authorization Rewrite
+
+Return Authorizations have received a major rewrite, and can be read more about in the updated [Returning Orders](http://guides.spreecommerce.org/user/returning_orders.html) guide.
+
+The previous return authorization code has been extracted into an extension if you would like to continue using it:
+[https://github.com/spree-contrib/spree_legacy_return_authorizations](https://github.com/spree-contrib/spree_legacy_return_authorizations)
+
+### HTML Email Templates
+
+There are now default HTML email templates, which use [Zurb Ink](http://zurb.com/ink/templates.php) and [Premailer-Rails](https://github.com/fphilipe/premailer-rails) for responsive styling.
+
+### Guides moved into core repository
+
+The previous spree-guides repository has been merged into the [Spree Core](https://github.com/spree/spree) repository. Any documentation updates to this website should now be submitted with pull requests to [https://github.com/spree/spree](https://github.com/spree/spree)
+
+## Upgrade Tips
+
+### Read the upgrade guide
+
+For information about upgrading a basic spree store, please read the [2.3 to 2.4 upgrade guide](http://guides.spreecommerce.org/developer/two-dot-three-to-two-dot-four.html).
+
+### Other Gotchas
+
+If you have implemented your own authentication system instead of using spree_auth_devise,
+or you have implemented your own adjustment source please ensure you change your
+concerns to the new namespace:
+
+```ruby
+Spree::Core::AdjustmentSource => Spree::AdjustmentSource
+Spree::Core::CalculatedAdjustments => Spree::CalculatedAdjustments
+Spree::Core::UserAddress => Spree::UserAddress
+Spree::Core::UserPaymentSource => Spree::UserPaymentSource
+```
+
+Also please review each of the noteable changes, and ensure your customizations
+or extensions are not effected. If you are effected by a change, and have any
+of your own tips please submit a PR to help the next person!
+
+## Full Changelog
+
+You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/2-3-stable...2-4-stable)
+
+## Noteable Changes
+
+- The admin order creation/edit process now closely matches the frontend by allowing a separate cart and shipments page.
+
+ Tyler Smart (tesserakt)
+
+* Add authorize_payments! and capture_payments! to the public interface of orders.
+
+ Richard Wilson
+
+* Spree no longer holds aws-sdk as a core dependency. In case you use it
+ you need to add it to your Gemfile. See paperclip README for reference on
+ scenarios where this is needed https://github.com/thoughtbot/paperclip/tree/v4.1.1#understanding-storage
+
+ Washigton L Braga Jr
+
+* Added Spree::Config.auto_capture_on_dispatch that when set to true will
+ cause shipments to advance to ready state upon successfully authorizing
+ payment for the order. As each shipment is marked shipped the
+ shipment's total will be captured from the authorization. Fixes #4727
+
+ Jeff Dutil
+
+* Added `actionable?` for Spree::Promotion::Rule. `actionable?` defines
+ if a promotion action can be applied to a specific line item. This
+ can be used to customize which line items can accept a promotion
+ action by defining its logic within the promotion rule rather than
+ relying on Spree's default behaviour. Fixes #5036
+
+ Gregor MacDougall
+
+* Refactored Stock::Coordinator to optionally accept a list of inventory units
+ for an order so that shipments can be created for an order that do not comprise
+ only of the order's line items.
+
+ Andrew Thal
+
+* Default ship and bill addresses are now saved and restored in callbacks. This
+ makes the default address functionality available to orders driven through
+ frontend, backend and API without duplicating the code.
+
+ Magnus von Koeller
+
+* When a user successfully uses a credit card to pay for an order, that card
+ becomes the default credit card for that user. On future orders, we automatically
+ add that default card as a payment when the order reaches the payment step.
+
+ Magnus von Koeller
+
+* Provided hooks for extensions to seamlessly integrate with the order population workflow.
+ Extensions make use of the convention of passing parameters during the 'add to cart'
+ action https://github.com/spree/spree/blob/master/core/app/models/spree/order_populator.rb#L12
+ with a prefix like [:options][:voucher_attributes] (in the case of the spree_vouchers
+ extension). The extension then provides some methods named according to what was passed in
+ like:
+
+ https://github.com/spree-contrib/spree_vouchers/blob/master/app/models/spree/order_decorator.rb#L51
+
+ to determine if these possible line item customizations affect the line item equality condition and
+
+ https://github.com/spree-contrib/spree_vouchers/blob/master/app/models/spree/variant_decorator.rb#L3
+
+ to adjust a line item's price if necessary.
+
+ https://github.com/spree/spree/blob/master/core/app/models/spree/order_contents.rb#L70
+ shows how we expect inbound parameters (such as the voucher_attributes) to be saved in a
+ nested_attributes fashion.
+
+ Jeff Squires
+
+* Rename _\_filter callbacks to _\_action callbacks.
+
+ Masahiro Saito
+
+* Move some modules to model's concerns directory.
+ We move modules Spree::Core::AdjustmentSource, Spree::Core::CalculatedAdjustments, Spree::Core::UserAddress
+ and Spree::Core::UserPaymentSource. Fixes #5264.
+
+ Masahiro Saito
+
+* Enable default html email templates with Zurb Ink, and PreMailer for Rails.
+
+ Ben Morgan & Jeff Dutil
diff --git a/guides/src/content/release_notes/3_0_0.md b/guides/src/content/release_notes/3_0_0.md
new file mode 100644
index 00000000000..18e8868d80b
--- /dev/null
+++ b/guides/src/content/release_notes/3_0_0.md
@@ -0,0 +1,179 @@
+---
+title: Spree 3.0.0
+section: release_notes
+order: 7
+---
+
+## Major/New Features
+
+### Bootstrap Backend & Frontend
+
+The most visible change will be the use of Bootstrap to replace the previous
+Skeleton framework. We've redesigned the frontend, and backend while keeping
+much of the previous structure to the site. We hope this will make upgrades
+slightly easier to port to the new Bootstrap markup. Now that we've updated
+the default css/js framework we're hoping to begin implementing usability
+improvements & improved mobile support.
+
+### Rails 4.2 Support
+
+We've added Rails 4.2 support laying the groundwork for further improvements.
+Rails 4.2 comes with ActiveJob a background job API, which we will leverage in
+the future to send long running tasks to Sidekiq, DelayedJob or Resque if you
+set them up. We've also already updated mailers to use the deliver later
+functionality so your confirmation emails will be sent in the background.
+
+### Google Analytics Enhanced Ecommerce
+
+With the change to [enhanced ecommerce](https://developers.google.com/analytics/devguides/collection/analyticsjs/enhanced-ecommerce) tracking you should make sure to [upgrade your Google Analytics account to Universal Analytics](https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#overview). If you use Spree's default google analytics implementation you should be fine, but if you've customized these make sure to update to [Universal Analytics](https://developers.google.com/analytics/devguides/collection/analyticsjs/).
+
+## Upgrade Tips
+
+### First Read Rails 4.2 Release Notes & Upgrade Guide
+
+[Release Notes](http://edgeguides.rubyonrails.org/4_2_release_notes.html)
+
+[Upgrade Guide](http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-4-1-to-rails-4-2)
+
+### Spree Upgrade
+
+If you have extensions that your store depends on, you will need to manually
+verify that each of those extensions work within your 3.0.x store once this
+upgrade is complete. Typically, extensions that are compatible with this
+version of Spree will have a 3-0-stable branch.
+
+For this Spree release, you will need to upgrade your Rails version to at least 4.2.0.
+
+```ruby
+gem 'rails', '~> 4.2.0'
+```
+
+For best results, use the 3-0-stable branch from GitHub:
+
+```ruby
+gem 'spree', github: 'spree/spree', branch: '3-0-stable'
+```
+
+Run `bundle update spree`.
+
+Copy over the migrations from Spree (and any other engine) and run them using
+these commands:
+
+ rake railties:install:migrations
+ rake db:migrate
+
+Verify that everything is OK. Manually test your store, and make sure it's performing
+as normal. Fix any deprecation warnings you see.
+
+### Other Gotchas
+
+#### SSL
+
+SSLRequirement controller concern was removed in favor of using Rails built in [ForceSSL](http://api.rubyonrails.org/classes/ActionController/ForceSSL/ClassMethods.html).
+
+It is recommended that you enable config.force_ssl = true within your production.rb file which will enforce SSL on every page.
+
+If you were previously using ssl_required in any controllers you should now use force_ssl at the controller level if you're not going to use force_ssl at the application level.
+**This includes if you were previously relying on Spree's built in use of ssl_required so make sure your checkout process properly handles SSL connections.**
+
+#### PaymentMethods and Trackers are no longer based on environment.
+
+Previously payment methods and google analytics trackers could be assigned an environment,
+such as, production/staging/development etc.. This is no longer the case. If you previously
+relied on importing data from production to a development or staging environment you should
+ensure to sanitize and/or update these credentials to prevent submitting payments or analytics
+information to your production account credentials.
+
+We recommend that you begin to [manage your credentials with environment variables](http://www.gotealeaf.com/blog/managing-environment-configuration-variables-in-rails) instead.
+
+#### Noteable Changes
+
+Also please review each of the noteable changes, and ensure your customizations
+or extensions are not effected. If you are affected by a change, and have any
+of your own tips please submit a PR to help the next person!
+
+## Full Changelog
+
+You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/2-4-stable...3-0-stable).
+
+## Noteable Changes
+
+- Switched to Bootstrap.
+
+ Jeff Dutil & [Other Contributors](https://github.com/200Creative/spree_bootstrap_frontend/graphs/contributors)
+
+- Moved Core's helpers into Frontend.
+
+ Jeff Dutil
+
+- Use Google Analytics Enhanced ecommerce.
+
+ https://github.com/erikaxel
+
+- PaymentMethod's and Tracker's are no longer based on environment.
+
+ Clarke Brunsdon
+
+- Removed promo code field from cart page. This prevents issues with promos
+ attempting to be used before the order is ready. e.g. Free Shipping before shipments.
+
+ Jeff Dutil
+
+- Removed order token usage during guest checkout.
+
+ https://github.com/spree/spree/pull/6072
+
+- Replace outdated Spree::Config[:mails_from] with Spree::Store.current.mail_from_address
+
+ https://github.com/spree/spree/pull/6097
+
+- Moved current_currency helper from Spree::Core::ControllerHelpers::Order to Spree::Core::ControllerHelpers::Store.
+
+- The line items order population endpoint used to accept a hash, it now accepts an array. When submitting a Post request to `/api/line_items` you will receive this deprecation warning:
+
+ Ben A. Morgan
+
+ ```text
+ Passing a hash is now deprecated and will be removed in Spree 3.1.
+ It is recommended that you pass it as an array instead.
+
+ New Syntax:
+
+ {
+ "order": {
+ "line_items": [
+ { "variant_id": 123, "quantity": 1 },
+ { "variant_id": 456, "quantity": 1 }
+ ]
+ }
+ }
+
+ Old Syntax:
+
+ {
+ "order": {
+ "line_items": {
+ "1": { "variant_id": 123, "quantity": 1 },
+ "2": { "variant_id": 123, "quantity": 1 }
+ }
+ }
+ }
+ ```
+
+- Enable configuration of `Carmen.i18n_backend.locale` at `seeds.rb`
+
+ You can localize the `Country` and `State` seed data by adding [Carmen](https://github.com/jim/carmen) configuration to your `seeds.rb`.
+ See example below:
+
+ ```ruby
+ # add Carmen counfiguration with the following 2 lines
+ require 'carmen'
+ Carmen.i18n_backend.locale = :ja
+
+ Spree::Core::Engine.load_seed if defined?(Spree::Core)
+ Spree::Auth::Engine.load_seed if defined?(Spree::Auth)
+ ```
+
+ https://github.com/spree/spree/pull/6607
+
+ Tomoki Odaka
diff --git a/guides/src/content/release_notes/3_1_0.md b/guides/src/content/release_notes/3_1_0.md
new file mode 100644
index 00000000000..eee490af83b
--- /dev/null
+++ b/guides/src/content/release_notes/3_1_0.md
@@ -0,0 +1,284 @@
+---
+title: Spree 3.1.0
+section: release_notes
+order: 6
+---
+
+## Major/New Features
+
+### Store Credits support out of the box
+
+Adds Store Credit payment method. Store credit can be granted in Admin Panel
+by store owners and is frozen after first usage. Store credit behaves like a
+Credit Card in that once an amount is authorized, it cannot be used elsewhere.
+
+Store Credit can be used by customers on the checkout to pay for the order
+completely or be combined with other payment methods. In the near future
+we will work on official Spree Gift Cards extension which will use
+Store Credit as it's framework.
+
+Contributed by [Jeff Dutil](https://github.com/JDutil),
+[Peter Berkenbosch](https://github.com/peterberkenbosch),
+[Michael Lippold](https://github.com/smartacus),
+[Marc Leglise](https://github.com/mleglise) &
+[Spark Solutions](http://sparksolutions.co)
+
+### Versioned API
+
+While we've had the ability to version our API we weren't making use of it.
+Now that we're beginning to write a new API we've added a v1 namespace,
+and default routing `/api` requests to use `/api/v1`. The `/api/v2` will be opt-in
+until we feel it is complete, and have deprecated /api/v1 (likely for Spree 4).
+
+Conitrbuted by [Ben A. Morgan](https://github.com/BenMorganIO)
+
+### Dynamic prices depending on zone for VAT countries
+
+The European Union has come up with new legislation requiring digital products
+to be taxed using the customer's shipping address. In turn, this means that prices
+have to be shown depending on the current order's tax zone.
+
+Your order will always use the price for the current tax zone. After the address step in
+the checkout process, the order will fetch the prices from the Variant again in order to
+make sure they're correct.
+
+For more information, see the [taxation guide](http://guides.spreecommerce.org/developer/taxation.html).
+
+Conitrbuted by [Martin Meyerhoff](https://github.com/mamhoff)
+
+### Future Discontinue of Products & Variants
+
+Soft deleting means that the records are left in the database but behave as if they are really deleted. Because associations from other objects (like line itmes to variant) won't normally see the deleted, core code is forced (unnaturally) to use scopes like `.with_deleted`
+
+We are fixing this by adding new feilds 'Discontinue On' to products & variants (discontinue_on)
+
+This fixes a design flaw in that in most stores these objects really should not be considered "deleted." The approach proposed solves the underlying flaw and all the related bugs caused by this flaw in the following ways:
+
+- Migrate the timestamps deleted_at to discontinue_on (when possible), and un-delete the deleted variants (when there is not matching SKU) and products (when there is no matching slug)
+
+- Redefine scopes Products object (see active, not_discontinued, etc)
+
+- Removes all references to unscope association chains from other objects to the Product & Variant objects in places where unscope is used explicitly to work-around the default scope problem. (This is a big win because it makes the associations cleaner and easier to work with.)
+
+- Although it is slightly counter-intuitive, we have left the deleted_at fields in place (although their data will be moved to discontinued_at field and their values will be reset to NULL in the db migration). You can (and should!) use deleting to remove human-error mistakes (real mistakes) before the items get sold in your store, or in the case when you have duplicate slugs (Products) or SKUs (Variants) in your database. In those special cases only, you should continue to use delete. In all other cases, use the new discontinue_on feature.
+
+- You can only delete a Product or Variant if no orders have been placed for that product/variant. Once the variant is associated with a Line item, it can never be "deleted," and instead you must use the new discontinue_on feature. Model-level checks (before_destroy) enforce this.
+
+- Note: The DB migration should fix your database correctly unless you have created new Products & Variants with matching slugs/SKUs of deleted records. In this case, you must use the included rake db:fix_orphan_line_items task to clean up your records. Both the schema migration and the script are very pro-active in helping you fix your own database.
+
+Contributed by [Jason Fleetwood-Boldt](https://github.com/jasonfb) & [Spark Solutions](http://sparksolutions.co)
+
+### Fully responsive (RWD) notification emails
+
+Notification emails such as Order Confirmation email or Shipment Confirmation email
+are now displayed properly on any screen size (mobile, tablet, dektop). User experience
+of those emails were also improved with clickable products, store logo and so on.
+
+Contributed by [Spark Solutions](http://sparksolutions.co)
+
+### Return Authorizations & Customer Returns Admin Panel screens
+
+The returns index screens provide a listing of returns authorizations and customer returns.
+So you can browse them more easy instead of accessing them trough an order.
+You can easily search and filter both Return Authorizations and Customer Returns.
+
+Conitrbuted by [Rein Aris](https://github.com/reinaris) & [VinSol](http://vinsol.com)
+
+### Admin Panel User Experience fixes
+
+We've put a lot of work to make the Admin Panel more user-friendly, this includes among other things:
+
+- new `breadcrumbs` navigation [#7319](https://github.com/spree/spree/pull/7319)
+
+- unified and fixed `form validations` [#7306](https://github.com/spree/spree/pull/7306) [#7314](https://github.com/spree/spree/pull/7314) [#7315](https://github.com/spree/spree/pull/7315)
+
+- new order's completion date format with time [#7208](https://github.com/spree/spree/pull/7208)
+
+- `Stock Movement` form moved to the new modern look & feel [#7209](https://github.com/spree/spree/pull/7209)
+
+- `per_page`, `pagination` & `filtering` fixes for records listings [#6971](https://github.com/spree/spree/pull/6971)
+
+Contributed by [Spark Solutions](http://sparksolutions.co) & [Vinsol](http://vinsol.com)
+
+## Upgrade
+
+### Update Gemfile & Run Migrations
+
+### Other Gotchas
+
+#### Make sure to v1 namespace custom rabl templates & overrides.
+
+If your rabl templates reference others with extend you'll need to add the v1 namespace.
+
+For example:
+
+```ruby
+extends 'spree/api/zones/show'
+```
+
+Becomes:
+
+```ruby
+extends 'spree/api/v1/zones/show'
+```
+
+#### Remove Spree::Config.check_for_spree_alerts
+
+If you were disabling the alert checks you'll now want to remove this preference as it's no longer used.
+
+#### Noteworthy Changes
+
+Also please review each of the noteworthy changes, and ensure your customizations
+or extensions are not effected. If you are affected by a change, and have any
+of your own tips please submit a PR to help the next person!
+
+## Full Changelog
+
+You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/3-0-stable...3-1-stable).
+
+## Noteworthy Changes
+
+- API v1 namespace to begin transition to v2.
+
+ [Ben A. Morgan](https://github.com/spree/spree/pull/6046)
+
+- Remove all HABTM associations in favour of HMT associations.
+
+ This removes the `Spree::ShippingMethod::HABTM` error message;
+ Allows users to extend the joins tables since they are now models;
+ And use the [Apartment](https://github.com/influitive/apartment) gem to their hearts content.
+
+ [Ben A. Morgan](https://github.com/spree/spree/pull/6627) & [VinSol](https://github.com/spree/spree/pull/7034)
+
+- Removed `Spree::Alert`
+
+ [Jeff Dutil](https://github.com/spree/spree/pull/6516)
+
+- Remove automatic payment creation with default credit card
+
+ [Darby Perez](https://github.com/spree/spree/pull/6601)
+
+- Allow checkout errors to be displayed when updating customer details
+
+ [Darby Perez](https://github.com/spree/spree/pull/6604)
+
+- Add default Refund Reason to `seeds.rb`
+
+ Creating a Refund will fail if there's no refund reason record in the database. That
+ reason has to have the name set to "Return processing" and the mutable flag set to `false`.
+
+ See https://github.com/spree/spree/blob/master/core/app/models/spree/refund_reason.rb#L5-L10
+
+ [Martin Meyerhoff](https://github.com/spree/spree/pull/6528)
+
+- Add a `current_price_options` helper to guide price calculation in the shop
+
+ When you use dynamic prices (as detailed above), those prices will depend on something
+ (like the tax zon of the current order, or whether your customer is a business customer).
+ These option are set using the new `current_price_options` helper. If your prices depend on
+ something else, overwrite this method and add more key/value pairs to the Hash it returns.
+
+ Be careful though to also patch the following parts of Spree accordingly:
+
+ - `Spree::VatPriceCalculation#gross_amount`
+ - `Spree::LineItem#update_price`
+ - `Spree::Stock::Estimator#taxation_options_for`
+ - Subclass the `DefaultTax` calculator
+
+ [Martin Meyerhoff](https://github.com/spree/spree/pull/6662)
+
+- Added `Spree.admin_path` option for a dynamic admin path; making automated 'script' attacks on the backend more difficult.
+
+ You can simply configure the option by assigning the path in your Spree initializer:
+
+ ```ruby
+ Spree.admin_path = "/my-secret-backend"
+ ```
+
+ NOTE: Plugins are not converted and still use the default /admin path. But these plugins can be
+ changed easily by adding the `path: Spree.admin_path` option in the routes.
+
+ [Rick Blommers](https://github.com/spree/spree/pull/6739) & [VinSol](https://github.com/spree/spree/pull/7065)
+
+- Changed to Use Time.current instead of Time.now
+
+ Rails uses config.time_zone to set time zone for the application, but Time.now uses server time zone instead
+ of using set config.time_zone. So, in order to use application time zone we need to use Time.current/Time.zone.now.
+
+ [Abhishek Jain](https://github.com/spree/spree/pull/6761)
+
+- Removed `Order#has_available_shipment`, which was unnecessary since it always returned nil
+
+ [VinSol](https://github.com/spree/spree/pull/7007)
+
+- (OOS) Out of Stock Product page: better handling of cart form user experience
+
+ [Spark Solutions](https://github.com/spree/spree/pull/6970)
+
+- Spree Command Installer won't lock you to a specific Spree patch version, we'll produce eg. `gem 'spree', '~> 3.1.0'` for easier updating
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7018)
+
+- Spree Auth Devise & Spree Gateway available from RubyGems
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7047)
+
+- Added `/forbidden` path
+
+ [Faruk Aydin & Spark Solutions](https://github.com/spree/spree/pull/6991)
+
+- Changed `before_filter` to `before_action`
+
+ [VinSol](https://github.com/spree/spree/pull/7114)
+
+- Additional database indexes for better performance
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7098)
+
+- Removed `Order#shipping_method_id` column
+
+ [VinSol](https://github.com/spree/spree/pull/6966)
+
+- Added `OrderContents#remove_line_item` method
+
+ [wuboy0307](https://github.com/spree/spree/pull/6934)
+
+- Ensure that Order#guest_token is unique
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7184)
+
+- Added missing views for image, option_type, option_value API controllers
+
+ [Alex B.](https://github.com/spree/spree/pull/7159)
+
+- Renamed `Order#update!` to `update_with_updater!` as it conflicts with rails `update!` method, also deprecate current update! method
+
+ [VinSol](https://github.com/spree/spree/pull/7144)
+
+- Removed `Calculator#register` method
+
+ [VinSol](https://github.com/spree/spree/pull/7001)
+
+- Added HTML5 Product Microdata information
+
+ [Rehan Jaffer](https://github.com/spree/spree/pull/6986)
+
+- Moved user class extensions to a module
+
+ [Alessandro Lepore](https://github.com/spree/spree/pull/6740)
+
+- Added caching for default store and current tracker
+
+ [wuboy0307](https://github.com/spree/spree/pull/6671) & [Spark Solutions](https://github.com/spree/spree/pull/7223)
+
+- Added `destroy` functionality to `refund_reason` and `return_authorization_reason`
+
+ [VinSol](https://github.com/spree/spree/pull/7049)
+
+- Deprecated `Spree::Validations::DbMaximumLengthValidator`, added `DbMaximumLengthValidator` instead
+
+ [VinSol](https://github.com/spree/spree/pull/7062)
+
+- Deprecated Spree CMD installer in favour of standard gem-like installation process
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7263)
diff --git a/guides/src/content/release_notes/3_2_0.md b/guides/src/content/release_notes/3_2_0.md
new file mode 100644
index 00000000000..404332475f7
--- /dev/null
+++ b/guides/src/content/release_notes/3_2_0.md
@@ -0,0 +1,280 @@
+---
+title: Spree 3.2.0
+section: release_notes
+order: 5
+---
+
+## Major/New Features
+
+### Rails 5 support
+
+`Spree 3.2` is now compatible with `Rails 5` compared to `3.1` which used to run on `Rails 4.2`.
+Thanks to that you can start using all of the [new great features available in Rails 5](http://edgeguides.rubyonrails.org/5_0_release_notes.html)
+
+Contributed by [Spark Solutions](http://sparksolutions.co) & [Vinsol](http://vinsol.com) & [John Hawthorn](https://github.com/jhawthorn)
+
+### Products Tagging
+
+You can now tag Products additionally to just using Taxons. Tagging module uses `acts-as-taggable-on` gem. You can easily fetch a list of Products associated to any given tag(s) which should be very helpful for creating new dynamic products listings.
+
+Contributed by [Vishal Zambre](https://github.com/spree/spree/pull/7347)
+
+### Promo code applier on Cart
+
+Discount code applier was brought back to the Cart view in customer frontend. Now enhanced, more user friendly and using API V1.
+
+Contributed by [Vinsol](https://github.com/spree/spree/pull/7684)
+
+### Extended Google Analytics Enhanced Ecommerce integration
+
+Spree now passes more meaningful information into Google Analytics when adding Products to Cart like:
+
+- product category name
+- product brand name
+- variant description
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/7727)
+
+### New Spree open source branding
+
+We've changed logo for default customer frontend and Admin Panel to differentiate
+Spree Open Source project from Spree Commerce (company).
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/7707)
+
+### Lighter and more user-friendly Admin Panel
+
+We've removed a lot of unused, deprecated, outdated code and assets from the Admin Panel,
+overall reducing the codebase by a lot. We've also fixed a huge amount of UX issues
+like missing validation, bad navigation or lack of thereof and so on.
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pulls?q=is%3Apr+is%3Aclosed+label%3A%22Admin+Panel%22)
+
+### Semantic versioning for future Extensions
+
+We're changing how extensions dependencies work. Previously you had to match
+extension branch to Spree branch. Starting from now `master` branch of all
+generated extensions will work with Spree >= `3.1` and < `4.0`. Thanks to
+that we're dropping versioning of extensions same as Spree and using
+standard [Semantic versioning](http://semver.org/).
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/7747)
+
+### More developer-friendly Extension generator
+
+Extension template used by the generator includes now Travis CI config with
+settings to test extension against Spree 3.1, 3.2 & 3.3 (edge), PostgreSQL,
+MySQL, Ruby 2.2 & 2.3 thanks to [Appraisals gem](https://github.com/thoughtbot/appraisal)
+by thoughtbot.
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/7761)
+
+## Upgrade
+
+### Update your Rails version to 5.0
+
+Please follow the
+[official Rails guide](http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-4-2-to-rails-5-0)
+to upgrade your store.
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.2.0'
+gem 'spree_auth_devise', '~> 3.2.0'
+gem 'spree_gateway', '~> 3.2.0'
+```
+
+### Update your extensions
+
+We're changing how extensions dependencies work. Previously you had to match
+extension branch to Spree branch. Starting from now `master` branch of all
+`spree-contrib` extensions should work with Spree >= `3.1` and < `4.0`. Please change
+your extensions in Gemfile eg.:
+
+from:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero', branch: '3-1-stable'
+```
+
+to:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+### Other Gotchas
+
+#### Ruby >= 2.2.2 required
+
+As Rails 5 is now a dependency it requires at least Ruby 2.2.2 to run. Ruby 2.3
+is also supported.
+
+#### Noteworthy Changes
+
+Also please review each of the noteworthy changes, and ensure your customizations
+or extensions are not effected. If you are affected by a change, and have any
+of your own tips please submit a PR to help the next person!
+
+## Full Changelog
+
+You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/3-1-stable...master).
+
+## Noteworthy Changes
+
+- Removed previously deprecated Spree CMD installer
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7278)
+
+- Removed unused `Spree::StoreController#apply_coupon_code` action
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7284)
+
+- Removed `Admin::SearchController` and pointed Admin Panel search actions to API endpoints
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7444)
+
+- Removed previously deprecated `Spree::ProductsHelper::line_item_description`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7283)
+
+- Removed unused method `Spree::Admin::BaseHelper#attribute_name_for`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7282)
+
+- Removed unused `Spree::Admin::InventorySettingsHelper`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7282)
+
+- Removed unused `Spree::Admin::TablesHelper`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7282)
+
+- Removed unused `Spree::Admin::ProductsHelper`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7282)
+
+- Removed unused method `Spree::Admin::NavigationHelper#collapse_sidebar_link`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7282)
+
+- Removed unused method `Spree::Admin::NavigationHelper#configurations_menu_item`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7282)
+
+- Moved `Spree::PromotionRulesHelper` to `Spree::Admin::PromotionRulesHelper`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7282)
+
+- Fixed permissions for Product associations management in Admin Panel
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7663)
+
+- ActiveMerchant updated to `~> 1.59`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7616)
+
+- Added `created_at` to Variant model
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7627)
+
+- Additional Admin Panel usability fixes
+
+ [Nimish Gupta](https://github.com/spree/spree/pull/7551, https://github.com/spree/spree/pull/7552. https://github.com/spree/spree/pull/7553)
+
+- Allow order to transit from resumed state to returned state
+
+ [Nimish Gupta](https://github.com/spree/spree/pull/7554)
+
+- Changed `state` translation key in Admin Panel to `status`
+
+ [orbit-dna](https://github.com/spree/spree/pull/7557)
+
+- Fixed API pagination `per_page` and `current_page` values (now always returned as integer)
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7550)
+
+- Display Spree version in Admin Panel
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7685)
+
+ By default, it's displayed only for Admin users who can manage the current Store, but can be disabled altogether by changing preferences:
+
+ ```
+ Spree::Config[:admin_show_version] = false
+ ```
+
+ in `config/initializers/spree.rb`
+
+- New preferences for Admin Panel `per_page` settings, default value for all of them is now
+ `Kaminari.config.default_per_page` (by default `25`)
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7730)
+
+ - `orders_per_page` renamed to `admin_orders_per_page`
+ - `properties_per_page` renamed to `properties_per_page`
+ - `promotions_per_page` renamed to `admin_promotions_per_page`
+ - `customer_returns_per_page` renamed to `admin_customer_returns_per_page`
+ - added `admin_users_per_page`
+
+- Removed unused `Spree::Admin::UsersController#json_data` method
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7731)
+
+- Added validation errors and other UX fixes for return process in Admin Panel
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7728)
+
+- Added GitGub issue template
+
+ [Josh Powell](https://github.com/spree/spree/pull/7721)
+
+- Added revert and deactivate methods to allow actions to take place when a
+ Promotion is removed from the Cart
+
+ [Joe Connor](https://github.com/spree/spree/pull/7705)
+
+- Use `Spree::Money` to display values in Sales Total report
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7702)
+
+- Fixed filtering out Products by `discontinue_on` in Admin Panel products index
+
+ [Joe Swann](https://github.com/spree/spree/pull/7686)
+
+- Allow title separator to be customized
+
+ [Ryan Siddle](https://github.com/spree/spree/pull/7672)
+
+- Additional fixes for Spree mounted in other then default `/` mountpoint
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7725)
+
+- Search for current Order in Frontend using guest token only
+
+ [Björn Andersson](https://github.com/spree/spree/pull/7626)
+
+- Deprecated `Spree::Calculator::FreeShipping`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7753)
+
+- Added promotion rule for shipping country
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7542)
diff --git a/guides/src/content/release_notes/3_3_0.md b/guides/src/content/release_notes/3_3_0.md
new file mode 100644
index 00000000000..32f93bd8c15
--- /dev/null
+++ b/guides/src/content/release_notes/3_3_0.md
@@ -0,0 +1,318 @@
+---
+title: Spree 3.3.0
+section: release_notes
+order: 4
+---
+
+## Major/New Features
+
+### Rails 5.1 support
+
+`Spree 3.3` is now compatible with `Rails 5.1` compared to `3.2` which used to run on `Rails 5.0`.
+Thanks to that you can start using all of the [new great features available in Rails 5](http://edgeguides.rubyonrails.org/5_1_release_notes.html)
+
+Contributed by [Josh Powell](https://github.com/joshRpowell) & [Spark Solutions](http://sparksolutions.co) & [John Hawthorn](https://github.com/jhawthorn)
+
+### Ruby 2.4 support
+
+Spree now works with Ruby `2.2` (`>= 2.2.7`), `2.3.x` and `2.4.x`.
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/7934)
+
+### Segment.com tracker integration
+
+We've extended Tracker system to include other trackers besides Google Analytics.
+First of the bunch is [Segment](http://segment.com/) which enables you to
+connect your store with over 200 analytics engines, CRMs, live chats, remarketing platforms,
+A/B systems and much more. Now in Spree out of the box! No additional development required!
+
+Developed during a [Spree hackaton](https://www.facebook.com/sparksolutions.co/posts/496593110678871).
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/8157)
+
+### Preventing duplicate values for number fields
+
+We've added unique indexes and uniqueness validation on `number` field for those models:
+
+- `CustomerReturn`
+- `Order`
+- `Payment`
+- `Reimbursement`
+- `ReturnAuthorization`
+- `Shipment`
+- `StockTransfer`
+
+This change will fix records with duplicate numbers. Migration scripts we're take care of that.
+
+**WARNING** migration process can take considerable amount of time, depending on volume of your data
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/7870)
+
+### Added missing indexes and unique indexes
+
+Besides number fields we've added multiple regular & unique indexes that were missing. This will keep data consistency of your app in check and also will boost it performance.
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/7905)
+
+### Add `quantity` to inventory units and split on demand
+
+Optimising Shipments and Inventory Units.
+
+- Creates a single `inventory_unit` per `state`, per `line_item`, per `stock_location` with a `quantity` field indicating inventory units in that state.
+- Adds a `return_quantity` field in `ReturnAuthorization` indicating the number of units to be returned/exchanged.
+- Split inventory unit to extract the portion/quantity that needs to be returned. This might be further split depending upon stock availability at the time of generating return.
+- Changes prioritizer and adjuster to maximise the number of `on_hand` items from available stocks in multiple `stock_locations`
+- Change `splitters/weight.rb` to use **Worst Fit** algo.
+
+Contributed by [Vinsol](https://github.com/spree/spree/pull/7790)
+
+### Better Store Credits management in customer Frontend and Admin Panel
+
+Store credit removal feature on spree front-end for partially paid order (with store credit payments). User can remove its store credit payment if additional payment is required/
+
+Admin user can do the same from Admin Panel.
+
+Contributed by [Vinsol](https://github.com/spree/spree/pull/8026)
+
+## Upgrade
+
+### Update your Rails version to 5.1
+
+Please follow the
+[official Rails guide](http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-5-0-to-rails-5-1)
+to upgrade your store.
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.3.0'
+gem 'spree_auth_devise', '~> 3.3'
+gem 'spree_gateway', '~> 3.3'
+```
+
+### Update your extensions
+
+We're changing how extensions dependencies work. Previously you had to match
+extension branch to Spree branch. Starting from Spree 3.2 release `master` branch of all
+`spree-contrib` extensions should work with Spree >= `3.1` and < `4.0`. Please change
+your extensions in Gemfile eg.:
+
+from:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero', branch: '3-1-stable'
+```
+
+to:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+### Other Gotchas
+
+#### Include `UserMethods` in your `User` class
+
+With this release we're not including this automatically. You need to do it manually if you're not using `spree_auth_devise`.
+
+You need to include `Spree::UserMethods` in your user class, eg.
+
+```ruby
+class User
+ include UserAddress
+ include UserMethods
+ include UserPaymentSource
+end
+```
+
+#### Update `aws-sdk` gem to `>= 2.0`
+
+Spree 3.3 comes with paperclip 5.1 support so if you're using Amazon S3 storage you need to change in your Gemfile, from:
+
+```ruby
+gem 'aws-sdk', '< 2.0'
+```
+
+to:
+
+```ruby
+gem 'aws-sdk', '>= 2.0'
+```
+
+and run `bundle update aws-sdk`
+
+In your paperclip configuration you also need to specify
+`s3_region` attribute eg. https://github.com/spree/spree/blame/master/guides/content/developer/customization/s3_storage.md#L27
+
+Seel also [RubyThursday episode](https://rubythursday.com/episodes/ruby-snack-27-upgrade-paperclip-and-aws-sdk-in-prep-for-rails-5) walkthrough of upgrading paperclip in your project.
+
+#### Add jquery.validate to your project if you've used it directly from Spree
+
+If your application.js file includes line
+`//= require jquery.validate/jquery.validate.min`
+you will need to add it this file manually to your project because this library was
+[removed from Spree in favour of native HTML5 validation](https://github.com/spree/spree/pull/8173).
+
+#### Noteworthy Changes
+
+Also please review each of the noteworthy changes, and ensure your customizations
+or extensions are not effected. If you are affected by a change, and have any
+of your own tips please submit a PR to help the next person!
+
+## Full Changelog
+
+You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/3-2-stable...3-3-stable).
+
+## Noteworthy Changes
+
+- Removed `jquery.validate` in favour of HTML5 validation for address on checkout
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8173)
+
+- Remove frontend routes from core, to allow usage without default Spree frontend
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8172)
+
+- Use HTML5 `email_field` on frontend checkout
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8160)
+
+- Frontend views cleanup
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8174)
+
+- `acts_as_list` index starts from 1
+
+ [Sai Ram Kunala](https://github.com/spree/spree/pull/7785) & [Jonathan Kelley](https://github.com/spree/spree/issues/7996)
+
+- API: Remove core dependency on api user db column
+
+ [Viktor Fonic](https://github.com/spree/spree/pull/7912)
+
+- Added missing permission checks in Admin Panel
+
+ [Vinsol](https://github.com/spree/spree/pull/8076)
+
+- Loosen project dependency on `FriendlyId` in Admin Panel and API endpoints
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8072)
+
+- Admin has to confirm clear cache in Admin Panel
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8162)
+
+- Added uniqueness validation added on promotion's code
+
+ [Vinsol](https://github.com/spree/spree/pull/8067)
+
+- Restrict deletion of payment method if associated with payments or credit cards
+
+ [Vinsol](https://github.com/spree/spree/pull/8055)
+
+- Uniqueness validation added on Taxonomy's name and Taxon's permalink
+
+ [Vinsol](https://github.com/spree/spree/pull/8050)
+
+- Admin Panel `field_container` now supports options like content_tag does
+
+ [Fabrizio Monti](https://github.com/spree/spree/pull/8163)
+
+- Simplify `EmailValidator` regexp to align with `Devise.email_regexp`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8171)
+
+- Schema.org fixes for products microformat data
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8039)
+
+- Allow valid image types to be overridden without deleting validations for `Spree::Image`
+
+ [Tom Chipchase](https://github.com/spree/spree/pull/8176)
+
+- Updated `paperclip` to `~> 5.1.0`
+
+ [Josh Powell](https://github.com/spree/spree/pull/7858)
+
+- Updated `acts-as-taggable-on` to `5.0`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8015)
+
+- Updated `FriendlyId` to `5.2`
+
+ [Zeke Gabrielse](https://github.com/spree/spree/pull/8005)
+
+- Loosen ActiveMerchant dependency (`~> 1.67`)
+
+ [Justin Grubbs](https://github.com/spree/spree/pull/8081)
+
+- Updated `cancancan` to `2.0`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8009)
+
+- Updated `jquery-ui-rails` to `6.0.1` & removed unused assets
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8000)
+
+- Removing mutant dependency
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8013)
+
+- Updated `state_machines-activerecord` to `~> 0.5`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8083)
+
+- Removed previously deprecated `LineItem#invalid_quantity_check`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8040)
+
+- Removed deprecated automatic `UserMethods` injection
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8031)
+
+- Deprecated `Spree::Calculator::PercentPerItem` & removed `Spree::Calculator::FreeShipping`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8030)
+
+- Removed minimum length validation on product slug
+
+ [Viktor Fonic](https://github.com/spree/spree/pull/8028)
+
+- Removed `font-awesome-rails` dependency
+
+ [Spark Solutions](https://github.com/spree/spree/pull/7949)
+
+- Deprecate `Shipment#editable_by?` & `Shipment#send_shipped_email`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8106)
+
+- Deprecate `Variant#having_orders` & `Variant#on_backorder`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8225)
+
+- Deprecate `DelegateBelongsTo`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8179)
+
+- Removed unused `jquery.migrate`, `normalize` & `skeleton` assets from Core
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8185)
+
+- Removed `shoulda-matchers` dependency
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8158)
diff --git a/guides/src/content/release_notes/3_4_0.md b/guides/src/content/release_notes/3_4_0.md
new file mode 100644
index 00000000000..a96c135cc41
--- /dev/null
+++ b/guides/src/content/release_notes/3_4_0.md
@@ -0,0 +1,188 @@
+---
+title: Spree 3.4.0
+section: release_notes
+order: 3
+---
+
+## Major/New Features
+
+3.4 is a smaller release to lay the groundwork for major new features
+described in [Spree Development Roadmap](https://github.com/spree/spree/milestones?direction=asc&sort=due_date&state=open).
+It also contains number of bug fixes and improvements besides described in this document.
+
+### Enhanced codebase quality
+
+We've removed tons of unused & deprecated code and on top of that, we've updated
+RuboCop rules and tidied up the code quality of all Spree modules.
+
+Contributed to [Spark Solutions](https://github.com/spree/spree/issues/8268)
+
+### Enhanced Segment.com integration
+
+We've added [Segment.com integration in Spree 3.3](https://guides.spreecommerce.org/release_notes/spree_3_3_0.html#segmentcom-tracker-integration)
+but it was still limited. Since then we've put a lot of work to implement
+tracking of nearly all e-commerce events supported by Segment.com.
+
+Contributed to [Spark Solutions](https://github.com/spree/spree/issues/8264)
+
+### Updated User Documentation
+
+We've updated the entire [User section](https://guides.spreecommerce.org/user/) of
+[Spree Documentation](https://guides.spreecommerce.org/) also adding new content describing new features like
+[Store Credits](https://guides.spreecommerce.org/user/configuring_store_credit_categories.html).
+
+This is part of our [Guides 2.0 project](https://github.com/spree/spree/issues/8270)
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/8256)
+
+### Searchable Documentation
+
+Thanks to cooperation with [Algolia DocSearch](https://community.algolia.com/docsearch/)
+you can easily search the entire [Spree Documentation](https://guides.spreecommerce.org/).
+
+This is part of our [Guides 2.0 project](https://github.com/spree/spree/issues/8270)
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/8300)
+
+### Spree Demo
+
+You can fire up the newest fully functional version of Spree
+on Heroku with a just one click :)
+
+[Try it out!](https://heroku.com/deploy?template=https://github.com/spree/spree)
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/8266)
+
+### Spree Frontend views exporter
+
+You can easily copy all of the default spree views into
+your project with just one command line:
+
+```bash
+rails g spree:frontend:copy_views
+```
+
+We hope will make frontend customization easier.
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/issues/8265)
+
+## Upgrade
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.4.0'
+gem 'spree_auth_devise', '~> 3.3'
+gem 'spree_gateway', '~> 3.3'
+```
+
+### Update your extensions
+
+We're changing how extensions dependencies work. Previously you had to match
+extension branch to Spree branch. Starting from Spree 3.2 release `master` branch of all
+`spree-contrib` extensions should work with Spree >= `3.1` and < `4.0`. Please change
+your extensions in Gemfile eg.:
+
+from:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero', branch: '3-1-stable'
+```
+
+to:
+
+```ruby
+gem 'spree_braintree_vzero', github: 'spree-contrib/spree_braintree_vzero'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+### Other Gotchas
+
+#### Migrate Spree::Taxon icons to Spree Assets
+
+We changed `Spree::Taxon` icon to use `Spree::Asset` to unify attachment usage
+across all Spree models. If you were using icon images in `Spree::Taxon`
+please run this to migrate your icons:
+
+```bash
+rails db:migrate_taxon_icons
+```
+
+#### Noteworthy Changes
+
+Also please review each of the noteworthy changes, and ensure your customizations
+or extensions are not effected. If you are affected by a change, and have any
+of your own tips please submit a PR to help the next person!
+
+## Full Changelog
+
+You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/3-3-stable...master).
+
+## Noteworthy Changes
+
+- Moved Admin Panel account menu into Spree core and redesigning it
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8249)
+
+- Better multi store support for Spree frontend (use `current_store` with `last_incomplete_spree_order`)
+
+ [Alexandre Ferraille](https://github.com/spree/spree/pull/8078)
+
+- Added OptionTypes & OptionValues API V1 Documentation
+
+ [Yatindra Rao](https://github.com/spree/spree/pull/8394)
+
+- Removed previously deprecated `Shipment#editable_by?` & `Shipment#send_shipped_email`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8276)
+
+- Removed previously deprecated `Variant#having_orders` & `Variant#on_backorder`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8277)
+
+- Removed previously deprecated `DelegateBelongsTo`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8275)
+
+- Updated `Address#require_phone?` to use `address_requires_phone` preference
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8290)
+
+- Performance optimization - replaced `pluck.sum` and `map.sum` with `sum`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8283)
+
+- Performance optimization - replaced `.count > 0` with `.exists?`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8282)
+
+- Removed previously deprecated `Calculator::PercentPerItem`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8314)
+
+- Removed deprecated `StoreHelper` from frontend
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8316)
+
+- Removed deprecated `TestingSupport::MicroData`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8315)
+
+- Deprecated `ContentController#show`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8379)
diff --git a/guides/src/content/release_notes/3_5_0.md b/guides/src/content/release_notes/3_5_0.md
new file mode 100644
index 00000000000..09dede1e416
--- /dev/null
+++ b/guides/src/content/release_notes/3_5_0.md
@@ -0,0 +1,183 @@
+---
+title: Spree 3.5.0
+section: release_notes
+order: 2
+---
+
+## Major/New Features
+
+3.5 is a smaller release to lay the groundwork for major new features
+described in [Spree Development Roadmap](https://github.com/spree/spree/milestones?direction=asc&sort=due_date&state=open).
+It also contains number of bug fixes and improvements besides described in this document.
+
+### Multi-store management
+
+We've added an ability to manage multiple stores in one Spree instance of your application.
+Thanks to that you can easily create stores per region / language / currency.
+This feature will be enhanced in the next releases.
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/8545)
+
+### Extracted Analytics tracker into an Extension
+
+Our goal is always to keep the core Spree lean and flexible that's why we've moved all of the code of
+Analytics Trackers to an Extension which you can use with any `Spree 3.1+` version.
+
+[Spree Analytics Trackers](https://github.com/spree-contrib/spree_analytics_trackers) extension currently supports Google Analytics and Segment.com.
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/8408)
+
+### Admin Panel views exporter
+
+You can easily copy all of the default spree admin Panel into
+your project with just one command line:
+
+```bash
+rails g spree:backend:copy_views
+```
+
+We hope will make backend customization the Admin Panel a lot easier.
+
+Contributed by [supatk](https://github.com/spree/spree/issues/8583)
+
+## Installation
+
+### Add Spree gems to Gemfile
+
+```ruby
+gem 'spree', '~> 3.5.0'
+gem 'spree_auth_devise', '~> 3.3'
+gem 'spree_gateway', '~> 3.3'
+```
+
+### Run `bundle install`
+
+### Use the install generators to set up Spree
+
+```shell
+rails g spree:install --user_class=Spree::User
+rails g spree:auth:install
+rails g spree_gateway:install
+```
+
+## Upgrade
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.5.0'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+### Other Gotchas
+
+#### Install Spree Analytics Trackers extension
+
+If you were previously using Analytics Trackers feature you need to install it as an extension
+as it was [extracted from the core](https://github.com/spree/spree/pull/8408).
+
+1. Add [Spree Analytics Trackers](https://github.com/spree-contrib/spree_analytics_trackers) to your `Gemfile`:
+
+```ruby
+gem 'spree_analytics_trackers', github: 'spree-contrib/spree_analytics_trackers'
+```
+
+2. Install the gem using Bundler:
+
+```bash
+bundle install
+```
+
+3. Copy and run migrations:
+
+```bash
+bundle exec rails g spree_analytics_trackers:install
+```
+
+You're good to go!
+
+## Noteworthy Changes
+
+Also please review each of the noteworthy changes, and ensure your customizations
+or extensions are not effected. If you are affected by a change, and have any
+of your own tips please submit a PR to help the next person!
+
+- Added `Address#EXCLUDED_KEYS_FOR_COMPARISION` so developers won't need to rewrite `Address#same_as` method
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8387)
+
+- Added `PromotionActionLineItem` validations
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8533)
+
+- Renamed `FrontendHelper#breadcrumbs` to `FrontendHelper#spree_breadcrumbs` and
+ `Admin::NavigationHelper#icon` to `Admin::NavigationHelper#spree_icon`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8445)
+
+- Deprecated `EnvironmentExtension`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8459)
+
+- Deprecated `render_404`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8465)
+
+- Updated javascript libraries in vendor
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8402)
+
+- Added `Dockerfile` for sandbox application
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8402)
+
+- Replaced phantomjs with chrome headless
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8429)
+
+- Replaced FactoryGirl with FactoryBot
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8431)
+
+- Moved EmailValidator from lib to app
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8612)
+
+- Added `rubocop-rspec` and fixed linter issues
+
+ [artplan1](https://github.com/spree/spree/pull/8574)
+
+- Added searching for a taxon with taxonomy in api
+
+ [saravanak](https://github.com/spree/spree/pull/8594)
+
+- `rescue nil` removed from promotion rules and and promotion actions partials
+
+ [leemour](https://github.com/spree/spree/pull/8510)
+
+- Dropped dependency on `with_model` gem
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8528)
+
+- Updated `paperclip` to `~> 6.0.0`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8775)
+
+## Full Changelog
+
+You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/3-4-stable...3-5-stable).
diff --git a/guides/src/content/release_notes/3_6_0.md b/guides/src/content/release_notes/3_6_0.md
new file mode 100644
index 00000000000..62ca2bac17f
--- /dev/null
+++ b/guides/src/content/release_notes/3_6_0.md
@@ -0,0 +1,99 @@
+---
+title: Spree 3.6.0
+section: release_notes
+order: 1
+---
+
+## Major/New Features
+
+Alongside Spree 3.5 (for Rails 5.1) we're releasing Spree 3.6 (for Rails 5.2).
+Spree 3.6 has all of the features of Spree 3.5 plus Rails 5.2,
+ActiveStorage and Ruby 2.5 support out of the box.
+
+If you're on a Spree 3.4 (or earlier) please see [Spree 3.5 release notes](https://guides.spreecommerce.org/release_notes/spree_3_5_0.html) also.
+
+### Rails 5.2 support
+
+`Spree 3.6` is now compatible with `Rails 5.2` compared to `3.5` which used to run on `Rails 5.1`.
+Thanks to that you can start using all of the [new great features available in Rails 5.2](http://edgeguides.rubyonrails.org/5_2_release_notes.html)
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/8601)
+
+### ActiveStorage support
+
+Active Storage facilitates uploading files to a cloud storage service like Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching those files to Active Record objects. It comes with a local disk-based service for development and testing and supports mirroring files to subordinate services for backups and migrations.
+
+ActiveStorage was introduced in Rails 5.2 and with Spree 3.6 release it's the default file stoage option. We'll still support Paperclip until Spree 4.0 when it will be removed from Spree core.
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/8709)
+
+### Ruby 2.5 support
+
+Spree now works with Ruby `2.2` (`>= 2.2.7`), `2.3.x`, `2.4.x` and `2.5.x`.
+
+Contributed by [Spark Solutions](https://github.com/spree/spree/pull/8743)
+
+## Installation
+
+### Add Spree gems to Gemfile
+
+```ruby
+gem 'spree', '~> 3.6.1'
+gem 'spree_auth_devise', '~> 3.3'
+gem 'spree_gateway', '~> 3.3'
+```
+
+### Run `bundle install`
+
+### Use the install generators to set up Spree
+
+```shell
+rails g spree:install --user_class=Spree::User
+rails g spree:auth:install
+rails g spree_gateway:install
+```
+
+## Upgrade
+
+### Update your Rails version to 5.2
+
+Please follow the
+[official Rails guide](http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-5-1-to-rails-5-2)
+to upgrade your store.
+
+### Update Gemfile
+
+```ruby
+gem 'spree', '~> 3.6.1'
+```
+
+### Run `bundle update`
+
+### Install missing migrations
+
+```bash
+rails spree:install:migrations
+rails spree_auth:install:migrations
+rails spree_gateway:install:migrations
+```
+
+### Run migrations
+
+```bash
+rails db:migrate
+```
+
+### Migrate to ActiveStorage (optional)
+
+Please follow the [official paperclip guide](https://github.com/thoughtbot/paperclip/blob/master/MIGRATING.md) if you
+want to use ActiveStorage instead of paperclip.
+
+You cann still use paperclip for attachment management by setting `SPREE_USE_PAPERCLIP` environment variable to `true`, but keep in mind that paperclip is DEPRECATED and we will remove paperclip support in Spree 4.0.
+
+## Noteworthy changes
+
+Please see [Spree 3.5 release notes](https://guides.spreecommerce.org/release_notes/spree_3_5_0.html)
+
+## Full Changelog
+
+You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/3-5-stable...3-6-stable).
diff --git a/guides/src/content/release_notes/3_7_0.md b/guides/src/content/release_notes/3_7_0.md
new file mode 100644
index 00000000000..b58bb6c5cce
--- /dev/null
+++ b/guides/src/content/release_notes/3_7_0.md
@@ -0,0 +1,260 @@
+---
+title: Spree 3.7.0
+section: release_notes
+order: 0
+---
+
+# Spree 3.7.0 Release Notes
+
+## Major/New Features
+
+3.7 release is our last 3.x line release bridging the gap between 3.x and Spree 4.0. This is a big release packed with several amazing features and a huge number of bug fixes ([over 700 commits by 17 contributors](https://github.com/spree/spree/compare/3-6-stable...3-7-stable)!). Upgrading to 3.7 guarantees a smooth and easy migration to [Spree 4.0](https://github.com/spree/spree/milestone/37) (April 2019 with Rails 6.0 support).
+
+**Spree 3.7** requires **Rails 5.2.2** and **Ruby 2.3.3** (or higher).
+
+### [Storefront API v2](/api/v2/storefront)
+
+We've worked hard over the last few months to deliver a completely new, easy to work with and lightweight REST API for building amazing customer interfaces with modern JavaScript libraries (React, Vue, Angular) and for native mobile apps. New Storefront API v2 is fast, easy to use and extend. It's also well documented in [OpenAPI 3.0 (Swagger) format](https://raw.githubusercontent.com/spree/spree/master/api/docs/v2/storefront/index.yaml) which you can [import into Postman app](http://blog.getpostman.com/2018/12/11/postman-supports-openapi-3-0/).
+
+New API is based on [JSON API](https://jsonapi.org/) spec and uses blazing fast Netflix [fast_json_api](https://github.com/Netflix/fast_jsonapi) serializer library. [Authentication](/api/v2/authentication) is based on Oauth using [doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) library. Besides that there are no additional dependencies making it lightweight and future-proof.
+
+Storefront API v2 consists of:
+
+- [Cart endpoints](/api/v2/storefront#tag/Cart) (Create Cart, Add Item, Set Quantity, Remove Item, Empty Cart)
+- [Checkout endpoints](/api/v2/storefront#tag/Checkout) (Update Checkout, Advance Checkout, Complete Checkout, Get Shiping Rates, Get Payment Methods)
+- [Products](/api/v2/storefront/#tag/Products) Catalog
+- [Taxons](/api/v2/storefront/#tag/Taxons) Catalog
+- [Account](/api/v2/storefront#tag/Account) (Account Information, Credit Cards)
+- [Countries](/api/v2/storefront/#tag/Countries)
+- [Authentication](/api/v2/authentication)
+
+All of the endpoints support JSON API's [Sparse Fieldsets](https://jsonapi.org/format/#fetching-sparse-fieldsets) to fix usual Over-Fetching issues and [Related Resources](https://jsonapi.org/format/#fetching-includes) to reduce the number of API queries you need to perform.
+
+### Service Oriented Architecture
+
+While building the API v2 we've also refactored a huge portion of Spree internals by introducing modular Service Oriented Architecture to the codebase.
+
+We're in the process of moving domain-specific code from models to `Service Objects` with a well-defined scope and predictable return values. All service objects include [Service Module](https://github.com/spree/spree/blob/master/core/lib/spree/service_module.rb) which unifies how those classes handle arguments and what they return.
+
+Also, we're moving away from ransack library by introducing `Finders` and `Sorters` classes for simpler fetching resources and collections.
+
+This makes Spree codebase easier to read and learn. It also makes any customizations way easier. At the same time, public APIs won't change a lot as providing backward compatibility is one of our top priorities.
+
+### Dependencies system
+
+We're introducing a new painless way of customizing Spree without the need of decorators. With Dependencies you can easily replace parts of Spree internals with your custom classes. You can replace Services, Abilities and Serializers. More will come in the future. We hope using Dependencies will remove the need for creating decorators at all!
+
+[See Documentation](/developer/customization/dependencies.html)
+
+### Removing Coffee Script
+
+CoffeeScript a few years back ago was a really great JavaScript enhancement. Nowadays with ES6 and TypeScript around it became obsolete. That's why we've converted all of the CoffeeScript assets in Spree and extensions to plain JavaScript and removed CoffeeScript dependency.
+
+### Improved MySQL support
+
+A lot of merchants were using Spree with MySQL for years now, but development of the platform was mainly focused on PostgreSQL support. We've changed that and all of our CI builds are tested and verified against both PostgreSQL and MySQL. We've also fixed all MySQL-related bugs.
+
+## Installation
+
+### Add Spree gems to Gemfile
+
+```ruby
+gem 'spree', '~> 3.7.0.rc1'
+gem 'spree_auth_devise', '~> 3.4'
+gem 'spree_gateway', '~> 3.4'
+```
+
+### Run `bundle install`
+
+### Use the install generators to set up Spree
+
+```shell
+rails g spree:install --user_class=Spree::User
+rails g spree:auth:install
+rails g spree_gateway:install
+```
+
+## Upgrade
+
+[Spree 3.6 to 3.7 upgrade guide](https://github.com/spree/spree/blob/master/guides/content/developer/upgrades/three-dot-six-to-three-dot-seven.md)
+
+## Noteworthy changes
+
+Please review each of the noteworthy changes to ensure your customizations or extensions are not affected. If you are affected by a change, and have any suggestions please submit a PR to help the next person!
+
+- Dropped support for Ruby 2.2
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8866)
+
+- Support multiple currencies for `Store Credits` management in Admin Panel
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8912)
+
+- Added information if Product is backorderable on Product page
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8438)
+
+- Improved OmniChannel support in the Admin Panel
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8892)
+
+- Improved error handling for XHR requests in the Admin Panel
+
+ [Vernon de Goede](https://github.com/spree/spree/pull/9089)
+
+- Improved Admin Panel Variant autocomplete
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8780)
+
+- `Order` is now associated with `Store`, the presence of `Store` is required for validation
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8926)
+
+- `Order` now has to have `currency` value set
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8927)
+
+- Made `CreditCard` deleted softly by default
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9054)
+
+- Payment `source` needs to be present (validation can be turned off via `source_required?` in `PaymentMethod`)
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9054)
+
+- Added `StockLocation#name` unique validation
+
+ [himanshumishra31](https://github.com/spree/spree/pull/8804)
+
+- Added `FulfilmentChanger` class
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8725)
+
+- Extended `OrderPromotion` model
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9126/commits/cdbcef82c3b90a9b07d107f7eb9cbea04dbe7266)
+
+- Added `ShippingRate#final_price` method
+
+ [Spark Solutions](https://github.com/spree/spree/commit/b55ac6e01f3cba9c4b7f4409de767646ff5f7bf7#diff-f679c11eac90d5fa687247a06a0de8db)
+
+- Added `Shipment#free?` method
+
+ [Spark Solutions](https://github.com/spree/spree/commit/5efa747f66c07beab6742d3b41e721c7727a435d#diff-f679c11eac90d5fa687247a06a0de8db)
+
+- Added `Product#default_variant` and `Product#default_variant_id` methods
+
+ [Spark Solutions](https://github.com/spree/spree/commit/0ec65dba11ccd969efd3944c76f3befb5a8fbcaa#diff-f679c11eac90d5fa687247a06a0de8db)
+
+- Delegated `iso` and `iso_name` in `Address` model to `Country`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9173/commits/f4405e8c3999bb844287b55617a3f318d31edb7f)
+
+- Replaced `jquery.cookie` with `js.cookie`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8758)
+
+- Fixed deprecation warning: `BigDecimal.new() is deprecated`
+
+ [Deepak Mahakale](https://github.com/spree/spree/pull/8897)
+
+- Fixed displaying currency in `Admin Panel` -> `Return Authorizations`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9005)
+
+- Fixed all deprecations of Ruby Money 6.13+
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9036)
+
+- Fallback to `Order#currency` if there's no currency set for `Adjustment`
+
+ [Spark Solutions](https://github.com/spree/spree/commit/04d4c94d30feb770ff4b14db8d7a597888535676#diff-f679c11eac90d5fa687247a06a0de8db)
+
+- Renamed `Order#guest_token` to `Order#token`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8826)
+
+- Renamed `Admin::GeneralSettingsHelper` to `Admin::CurrencyHelper`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8912)
+
+- Renamed `TaxonIcon` to `TaxonImage`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8879)
+
+- Deprecated `OrderContents` and replaced with several `Cart` services (eg. `Cart::AddItem`)
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8732/commits/f86b4c2c4d3fb468038bfb99389490be73ba998c)
+
+- Deprecated `Order#add_store_credit_payments` in favor of `Checkout::AddStoreCredit` service
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9146)
+
+- Deprecated `Order#remove_store_credit_payments` in favor of `Checkout::RemoveStoreCredit` service
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9152)
+
+- Deprecated `OrdersController#populate` and `OrdersController#populate_redirect`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9050/commits/215cb4391c574136f87824771e14ab211d6cf59c)
+
+- Deprecated `Address#same_as?` in favor of `Address#==`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8386)
+
+- Deprecated `Order#add register_line_item_comparison_hook` (please use `Rails.application.config.spree.line_item_comparison_hooks << hook` instead)
+
+ [Spark Solutions](https://github.com/spree/spree/commit/0a525ccab9ea0bc52cc4e249d53b38df6f231691)
+
+- Deprecated `ControllerHelpers::RespondWith`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9081)
+
+- Deprecated `BaseHelper#variant_options`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9105)
+
+- Deprecated `Address#iso_name`, please use `Address#country_iso_name`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9173)
+
+- Removed previously deprecated `Spree::Core::EnvironmentExtension`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8782)
+
+- Removed previously deprecated `ControllerHelpers::Common#render_404`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/8782)
+
+- Removed previously deprecated `EmailValidator`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9029)
+
+- Removed previously deprecated `NavigationHelper#icon`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9121)
+
+- Removed previously deprecated `FrontendHelper#breadcrums`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9121)
+
+- Removed `Product#distinct_by_product_ids`
+
+ [Spark Solutions](https://github.com/spree/spree/commit/960b211026fbec0095f774b69c78dea64559a6c3#diff-f679c11eac90d5fa687247a06a0de8db)
+
+- Bumped `highline` dependency to `2.0.x`
+
+ [Chamnap Chhorn
+ ](https://github.com/spree/spree/pull/9079)
+
+- Bumped `paperclip` dependency to `6.1.x`
+
+ [Josh Powell](https://github.com/spree/spree/pull/9028)
+
+- Bumped `bootstrap-sass` to `3.4.x`
+
+ [Spark Solutions](https://github.com/spree/spree/pull/9168)
+
+## Full Changelog
+
+You can view the full changes using [Github Compare](https://github.com/spree/spree/compare/3-6-stable...3-7-stable).
diff --git a/guides/src/content/release_notes/index.md b/guides/src/content/release_notes/index.md
new file mode 100644
index 00000000000..52b2eace088
--- /dev/null
+++ b/guides/src/content/release_notes/index.md
@@ -0,0 +1,12 @@
+---
+title: Spree
+section: release_notes
+---
+
+## Release Notes
+
+Each major new release of Spree has an accompanying set of release notes. The purpose of these notes is to provide a high level overview of what has changed since the previous version of Spree.
+
+If you are upgrading to a new version of Spree that is several versions ahead of your current version, it is suggested that you update one version at a time and follow the release notes associated with each update.
+
+You can find the detailed release notes for each version of Spree using the table of contents on the right hand side of this page.
diff --git a/guides/src/content/user/configuration/configuring_analytics.md b/guides/src/content/user/configuration/configuring_analytics.md
new file mode 100644
index 00000000000..7f596d5f254
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_analytics.md
@@ -0,0 +1,11 @@
+---
+title: Analytics Tracker
+---
+
+## Introduction
+
+Understanding your site's visitor traffic patterns are important for planning your company's marketing and growth strategies.
+
+### Google Analytics
+
+Spree's Admin Interface makes it easy for you to add the robust Google Analytics toolset to your site. [This blog entry](http://spreecommerce.org/pages/blog/ecommerce-tracking-in-spree) covers all of the intricacies with registering a Google Analytics account, adding the tracker to your site, and testing the functionality.
diff --git a/guides/src/content/user/configuration/configuring_general_settings.md b/guides/src/content/user/configuration/configuring_general_settings.md
new file mode 100644
index 00000000000..e207ededc25
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_general_settings.md
@@ -0,0 +1,52 @@
+---
+title: General Settings
+---
+
+## Introduction
+
+The General Settings section is where you will make site-wide settings about things like your store's name, security, and currency display. You can access this area by going to your Admin Interface, clicking "Configuration", then clicking the "General Settings link."
+
+
+
+Each setting on the page is covered below.
+
+### Site Name
+
+The Site Name is what is set in the `` tag of your website. It renders in your browser's title bar on every page of the public-facing area of the site.
+
+
+
+### Default SEO Title
+
+"SEO" stands for "Search Engine Optimization". It is a way to improve your store's visibility in search results. If you assign a value to the Default SEO Title field, it would override what you had set for the "Site Name" field.
+
+
+
+### Default Meta Keywords
+
+Meta keywords give search engines more information about your store. Use them to supply a list of the words and phrases that are most relevant to the products you offer. These keywords show up in the header of your site. The header is not visible to the casual site visitor, but it does inform your rankings with web browsers.
+
+***
+For more information about Search Engine Optimization, try reading the [Google Webmaster Tools topic](https://support.google.com/webmasters/answer/35291?hl=en) on the subject.
+***
+
+### Default Meta Description
+
+Whereas meta keywords constitutes a comma-separated list of words and phrases, the meta description is a fuller, prose description of what your store is and does. The phrasing you use can help distinguish you from any other e-commerce websites offering products similar to yours.
+
+### Site URL
+
+The site's URL is the website address for your store, such as "http://myawesomestore.com". This address is used when your application sends out confirmation emails to customers about their purchases.
+
+## Currency Settings
+
+The remaining settings all cover how currency is rendered in your store.
+
+
+
+### Choose Currency
+
+In the newest version of Spree, currency is chosen by the Primary Country you pick in Currency Settings.
+
+
+
diff --git a/guides/src/content/user/configuration/configuring_geography.md b/guides/src/content/user/configuration/configuring_geography.md
new file mode 100644
index 00000000000..8e1f6465afe
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_geography.md
@@ -0,0 +1,75 @@
+---
+title: Zones, Countries, and States
+---
+
+## Introduction
+
+Your Spree store allows you to make decisions about which parts of the world you will sell products to, and how those areas are grouped into geographical regions for the convenience of setting [shipping](shipments) and [taxation](taxation) policies. This is accomplished through the use of:
+
+* [zones](#zones)
+* [countries](#countries), and
+* [states](#states)
+
+### Zones
+
+Within a Spree store, zones are geographical groupings - collections of either states or countries. You can read all about zones in the [zones guide](zones), including how to [create zones](#zones#creating-a-zone), how to [add members to a zone](zones#adding-members-to-a-zone), and how to [remove members from a zone](zones#removing-members-from-a-zone).
+
+### Countries
+
+If you pre-loaded the seed data into your Spree store, then you already have several countries configured. You may want to edit items in this list based on your needs. To access the Countries list, go to your Admin Interface, click "Configuration", then click "Countries".
+
+
+
+#### Editing a Country
+
+
+
+To edit a country, click the "Edit" icon next to the country.
+
+
+
+On this page, you can input the country's name, its [ISO Name](https://www.iso.org/obp/ui/#search), and whether or not a state name is required at the time of checkout for orders either billed to or shipped to an address in this country. Click "Update" to save any changes.
+
+### States
+
+A Spree store pre-loaded with seed data already has all of the states in the US configured for it.
+
+
+
+You can edit, remove, or add states to your store to suit your needs.
+
+#### Editing a State
+
+To edit an existing store, click the "Edit" icon next to its name in the list.
+
+
+
+You can change the name and abbreviation for the state. Click "Update" to save your changes.
+
+
+
+#### Removing a State
+
+To remove a state from your store, click the "Delete" icon next to its name in the list.
+
+
+
+Click "OK" to confirm the deletion.
+
+#### Adding a State
+
+To add a state to your store, first select the country the state belongs to from the "Country" drop-down list.
+
+
+
+Next, click the "New State" button. A data entry form appears. Enter the name and abbreviation for the new state, and click "Create".
+
+
+
+The new state is created, and you can now edit or delete it like the other states.
+
+
+
+***
+Don't forget to add new states and countries to your store's [zones](zones), so the system can accurately calculate tax and shipping options.
+***
\ No newline at end of file
diff --git a/guides/src/content/user/configuration/configuring_inventory.md b/guides/src/content/user/configuration/configuring_inventory.md
new file mode 100644
index 00000000000..1e404b02e29
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_inventory.md
@@ -0,0 +1,79 @@
+---
+title: Inventory Settings
+---
+
+## Introduction
+
+The Spree store gives you a great deal of leverage in managing your business' inventory. You can set up multiple [stock locations](#stock-locations), each of which represents a physical location at which you store your products for delivery to customers. As you add new products and make sales, [stock movements](#stock-movements) are recorded. You can receive stock from a supplier, and even move products from one stock location to another by recording [stock transfers](#stock-transfers). All of this helps to keep your inventorying manageable and current.
+
+### Stock Locations
+
+To reach the Stock Locations management panel, go to your Admin Interface, click "Configuration", then click "Stock Locations". Your store should already have at least one default stock location. If you keep all of your inventory in one place, this may be all you need.
+
+#### Create a New Stock Location
+
+To add a stock location to your store, click the "New Stock Location" button.
+
+
+
+Here, you can input everything of relevance about your stock location: name, address, and phone are the most obvious. The three checkboxes on the right-hand side merit more explanation:
+
+* **Active** - Denotes whether the stock location is currently in operation and serving inventory for orders.
+* **Backorderable Default** - Controls whether inventory items at this location can be backordered when they run out. You can still change this on an item-by-item basis as needed.
+* **Propagate All Variants** - Checking this option when you create a new stock location will loop through all of the products you already have in your store, and create an entry for each one at your new location, with a starting inventory amount of 0.
+
+Input the values for all of the fields, and click "Create" to add your new stock location.
+
+#### Edit a Stock Location
+
+To edit a stock location, click the "Edit" icon next to it in the Stock Locations list.
+
+
+
+Make the desired changes in the form and click "Update".
+
+#### Delete a Stock Location
+
+To remove a stock location, click the "Delete" icon next to it in the Stock Locations list.
+
+
+
+Click "OK" to confirm the deletion.
+
+### Stock Movements
+
+Notice the "Stock Movements" link on the Stock Locations list.
+
+
+
+Clicking this link will show you all of the stock movements that have taken place for this stock location, both positive and negative.
+
+Stock movements are actions that happen automatically through the normal management and functioning of your store. You do not have to (and in fact, can not) manually manipulate them. This is just a way for you to see which things are moving in and out of a particular stock location.
+
+### Stock Transfers
+
+If you have more than one stock location, your Spree store offers you a way to record the movement on inventory from one location to another: the stock transfer.
+
+To create a new stock transfer, go to your Admin Interface, click "Configuration", then "Stock Transfers", then click the "New Stock Transfer" button.
+
+
+
+You can enter an optional Reference Number - this could correlate to a PO number, a transfer request number, a tracking number, or any other identifier you wish to use.
+
+Next, select your Source and Destination stock locations. If you are receiving stock from a supplier, check the "Receive Stock" checkbox and the "Source" drop-down box will be hidden.
+
+Select a product variant from the "Variant" drop-down list and enter the quantity of that product being transferred. Click the "Add" button.
+
+
+
+***
+If you try to transfer an item that you do not have in stock at your Source location, the Spree system will record a stock transfer with a quantity of 0.
+***
+
+The new stock transfer is readied. Once you have added all of the items you want to transfer, click the "Transfer Stock" button.
+
+
+
+Now when you look at the [Stock Movements](#stock-movements) for each of the stock locations, you see that there are two new entries that correspond to the stock transfer, both with a system-assigned "Action" number (actually, the id for the stock transfer).
+
+
\ No newline at end of file
diff --git a/guides/src/content/user/configuration/configuring_mail_methods.md b/guides/src/content/user/configuration/configuring_mail_methods.md
new file mode 100644
index 00000000000..11a144888ff
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_mail_methods.md
@@ -0,0 +1,33 @@
+---
+title: Mail Methods
+---
+
+"Spree mail settings has been extracted out into a gem in favor of generic action mailer settings."
+
+## Introduction
+
+As this has been extracted, please be sure to add [spree_mail_settings](https://github.com/spree-contrib/spree_mail_settings) to your Gemfile before proceeding if you desire this behavior.
+
+The configurable components of your Spree site are managed in the Mail Method Settings panel. You can reach this by going first to the Admin Interface, clicking "Configuration" and then "Mail Method Settings".
+
+
+
+### Enable Mail Delivery
+
+Checking the "Enable Mail Delivery" option will cause all of the confirmation and notification emails the Spree shopping cart system generates to be sent. You may want to disable this option if you want to test other functionality of the store without sending bogus emails.
+
+### Send Mails As
+
+Set this to the email address you want to use as the "From" line on emails that are auto-generated by your store.
+
+### Send Copy of All Mails To
+
+You may want to keep track of the emails your store sends, especially if you are newly launching your e-commerce business. If so, you can configure the system to send a copy of all confirmation and notification emails to the email address you input for this setting.
+
+### Intercept Email Address
+
+Setting this option causes any notification emails to be re-routed to the email address you declare.
+
+### SMTP Settings
+
+The SMTP Mail Method settings allow you to fully configure your Spree store's server to send out email messages via [SMTP - Simple Mail Transfer Protocol](http://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol). A full explanation of how SMTP works is beyond the scope of this user guide, but any changes you or your site's developer need to make can be done through this area of your Admin Interface.
diff --git a/guides/src/content/user/configuration/configuring_reimbursement_types.md b/guides/src/content/user/configuration/configuring_reimbursement_types.md
new file mode 100644
index 00000000000..09db08aa394
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_reimbursement_types.md
@@ -0,0 +1,38 @@
+---
+title: Reimbursement Types
+---
+
+## Introduction
+
+To make it easier for you when you make a reimbursement, there are reimbursement types that helps you define what kind of return you did for the customer(e.g Store Credit, Credit Card, Exchange for a new item).
+
+## Creating Reimbursement Type
+
+You simply open **Configuration** tab and press **Reimbursement Types**.
+Now you can see all previously set up types, with possibility to add new one.
+
+
+
+To add New Reimbursement Type press button placed in the right upper corner **Add New Reimbursement Type**.
+
+
+
+Now you just have to add name and choose type that you would like to have in your store.
+
+
+
+Type is created via backend work, our company is at your service to make it easier for you. This option defines what kind of return you will make for the user e.g (it might be Credit Card return or Exchange for a new item).
+
+
+
+Now just simply check **Active** checkbox and press **Create** button. Brand new Reimbursement type has been created in your shop!
+
+## Editing and Deleting existing Reimbursement Type
+
+Once you have created a type that you would like to edit, you can press **Edit Icon** on the right side of the type's name.
+
+
+
+To delete just press **Delete Icon** next to **Edit Icon**.
+
+
diff --git a/guides/src/content/user/configuration/configuring_return_authorization_reasons.md b/guides/src/content/user/configuration/configuring_return_authorization_reasons.md
new file mode 100644
index 00000000000..3d44368e411
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_return_authorization_reasons.md
@@ -0,0 +1,31 @@
+---
+title: Return Authorization Reasons
+---
+
+## Introduction
+
+Before you make any Return you have to describe reasons, why the user wants to return the items. This is the place where you can create Reasons and name them.
+
+
+
+Press button in right upper corner *New RMA Reason*.
+
+
+
+Input name that will be present during Return process and click *Create* button. It is not necessary to activate Reason instantly. You can choose when you want to make a Return Reason active. For example you might want to create a Holiday refund reason - the user ordered something during holiday but wants to return it when you already deactived this certain reason.
+
+Thats it, you have a reason to make refunds to the Users !
+
+
+
+## Deleting and Editing RMA Reasons
+
+To make additional Edit in RMA Reason just simply click *Edit* button. Now you can change name of a reason or just deactive it.
+
+
+
+
+
+To delete RMA Reason just press *Delete* button next to *Edit*.
+
+
\ No newline at end of file
diff --git a/guides/src/content/user/configuration/configuring_roles.md b/guides/src/content/user/configuration/configuring_roles.md
new file mode 100644
index 00000000000..08232c40da2
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_roles.md
@@ -0,0 +1,29 @@
+---
+title: Roles
+---
+
+## Introduction
+
+Roles are used to define user's permissions on your website. This option gives you ability to control users rights. By default Spree offers two kinds of Roles **Admin** and **User**. If you would like to make more roles with specific features than Admin and User role, our company offers making those by backed-end work. To open **Roles** tab extend **Configuration** dropdown and find **Roles**.
+
+## Creating new Role
+
+To create new role you have to click **New Role** button that is placed in the right upper corner of this page.
+
+
+
+Simply input name for the certain role and press Update button.
+
+
+
+When the role is created you can still edit the name by pressing **Edit** button.
+
+
+
+Or Delete it by pressing **Delete** button.
+
+
+
+## Assigning Role to the User
+
+To assign new Role, you need to open edition for the certain User. To find out more [click here](editing_users).
\ No newline at end of file
diff --git a/guides/src/content/user/configuration/configuring_shipping.md b/guides/src/content/user/configuration/configuring_shipping.md
new file mode 100644
index 00000000000..c8ea14e02aa
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_shipping.md
@@ -0,0 +1,16 @@
+---
+title: Shipping Settings
+---
+
+## Introduction
+
+Once, you set up your shop, and add the products. You have to create Shipping Category and define your Shipping Methods using those categories.
+To make it easier for you, there is already an explanation for both, [Shipping Category](shipping_categories) and [Methods](shipping_methods).
+
+## Corelation between Shipping Categories and Shipping Methods
+
+#### Shipping Categories
+In Shipping Categories you can create a category that defines what kind of shipping the user can choose for a certain product.
+
+
+
diff --git a/guides/src/content/user/configuration/configuring_store_credit_categories.md b/guides/src/content/user/configuration/configuring_store_credit_categories.md
new file mode 100644
index 00000000000..634cf11d26d
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_store_credit_categories.md
@@ -0,0 +1,32 @@
+---
+title: Store Credit Categories
+---
+
+## Introduction
+
+In order to assign Store Credits to the User's account you have to create **Store Credit Categories**, category defines reason for which the User received Store Credits to spent in your's store.
+
+To find **Store Credit Categories** click on **Configuration** dropdown and look for Store Credit Categories option.
+
+
+
+## Creating New Store Credit Category
+
+Now, it is simple way to create new Store Credit Category, find **New Store Credit Category** button in the right upper corner and press it.
+
+
+
+There is only one input box that tells what kind of category you will create. Simply input name and press **Create** button.
+
+
+
+## Editing and Deleting Store Credit Categories
+
+Previously created Category is visible on Store Credit Categories settings. Now, you can create more of them or make additional changes to existing ones.
+Simple as that, in every other section in Spree you can see **Edit** and **Delete** buttons next to the name of category.
+
+
+
+
+
+Now you can assign Store Credit to the User's accounts. You can learn more about it [here](editing_users)
\ No newline at end of file
diff --git a/guides/src/content/user/configuration/configuring_taxes.md b/guides/src/content/user/configuration/configuring_taxes.md
new file mode 100644
index 00000000000..b0a555671d4
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_taxes.md
@@ -0,0 +1,69 @@
+---
+title: Taxes
+---
+
+## Introduction
+
+Taxation, as you undoubtedly already know, is a very complicated topic. It can be challenging to manage taxation settings in an e-commerce store - particularly when you sell a variety of types of goods - but the Spree shopping cart system gives you simple, yet effective tools to manage it with ease.
+
+There are a few concepts you need to understand in order to configure your site adequately:
+
+* [Tax Categories](#tax-categories)
+* [Zones](#zones)
+* [Tax Rates](#tax-rates)
+
+## Tax Categories
+
+Tax Categories is Spree's way of grouping products into those which are taxed in the same way. This is behind-the-scenes functionality; the customer never sees the category a product is in. They only see the amount they will be charged based on their order's delivery address.
+
+To access your store's existing Tax Categories, go to your Admin Interface, click "Configuration" then "Tax Categories".
+
+
+
+You can edit existing Tax Categories by clicking the "Edit" icon next to each in the list.
+
+
+
+You can also remove a Tax Category by clicking the "Delete" icon next to the category, then clicking "OK" to confirm.
+
+
+
+To create a new Tax Category, click the "New Tax Category" button.
+
+
+
+You supply a name, an optional description, and whether or not this is the default tax category for this store.
+
+Each product in your store will need a tax category assigned to it to accurately calculate the tax due on an order. Any product that does not have a tax category assigned will be put in the default tax category. If there is no default tax category set, the item will be treated as non-taxable.
+
+## Zones
+
+In addition to a product's tax category, the zone an order is being shipped to will play a role in determining the tax amount. You can read more about how zones work in the [Zones guide](zones).
+
+## Tax Rates
+
+Tax rates are how it all comes together. A product with a given [Tax Category](#tax-categories), being shipped to a particular [Zone](#zones), will accrue tax charges based on the relevant tax rate that you create.
+
+To add a new Tax Rate, go to your Admin Interface. Click "Configuration" then "Tax Rates".
+
+
+
+Here, you can see all of your existing tax rates and how they are configured. To create a new tax rate, click the "New Tax Rate" button.
+
+
+
+* **Name** - Give your new tax rate a meaningful name (like "Taxable US Goods", for example)
+* **Zone** - You'll need to make separate tax rates for each zone you serve, per category. Suppose you have a "Clothing" tax category, and you sell to both the US and Europe. You'll need to make two different tax rates - one for the US zone, and one for the European zone.
+* **Rate** - The actual percentage amount you are charging. 8% would be expressed as .08 in this field.
+* **Tax Category** - The [tax category](#tax-categories) that relates to this tax rate.
+* **Included in Price** - Check this box if you have already added the cost of tax into the price of the items.
+* **Show Rate in Label** - When this box is checked, order summaries will include the tax rate, not just the tax amount.
+* **Calculator** - By default, Spree uses the Default Tax calculator (a simple tax rate times item price adjusted for any promotions) calculation to determine tax. If you need something more specific or customized than this, it can be done - you'll just need to work with your development team to make it happen.
+
+## Tax Settings
+
+Finally, European stores will benefit from the Tax Settings page.
+
+
+
+When this option is checked, your Spree site will take its default [tax category](#tax_categories), find the corresponding [tax rate](#tax-rate), and multiply it times the shipping rate for each available [shipping method](shipping_methods) offered to a customer during checkout.
\ No newline at end of file
diff --git a/guides/src/content/user/configuration/configuring_taxonomies.md b/guides/src/content/user/configuration/configuring_taxonomies.md
new file mode 100644
index 00000000000..6ba80f6dc34
--- /dev/null
+++ b/guides/src/content/user/configuration/configuring_taxonomies.md
@@ -0,0 +1,109 @@
+---
+title: Taxonomies
+---
+
+## Introduction
+
+Taxonomies are the Spree system's approach to category trees. The heading of a tree is called a _Taxonomy_. Any child branches are called _Taxons_. Taxons themselves can have their own branches. So you could have something like the following categories structure:
+
+
+
+In this example, "Categories" is the name of the Taxonomy. It has three child branches - "Luggage", "Housewares", and "Clothing". The last two branches each have three of their own child branches. Thus, "Clothing" is a child taxon to "Categories" and a parent taxon to "Men's".
+
+To reach the Taxonomies list, first go to your Admin Interface, then click "Configurations" and "Taxonomies".
+
+### Create a New Taxonomy
+
+To create a new taxonomy, click the "New Taxonomy" button. Enter a name for the taxonomy and click "Create".
+
+
+
+You can then [add child taxons](#adding-a-taxon-to-a-taxonomy) to your new taxonomy.
+
+### Edit a Taxonomy
+
+To edit an existing taxonomy, click the "Edit" icon next to the name in the Taxonomies list.
+
+
+
+Here, you can change the name of the Taxonomy. You can also [reorder the child taxons](#reorder-a-taxon), [delete a taxon](#delete-a-taxon), [add a new taxon](#adding-a-taxon-to-a-taxonomy), or [edit a taxon](#edit-a-taxon). Make your changes, then click the Update button.
+
+
+
+### Delete a Taxonomy
+
+
+
+To delete a taxonomy, click the "Delete" icon next to the name in the Taxonomies list. Click "OK" to confirm.
+
+### Adding a Taxon to a Taxonomy
+
+Once you have created a taxonomy, you may want to add child taxons to it. Do do this, right-click the name of the Taxonomy, and click "Add".
+
+
+
+This will cause a new input field to open up, with "New node" in it. Replace this text with the name of your new taxon, and hit Enter. You'll now see the child tax in the taxonomy tree.
+
+
+
+Click "Update" to save your addition.
+
+### Adding a Taxon to Another Taxon
+
+If your site needs sub-trees, just add taxons to other taxons. To do so, right-click the name of what will become the parent taxon, and click Add.
+
+
+
+Enter the name of the child taxon and click enter. Repeat this process for any sub-trees you need.
+
+
+
+Remember to save your changes by clicking the "Update" button after you have added any taxons.
+
+***
+When you navigate away from your taxonomy's page, then navigate back to it, sub-trees will be collapsed by default. To see child taxons, just click the arrow next to the parent taxon.
+***
+
+### Reorder a Taxon
+
+Taxons are displayed in the order you add them by default. To reorder them, just drag and drop them to their correct location in the tree.
+
+Let's assume, for example, that we want the "Children's" taxon to be listed first, above "Women's" and "Men's". Just drag and drop the taxon to its new location.
+
+
+
+You can even drag a parent taxon into the tree of a different parent taxon, merging it into the second taxon's sub-tree.
+
+
+
+### Edit a Taxon
+
+To edit a taxon's name, just right-click it and click "Edit".
+
+
+
+Here, you can edit several aspects of the taxon:
+
+* **Name** - A required field for all taxons. The name determines what the user will see when they look at this product in your store.
+* **Permalink** - The end of the URL a user goes to, in order to see all products associated with this taxon. This field is also required, and a value is automatically generated for you when you create the taxon. Be careful with arbitrarily changing the permalink - if you have two taxons with the same permalink you will run into issues.
+* **Icon** - This option is currently not functional.
+* **Meta Title** - Overrides the store's setting for page title when a user visits the taxon's page on the front end of the website.
+* **Meta Description** - Overrides the store's setting for meta description when a user visits the taxon's page on the front end of the website.
+* **Meta Keywords** - Overrides the store's setting for meta keywords when a user visits the taxon's page on the front end of the website.
+* **Description** - This option is currently not functional.
+
+Remember to click "Update" after you make your changes.
+
+### Delete a Taxon
+
+To delete a taxon, right-click it in the taxonomy tree and click "Remove".
+
+
+
+Click "OK" to confirm.
+
+### Associating Products with Taxons
+
+To associate a product with one or more taxons, go to the Admin Interface, and click the "Products" tab. Locate the product you want to edit, and click its "Edit" icon. Select the taxons for the product in the Taxons field.
+
+
\ No newline at end of file
diff --git a/guides/src/content/user/configuration/index.md b/guides/src/content/user/configuration/index.md
new file mode 100644
index 00000000000..ab4ad6d47c0
--- /dev/null
+++ b/guides/src/content/user/configuration/index.md
@@ -0,0 +1,23 @@
+---
+title: Configuration
+---
+
+## Configuration
+
+The Configuration page of the Admin Interface is that area of your store where you implement decisions about how you want your store to be set up. This is where you decide which shipping methods you offer, which categories you assign to product, how you want currency displayed, and dozens of other settings.
+
+The guides in this section will walk you through making all of those configuration decisions, and show you how to customize your Spree store to best fit your particular needs.
+
+You will learn how to:
+
+* [Set up General Settings](configuring_general_settings)
+* [Set up Taxation](configuring_taxes)
+* [Create Zones, Countries, and States](configuring_geography)
+* [Configure Payment Methods](payment_methods)
+* [Understand how the Taxonomies works](configuring_taxonomies)
+* [Create Shipments and calculate them](configuring_shipping)
+* [Make additional Inventory Settings](configuring_inventory)
+* [Add robust Google Analytics](configuring_analytics)
+* [Configure Reimbursement Types](configuring_reimbursement_types)
+* [Configure Return Authorization Reasons](configuring_return_authorization_reasons)
+* [Set Store Credit categories](configuring_store_credit_categories)
\ No newline at end of file
diff --git a/guides/src/content/user/extensions.md b/guides/src/content/user/extensions.md
new file mode 100644
index 00000000000..f48376940a8
--- /dev/null
+++ b/guides/src/content/user/extensions.md
@@ -0,0 +1,11 @@
+---
+title: Common Add-Ons
+---
+
+## Introduction
+
+Spree is a powerful e-commerce platform in its own right. Right out of the gate, it offers you a lot of functionality that can be customized to fit your store's particular needs.
+
+Some customization scenarios are so common that developers have bundled them into [extensions](/developer/extensions_tutorial) - modules which can be added on to your store to extend its functionality. Consider asking your developer(s) to add one or more to your store. To see a complete list of Spree extensions, browse our [extension library](https://github.com/spree-contrib).
+
+All of these extensions are open source - meaning they, like Spree itself, are offered free of charge. It also means they require support from the community contributors to keep them current and documented.
diff --git a/guides/src/content/user/index.md b/guides/src/content/user/index.md
new file mode 100644
index 00000000000..933c7635869
--- /dev/null
+++ b/guides/src/content/user/index.md
@@ -0,0 +1,12 @@
+---
+title: Spree User Documentation
+---
+
+## Spree User Documentation
+
+Welcome to the Spree User Guides! This documentation is intended for business owners and site administrators of Spree e-commerce sites. Everything you need to know to configure and manage your Spree store can be found here.
+
+Should you find any errors in these guides, or topics you wish to see covered,
+please let us know by [creating an issue](https://github.com/spree/spree/issues/new) on GitHub.
+
+If you are a Spree developer, you may find the [Developer Guides](\developer/index) to be of benefit to you, though we strongly urge you to read through both sets of guides.
diff --git a/guides/src/content/user/orders/editing_orders.md b/guides/src/content/user/orders/editing_orders.md
new file mode 100644
index 00000000000..888382d6b61
--- /dev/null
+++ b/guides/src/content/user/orders/editing_orders.md
@@ -0,0 +1,32 @@
+---
+title: Editing an Order
+---
+
+## Introduction
+
+There will come times when you need to edit orders that are placed in your store. Some examples:
+
+* A customer may call you to adjust the quantity of items they want to purchase.
+* You may sell out of an item and need to remove it from an item altogether.
+* You may need to change the shipping being charged on an order.
+* The customer holds a store credit you need to manually apply to their order.
+
+## Editing an Order
+
+First, go to your Admin Interface. Click the "Orders" tab, and [locate the order](searching_orders) you want to change.
+
+
+
+This will bring up the order edit page:
+
+
+
+You can change any of the following components of an order from here:
+
+* [The types and quantities of products](entering_orders#add-products)
+* [Shipping method](entering_orders#shipments)
+* Tracking details for shipments
+* [Customer information](entering_orders#customer-details)
+* [Adjustments](entering_orders#adjustments)
+* [Payment information](entering_orders#payments)
+* [Return authorizations](returning_orders)
diff --git a/guides/src/content/user/orders/entering_orders.md b/guides/src/content/user/orders/entering_orders.md
new file mode 100644
index 00000000000..64b0acca105
--- /dev/null
+++ b/guides/src/content/user/orders/entering_orders.md
@@ -0,0 +1,122 @@
+---
+title: Manual Order Entry
+---
+
+## Introduction
+
+An order can be created in one of two ways:
+
+1. An order is generated when a customer purchase an item from your store.
+2. An order can be created manually in your store's Admin panel
+
+This guide covers how to create a manual order in the Admin Panel.
+
+## Add Products
+
+To create new order manually, go into the Admin Interface, click the "Orders" tab, and click the "New Order" button.
+
+
+
+Type the name of the product you would like to add to the order in the search field. A list of matching products and variants combinations will show up in the drop-down menu. Select the product/variant option you want to add to the order.
+
+
+
+The interface will show you you how many of that product/variant you currently have "on hand". Enter the quantity to add to the new order, and click the "Add" icon next to the item.
+
+
+
+The system creates the order and shows you the line items in it.
+
+
+
+Follow the same steps to add more products to the order.
+
+## Customer Details
+
+Click the "Customer" link. You can either select a name from the "Customer Search" field if the customer has ordered from you before, or you can enter the customer's email address in the "Email" field of the "Account" section. The setting for "Guest Checkout" will automatically change accordingly.
+
+Enter the customer's billing address and the shipping address for the order. You can click "USE BILLING ADDRESS" checkbox to use the same address for both. If you do so, the shipping address fields will become invisible.
+
+
+
+Click the "Update" button.
+
+## Shipments
+
+After you input the customer information, you might want to choose a shipping method. When you pressed the "Update" button, the page will reload on "Shipments" tab.
+
+The default shipping method for your store (if you have one) should already be assigned to this order. Depending on the items you added and the location you're shipping to, there could be additional methods available. You may also have shipping methods that are only available for your site's administrator to assign (in-store pickup, for example).
+
+Click the "Edit" link next to the order's shipping line.
+
+
+
+Click the "Shipping Method" drop-down menu, and make your selection.
+
+
+
+Click the "Save" icon to confirm your change. Your Spree site will re-calculate the shipping, any relevant adjustments, and total for your order.
+
+## Adjustments
+
+The Spree shopping cart will automatically add the cost of the Shipping Method to your order as an adjustment - you can change or remove this.
+
+### Editing Adjustments
+
+To edit an existing order adjustment, just click the "Adjustments" link in the order summary, then click the "Edit" icon next to the adjustment in the Adjustments list.
+
+
+
+### Deleting Adjustments
+
+To remove an adjustment, click the "Delete" icon next to it in the Adjustments list.
+
+
+
+Confirm the deletion by clicking "OK".
+
+### Opening and Closing Adjustments
+
+Some types of adjustments - tax and shipping, for example - may re-calculate as the order changes, new products are added to it, etc. If you want to be sure that the amount of an adjustments will remain the same, you can lock them. This is also known as closing the adjustments.
+
+***
+Closed adjustments can be re-opened and changed, up to the moment when the order is shipped. At that point, the adjustment is finalized and cannot be changed.
+***
+
+To open or close all of the adjustments in an order, just click the "Open All Adjustments" or "Close All Adjustments" buttons on the Adjustments list.
+
+
+
+### Adding Adjustments
+
+You can also add further adjustments - positive or negative - to the order to account for things like handling charges, store credits, etc. To add a new adjustment, click the "New Adjustment" button.
+
+
+
+You need only enter the "Amount" (positive for a charge on the order; negative for a credit) and a "Description", then click the "Continue" button.
+
+
+
+For a better understanding of adjustments, please read the [Developer Adjustments Guide](/developer/adjustments).
+
+Once you have finished all of the changes you want in the order's Adjustments, click "Continue".
+
+## Payments
+
+If you are manually entering this order, it is presumed that you have received payment either in person, on the phone, or through some other non-website means. You can manually enter payment using any of your site's configured [payment methods](payment_methods).
+
+Just click the "Payments" link in the right panel section.
+
+
+
+This form is pretty self-explanatory; you enter the amount of the payment, the method, and the credit card information (if applicable).
+
+One thing to note is that you can enter multiple payments on the same order. This is useful if, for example, a customer wants to pay part of their order in cash and put the rest on a credit card. In that case, all you have to do is create the first payment for the Cash amount, check the "Cash" payment method (be sure you have it configured in your store first!), and click Update.
+
+Then, click the "New Payment" link to enter the information for the credit card portion of the payment.
+
+
+
+Don't forget that you will need to [capture the payment](payment_states#authorize-vs-capture) on the credit card (unless your store is set up to automatically authorize and capture a paymentÅ›).
+
+For more on payments, be sure to read both the [Payment Methods](payment_methods) and [Payment States](payment_states) guides.
diff --git a/guides/src/content/user/orders/index.md b/guides/src/content/user/orders/index.md
new file mode 100644
index 00000000000..2611bcb52fd
--- /dev/null
+++ b/guides/src/content/user/orders/index.md
@@ -0,0 +1,16 @@
+---
+title: Orders
+---
+
+## Orders
+
+Much of your time administering your Spree store will be spent manipulating customer orders - processing payments, issuing refunds, confirming shipments, etc.
+
+In these guides, you will learn how to:
+
+* [Process orders](processing_orders) (capture payments, record shipments)
+* [Manually enter an order](entering_orders)
+* [Edit an order](editing_orders)
+* [Understand the states an order goes through](order_states)
+* [Process the return of an order](returning_orders)
+* [Search through your orders](searching_orders)
\ No newline at end of file
diff --git a/guides/src/content/user/orders/order_states.md b/guides/src/content/user/orders/order_states.md
new file mode 100644
index 00000000000..9991259c612
--- /dev/null
+++ b/guides/src/content/user/orders/order_states.md
@@ -0,0 +1,22 @@
+---
+title: Order States
+---
+
+## Introduction
+
+A new order is initiated when a customer places a product in their shopping cart. The order then passes through several states before it is considered `complete`. The order states are listed below. An order cannot continue to the next state until the previous state has been successfully satisfied. For example, an order cannot proceed to the `delivery` state until the customer has provided their billing and shipping address for the order during the `address` state.
+
+## Order States
+
+The states that an order passes through are as follows:
+
+* `cart` - One or more products have been added to the shopping cart.
+* `address` - The store is ready to receive the billing and shipping address information for the order.
+* `delivery` - The store is ready to receive the shipping method for the order.
+* `payment` - The store is ready to receive the payment information for the order.
+* `confirm` - The order is ready for a final review by the customer before being processed.
+* `complete` - The order has successfully completed all of the previous states and is now being processed.
+
+***
+The states described above are the default settings for a Spree store. You can customize the order states to suit your needs utilizing our API. This includes adding, removing, or changing the order of certain states. Customization details are provided in the [Checkout Flow API Guide](/developer/checkout.html#checkout-customization).
+***
diff --git a/guides/src/content/user/orders/processing_orders.md b/guides/src/content/user/orders/processing_orders.md
new file mode 100644
index 00000000000..e9442ba9b2e
--- /dev/null
+++ b/guides/src/content/user/orders/processing_orders.md
@@ -0,0 +1,50 @@
+---
+title: Processing Orders
+---
+
+## Introduction
+
+Once an order comes into your store - whether it is entered by a customer through the website frontend or you [manually enter it yourself](entering_orders) through the admin backend - it needs to be processed. That means these steps need to be taken:
+
+1. Verify that the products are, in fact, in stock and shippable.
+2. Process the payment(s) on the order.
+3. Package and ship the order.
+4. Record the shipment information on the order.
+
+Steps 1 and 3 you would obviously do physically at your stocking location(s). This guide covers how you use your Spree store to manage steps 2 and 4.
+
+## Processing Payments
+
+You have received an order in your store - hooray! Either the items are all in stock, or you have adjusted the order to include only those items you can sell to the customer.
+
+Now you need to process the payment on the order. You know best how your store processes things like checks, money orders, and cash, so this guide will focus on processing credit card payments.
+
+
+
+Pictured above is an order that is ripe for processing. The current order State is "Complete", meaning that all of the information the customer needs to provide is present. The Payment State is "Balance Due", meaning that there is an unpaid balance on the order. The Shipment State is "Pending", because you can't ship an order before you collect payment on it.
+
+If you click either the Order Number or the "Edit" icon, you will open the order in Edit mode. Instead, click the "Balance Due" payment state to open the order's payment info page.
+
+
+
+As you can see above, we have only a single payment to process, for the full amount ($22.59). Processing this payment is literally a click away - just click on the "Capture" icon next to the payment.
+
+If the payment is processed successfully, the page will re-load, showing you that the payment state has progressed from "Pending" to "Completed".
+
+
+
+That's it! Now you can prepare your packages for shipment, then update the order with the shipment information.
+
+## Processing Shipments
+
+Now, when you visit the Order Summary, you can see that there is a new "Ship" button in the middle of the page.
+
+
+
+Clicking this button moves the Shipment State from "Ready" to "Shipped".
+
+
+
+The only thing that remains to do now is to click the "Edit" icon next to the tracking details line, and provide tracking info for the package(s). Click the "Save" icon to record this information.
+
+
diff --git a/guides/src/content/user/orders/returning_orders.md b/guides/src/content/user/orders/returning_orders.md
new file mode 100644
index 00000000000..e23c27f2588
--- /dev/null
+++ b/guides/src/content/user/orders/returning_orders.md
@@ -0,0 +1,55 @@
+---
+title: Returns
+---
+
+## Introduction
+
+Returns are a reality of doing business for most e-commerce sites. A customer may find that the item they ordered doesn't fit, or that it doesn't fit their needs. The product may be damaged in shipping. There are many reasons why a customer could choose to return an item they purchased in your store. This guide covers how you, as the site administrator, issue RMAs (Return Merchandise Authorizations) and process returns.
+
+## Creating RMAs for Returns
+
+You can only create RMAs for orders that have already been shipped. That makes sense, as you wouldn't authorize a return for something you haven't sent out yet.
+
+
+
+To create an RMA for a shipped order, click the order's "Return Authorizations" link, then click the "New Return Authorization" button. The form that opens up enables you to select which items will be authorized to be returned, and issue an RMA for the corresponding amount.
+
+
+
+To use it, just select each line item to be returned, either a reimbursement type or exchange item. Select Quantity of the items(s), its set to "1" by default. For example, customer wants to return a damaged item. Selecting the "Original" reimbursement type will refund a user back to their original payment method when the items are returned and approved. Selecting an exchange item will create a new shipment to ship the exchange item to the customer. The form will automatically calculate the RMA value based on the sale price of the item(s), but you will have to confirm the amount when the reimbursement is issued. This gives you a chance to adjust for handling fees, restocking fees, damages, etc.
+
+Input the reason and any memo notes for the return, and select the [Stock Location](configuring_inventory) the item is coming back to. Click the "Create" button.
+
+Now, you just need to wait for the package to be received at your location.
+
+Even created Return Authorization can be edited or deleted. When the user changes his mind. As an Admin in your shop, you can still make additional changes to Return action.
+
+
+
+
+
+Inside of the Return Authorization Edit, its the same layout as creating one. Just simply make changes and press **Update** button.
+
+
+
+## Processing Returns
+
+Once you receive a return package, you need to create a "Customer Return". To do so, go to the order in question and click "Customer Returns". Click the "New Customer Return" button.
+
+
+
+Select which of the authorized return items were received or mark all of them by simply clicking next to product on the left side checkbox, and to which [Stock Location](creating_products). You can also set if the item that has been returned by the User is still **Resellable** or not. Once you are done, click the "Create" button.
+
+
+
+The return items are marked as accepted, and now you can create a reimbursement for the $24.14 you owe the customer.
+
+
+
+The reimbursement form will be populated according to your original reimbursement or exchange selections chosen during the return authorization form. You may override the selected reimbursement type or exchange item now if you want to, otherwise click the "Reimburse" button to create the refund.
+
+
+
+Your return-processing is complete! As you can see, there is $24.14 refund issued to the original credit card or Store credits to the user's account.
+
+
diff --git a/guides/src/content/user/orders/searching_orders.md b/guides/src/content/user/orders/searching_orders.md
new file mode 100644
index 00000000000..1d57e233938
--- /dev/null
+++ b/guides/src/content/user/orders/searching_orders.md
@@ -0,0 +1,72 @@
+---
+title: Searching Orders
+section: searching_orders
+---
+
+When you click the **Orders** tab on the Admin Interface, you are instantly presented with a summary of the most recent orders your store has received.
+
+
+
+The list shows you the following information about each order:
+
+* **Completed At** - The date on which the user finalized their order.
+* **Number** - The Spree-generated order number.
+* **State** - The current state of the order. You can learn more about [order states in another guide](order_states).
+* **Payment State** - Spree tracks the state of an order's payment separately from the state of the order itself. As payment is received, the state of the order progresses.
+* **Shipment State** - Having the Shipment State pictured separately lets you quickly see which orders are paid and need to be packed and shipped, improving your store's workflow.
+* **Customer Email**
+* **Total** - This amount includes item totals, tax, shipping, and any promotions or adjustments made to the order.
+
+Next to each row is an "Edit" icon. Clicking this icon allows you to [make changes to an order](editing).
+
+# Filtering Results
+
+You may not always want to see all of the most recent orders - the Spree default. You may want to view only those orders that you need to pack and ship, or only those from a particular customer. Spree gives you the flexibility to quickly find only those orders you need.
+
+
+
+You can choose one or more of the following options to narrow your order search, then click the **Filter Results** button to update the results.
+
+## Date Range
+
+You can input a **Start** and/or **Stop** date. If you enter both, the results shown will be all orders that fall on or between those dates.
+
+If you input only a **Start** date, you will get all orders placed on or after that date.
+
+If you input only a **Stop** date, the results will include all orders placed up to and on that date.
+
+## Status
+
+You can restrict orders to only those with a particular status. Available status options include:
+
+* **cart** - Customer has added items to a shopping cart, but has not yet checked out.
+* **address** - Customer has entered the checkout process, but has not yet completed input of shipping and/or billing address(es).
+* **delivery** - Customer has completed entry of addresses, but has not yet completed selection of delivery method(s).
+* **payment** - Customer has entered addresses and chosen a delivery method, but still needs to enter a payment method.
+* **confirm** - All required information has been entered; customer just needs to confirm the order.
+* **complete** - All required information is present, customer has confirmed the order, payment has not yet been received or processed.
+* **canceled** - Either customer or store admin has chosen to cancel the order.
+* **awaiting return** - Customer has elected to return products, but they have not yet been received.
+* **return** - A return has been processed.
+* **resumed** - A formerly canceled order has been reactivated.
+
+## Order Number
+
+Spree generates a unique order number for each order when the first item is added to a shopping cart. Order numbers begin with the letter R, followed by 9 random numbers. If you are searching for a particular order, you can just input the entire order number and that order is all that will be returned.
+
+## Email
+
+At this time, the filter does not allow you to search for only part of an email address. If you want to find all orders from `jane_doe@example.com`, you will have to use the full address. Inputting only "jane_doe" will result in a pop-up alert to enter a valid email address.
+
+## Name
+
+The **First Name Begins With** and **Last Name Begins With** fields will let you filter order results based on the *billing address*, not on the shipping address. You can use any number of letters, from just an initial to the full first and/or last name.
+
+## Complete
+
+By default, the filter restricts results to only orders that have reached the `complete` order state. To remove this restriction, uncheck the box that is marked **Only Show Complete Orders**.
+
+## Unfulfilled
+
+If you only want to review orders that have not been shipped, you can check the box marked **Show Only Unfulfilled Orders**.
+
diff --git a/guides/src/content/user/payments/index.md b/guides/src/content/user/payments/index.md
new file mode 100644
index 00000000000..85b9e2d41b5
--- /dev/null
+++ b/guides/src/content/user/payments/index.md
@@ -0,0 +1,9 @@
+---
+title: Payments
+---
+
+## Payments
+
+Processing payments for orders is a very important component of your Spree store. The flexibility of this system allows you to add, remove, or change [methods of payment](payment_methods) to suit your needs, as well as to use almost any [payment gateway](payment_methods#add-a-supported-gateway) you prefer.
+
+The guides in this section will also help you to understand the [states an order goes through](payment_states) from the time the first item is added to the cart, to the time you close out the fulfillment process.
\ No newline at end of file
diff --git a/guides/src/content/user/payments/payment_methods.md b/guides/src/content/user/payments/payment_methods.md
new file mode 100644
index 00000000000..479a8ca7aeb
--- /dev/null
+++ b/guides/src/content/user/payments/payment_methods.md
@@ -0,0 +1,89 @@
+---
+title: Payment Methods
+---
+
+## Introduction
+
+Payment methods represent the different payment options available to customers during the checkout process on an e-commerce store. Spree supports many types of payment methods, including both online and offline options. This guide describes how to add payment methods to your Spree store.
+
+## Terminology
+
+Let's begin by explaining the difference between a Payment Gateway and a Merchant Account.
+
+**Payment Gateway** - A payment gateway is a service that authorizes credit card payments, processes them securely, and deposits the funds into your bank account. A payment gateway performs the same functions as a credit card swipe machine at a restaurant or retail store, it just performs these functions for purchases made online instead of in person.
+
+**Merchant Account** - A merchant account is a type of bank account that allows you to accept credit card payments online. If you have a retail business and already accept credit card payments, then more than likely you have a merchant account. When you start to sell products online, you may need to call your bank and ask that they set you up with an _Internet_ merchant account. An Internet merchant account allows you to accept payments online without having the customer's credit card physically in front of you.
+
+## Evaluating Payment Gateways
+
+When researching payment gateway options, you may find that they offer an all-in-one solution that includes both the gateway and the merchant account. This is just something to be aware of and to evaluate if it makes sense for your store. Payment gateways also charge a fee for their services. Here are a few of the fees you might come across when evaluating providers:
+
+**Setup Fee** - A one-time charge to set up your payment gateway account.
+
+**Recurring Fixed Monthly Fees** - A fixed monthly fee that a payment gateway provider charges for access to their services and reports. Some gateways break this charge down further into a monthly Gateway Fee and a Statement Fee.
+
+**Transaction Fees** - A charge for each purchase made on your e-commerce store. The pricing structure for these fees differ per gateway. A popular structure is to charge a percentage of the purchase price plus a flat fee. For example, 2.9% of the purchase price plus $0.30 per transaction.
+
+## Add a Payment Method
+
+Spree enables you to utilize the payment method of choice for your e-commerce store. We have two [preferred payment gateway partners](http://spreecommerce.com/products/payment_processing) and a long [list](https://github.com/Shopify/active_merchant#supported-direct-payment-gateways) of payment gateways that are supported by default in Spree. We also enable you to add a custom payment gateway, as well as offer offline payment options such as checks and purchase orders.
+
+### Add a Supported Gateway
+
+Read through the following explanatory text to add one of the supported payment gateways as a payment method on your store.
+
+#### Select Provider
+
+To configure one of the supported payment gateways, you must first install the [Spree_Gateway](https://github.com/spree/spree_gateway) extension on your store. More than likely, you will want to ask someone from your technical team to do this. Once this extension has been installed, you can configure one of the supported gateways in the Admin Interface by clicking the "Configuration" tab and then clicking the "New Payment Method" button.
+
+
+
+If you installed the [Spree_Gateway](https://github.com/spree/spree_gateway) extension, you will see a long list of gateways in the "Provider" drop down menu. Select the one that you would like to add.
+
+
+
+#### Display
+
+Select whether you want the payment method to appear on the Frontend or the Backend of your store, or both.
+
+The Frontend is the customer-facing area of your store, meaning that the payment method will display as a payment option to your customers during the checkout step.
+
+The Backend is the Admin Interface for your store. Site administrators typically select this option when they want to make a payment option available to their internal staff but not to their end customers. For example, you might want to offer purchase orders as a payment option to customers on a one-off basis, but only if they contact one of your customer service representatives via email or telephone.
+
+#### Auto Capture
+
+There is a possibility to set Auto Capture feature for your payment methods. Once you set Auto Capture on **Yes** option for a particular payment method, all the payments will be captured automatically. At this point all the payments will be captured without Admin's direct interference. However, **Use App Default (false)** is default setting for newly created Payment Method.
+
+
+
+#### Active
+
+Select "Yes" if you want the payment method to be active on your store. Select "No" if you want to create the payment method, but not present it on your store until a later point.
+
+#### Name
+
+Give the payment method a name. The value you enter will appear on the customer-facing area of your store, on the Payment page as seen below:
+
+
+
+#### Description
+
+Add a description for the payment method. This field is optional and is only displayed to internal users and not to customers.
+
+Click "Update" once you've input the desired settings for your new payment method.
+
+### Add a Non-Supported Gateway
+
+It is possible to add a new payment gateway that is not included on the supported by default gateway [list](https://github.com/Shopify/active_merchant#supported-direct-payment-gateways), but doing so is outside the scope of this tutorial. Please consult with your development team if you need this functionality.
+
+## Edit a Payment Method
+
+To edit the configuration settings for an existing payment method, go to the Admin Interface, click the "Configuration" tab, and then click the "Payment Methods" link. Find the payment method that you would like to edit on the list that appears. Click the "Edit" icon next to the payment method to edit its settings.
+
+
+
+Make the desired changes to the payment method settings and then click "Update" to save them.
+
+## Processing Payments
+
+Processing orders and the payments associated with them are covered in detail in the [Processing Orders guide](processing_orders).
diff --git a/guides/src/content/user/payments/payment_states.md b/guides/src/content/user/payments/payment_states.md
new file mode 100644
index 00000000000..2340bcc2c6e
--- /dev/null
+++ b/guides/src/content/user/payments/payment_states.md
@@ -0,0 +1,56 @@
+---
+title: Payment States
+---
+
+## Introduction
+
+When an order is initiated for a customer purchase a payment is created in the Spree system. A payment goes through various states while being processed.
+
+## Payment States
+
+The possible payment states are:
+
+* **Checkout** - The checkout has not been completed.
+* **Processing** - The payment is being processed.
+* **Pending** - The payment has been processed but is not yet complete (ex. authorized but not captured).
+* **Failed** - The payment was rejected (ex. credit card was declined).
+* **Void** - The payment should not be applied against the order.
+* **Completed** - The payment is completed. Only payments in this state count against the order total.
+
+A payment does not necessarily go through each of these states in sequential order as illustrated below:
+
+
+
+You can determine the payment state for a particular order by going to the Admin Interface and clicking on the "Orders" tab. Find the order you want to look up and click on it. Then click on the "Payments" link.
+
+
+
+The details for the payment will appear. The "Payment State" column will display one of the possible payment states listed above.
+
+
+
+## Authorize vs Capture
+
+Authorizing a payment is the process of confirming the availability of funds for a transaction with the purchaser's credit card company. Capturing a payment is the process of telling the credit card company that you would like to get paid for the transaction amount. Typically, this two step process of first authorizing the payment and then capturing the payment is used by online retailers to delay charging the customer until the product(s) purchased are fulfilled (shipped).
+
+By default, Spree automatically handles authorizing the payment for a transaction. For capturing payments, we give you the choice of auto-capturing the payment or manually capturing the payment via the Admin Interface. If you like, you can read further [documentation about auto-capturing payments](/developer/payments#auto-capturing).
+
+Note: Not all payment gateways allow for the two step *authorize and then capture* payment process. If this functionality is required for your store, please confirm with your payment gateway that they can support this process.
+
+# Capture a Payment via the Admin
+
+To capture a payment using the Admin Interface, click on the "Orders" tab. Find the order you want to look up and click on it. Then click on the "Payments" link. The order details will appear. Click on the "Capture" icon to initiate the capture process.
+
+
+
+## Void a Payment
+
+To void a payment, go to the Admin Interface. click on the "Orders" tab. Find the order you want to look up and click on it. Then click on the "Payments" link. The order details will appear. Click on the "Void" icon to void the transaction.
+
+
+
+## Payment's amount edit
+
+Additionally, before accepting or voiding the payment you can edit the amount by clicking "Edit" button.
+
+
diff --git a/guides/src/content/user/products/cloning_products.md b/guides/src/content/user/products/cloning_products.md
new file mode 100644
index 00000000000..1356f78682c
--- /dev/null
+++ b/guides/src/content/user/products/cloning_products.md
@@ -0,0 +1,26 @@
+---
+title: Cloning Products
+section: cloning_products
+---
+
+## Introduction
+
+To clone a product in your store, go into the Admin Interface and click the "Products" tab. A list of your store's product inventory will appear. Find the product that you would like to clone and click the "Clone" icon to make a copy of it.
+
+
+
+This will create a copy of the product and it will now appear in your product inventory list as "COPY OF" with the product name appended afterward. A new permalink and SKU will also be created for the product.
+
+
+
+## Editing
+
+You may want to change the name of the product to remove the "COPY OF" text and to rename the permalink and SKU so it matches your product name.
+
+!!!
+Be very careful in assigning arbitrary permalinks, so that you don't accidentally assign two products the same permalink.
+!!!
+
+You can edit the information associated with a cloned product just like you would for any other product. To do this, click on the product from your inventory list. In the "Name" field, delete the "COPY OF" text. To modify the permalink name, enter your desired value in the "Permalink" field. To modify the product SKU, enter your desired value in the "SKU" field. Once you've made your changes, scroll to the bottom of the page and click "Update".
+
+
diff --git a/guides/src/content/user/products/creating_products.md b/guides/src/content/user/products/creating_products.md
new file mode 100644
index 00000000000..882cc6c0034
--- /dev/null
+++ b/guides/src/content/user/products/creating_products.md
@@ -0,0 +1,134 @@
+---
+title: Creating a New Product
+section: creating_products
+---
+
+## Introduction
+
+To create a new product for your store, go into the Admin Interface, click the "Products" tab, and click the "New Product" button.
+
+
+
+The three mandatory fields ("Name", "Master Price", and "Shipping Categories") are denoted with an asterisk (*) next to the label. You can leave SKU blank. If you don't add a value for "Available On" the product will not be shown in your store.
+
+***
+[Prototypes](product_prototypes) are a more complex topic, and are covered in their own guide.
+***
+
+## Product Details
+
+After you click the "Create" button, the Spree application brings you to a more detailed product entry page, where you can input more information about your new product.
+
+
+
+* **Name** - This field will either be blank, or the same as what you entered on the initial page. You can change this field whenever you like.
+* **Permalink** - The permalink is automatically created by the application for you when the product is first saved, and is based on the product's name. This is what is appended to the end of a URL when someone visits the page for a particular product. You can change the permalink, but should exercise extreme caution in doing so to avoid naming collisions with other products in your database.
+* **Description** - This is where you will provide a detailed description of the product and its features. The application gives you plenty of room to be thorough.
+* **Master Price** - For now, just think about the Master Price as the price you charge someone to buy the item. Later in this guide, you will learn more about variants and how they impact a product's actual price.
+* **Cost Price** - What the item costs you, the seller, to purchase or produce.
+* **Cost Currency** - It may be that the currency used when you purchased the product is not the same as that you use in your store. Spree makes these conversions for you - just enter the code for the currency used in acquiring your inventory.
+* **Available On** - This field will either be blank, or the same as what you entered on the initial page. You can change this field whenever you like.
+* **SKU** - This field will either be blank, or the same as what you entered on the initial page. You can change this field whenever you like.
+* **Weight** - The product's weight in ounces. May be used to calculate shipping cost.
+* **Height** - The product's height in inches. May be used to calculate shipping cost.
+* **Width** - The product's width in inches. May be used to calculate shipping cost.
+* **Depth** - The product's depth or breadth in inches. May be used to calculate shipping cost.
+* **Shipping Categories** - You will learn about setting up Shipping Categories in the [Shipping Categories](shipping_categories).
+* **Tax Category** - You will learn about setting up Tax Categories in the [Taxes Guide](configuring_taxes).
+* **Taxons** - Taxons are basically like categories. You will learn more about them in the [Taxonomies Guide](configuring_taxonomies).
+* **Option Types** - You can select any number of Options to associate your new product with. You'll learn more about Options in the [Options Guide](product_options).
+* **Meta Keywords** - These words are appended to the website's keywords you established in the [Site Settings](configuring_general_settings) and can help improve your site's search engine ratings, bringing you more web traffic. They should be words that are key to your new product.
+* **Meta Description** - The summary that someone sees when your product's page is returned in a web search. It should be descriptive but not overly verbose.
+
+## Images
+
+A store whose products had no images to look at would be pretty boring, and probably not garner a lot of sales. It would be very time-consuming to have to upload, crop, resize, and associate several photos to each product, if you had to do so manually. Luckily, Spree makes maintaining images of your products quick and painless.
+
+Just click the "Images" link under "Product Details" on the right-hand side of the screen. Any images that you may have already uploaded will be previewed for you. To add a new image for your product, click the "New Image" button.
+
+
+
+Select the Image file, and enter the Alternative Text for the image. Alternative Text is what appears when someone has their browser's image-rendering turned off, as with certain types of screen readers.
+
+You have the option to associate a photo only with a particular "Variant" (again, more on Variants later in this guide), or with all of the product's Variants.
+
+When you click Update, not only is the product photo uploaded, it is automatically resized and cropped to fit your store's requirements, and it is associated with the correct versions of your product.
+
+## Understanding Variants
+
+Suppose that in your store, you sell drink tumblers. All of the tumblers are made by the same manufacturer and have the same basic product specifications (materials used, machine washability rating, etc.), but your inventory includes several options:
+
+* **Size** - You carry both Medium and Large tumblers
+* **Decorative Wrap** - Your tumblers come with the option of several kinds of decorative plastic wraps: Stars, Owls, Pink Paisley, Purple Paisley, or Skulls.
+* **Lid Color** - The tumblers also come with with an assortment of lids to match the decorative wrap - the Star tumblers have Blue lids, the Owls have Orange lids, the Pink Paisley have Pink lids, the Purple Paisley have White lids, and the Skulls can be purchased with White *or* Black lids.
+
+Given this inventory, you will need to create a Drink Tumbler _Product_, with three _Option Types_, the corresponding _Option Values_, and twelve _Variants_:
+
+Size | Wrap | Lid Color
+|-----|------|---------|
+Large | Stars | Blue
+Small | Stars | Blue
+Large | Owls | Orange
+Small | Owls | Orange
+Large | Pink Paisley | Pink
+Small | Pink Paisley | Pink
+Large | Purple Paisley | White
+Small | Purple Paisley | White
+Large | Skulls | White
+Large | Skulls | Black
+Small | Skulls | White
+Small | Skulls | Black
+
+The _Option Types_ you would create for this inventory are - Size, Wrap, and Lid Color - with the corresponding _Option Values_ below.
+
+Option Type | Option Values
+|-----------|--------------|
+Size | Large, Small
+Wrap | Stars, Owls, Pink Paisley, Purple Paisley, Skulls
+Lid Color | Blue, Orange, Pink, White, Black
+
+Read the [Product Options Guide](product_options) for directions on creating Option Types and Option Values. You must establish your Option Types and Option Values before you can set up your Variants. Don't forget to associate the Option Types with the Tumbler product so they'll be available to you when you make your Variants.
+
+### Creating Variants
+
+Now that you have set up the appropriate options for your Product's Variants and associated those options with the product, you can create the Variants themselves.
+
+Let's create the large, star-wrapped, blue-lidded tumbler Variant as an example. You can then use the same approach to creating all of the other Variants we mentioned earlier.
+
+On your tumbler product edit page, click the "Variants" link. Click the "New Variant" button.
+
+
+
+Select the appropriate values for the Option Types. As you can see, you also have the choice to enter values for this particular Variant that may be different from what you input on the Product's main page. Let's raise the price on our Variant to $20. Click the "Create" button.
+
+
+
+## Product Properties
+
+You can set as many individual product properties as you like. These include things like the item's country of manufacture, material(s) used, design style, etc. Typically, these are characteristics that do not change across variants of a product.
+
+You can read much more in-depth information about this feature in the [Product Properties Guide](product_properties).
+
+## Stock Management
+
+As of version 2.0 of Spree, you now have much more granular control over how inventory is tracked through your store. You will learn more about stock locations in the [Stock Locations Guide](configuring_inventory), but for now it's enough to understand that you enter the number of each product variant that you have at each of your individual stock locations.
+
+Let's assume that you have two stock locations - your main New York warehouse and your satellite Detroit warehouse. Refer to the instructions on creating stock locations in the [Stock Locations Guide](configuring_inventory#create-a-new-stock-location) to add your warehouses.
+
+Now, go back to the Tumblers product page, and click the "Stock Management" link.
+
+
+
+For this guide, let's say we want to say that we have 7 of our tumbler variant in the New York warehouse, and 3 in Detroit. To accomplish this, change the quantity to 7, select "New York Warehouse" from the "Stock Location" drop-down list, and select "large-blue-stars" from the "Variant" drop-down list. Click the "Add Stock button".
+
+The "Stock Location Info" table will update, showing you that there are 7 of these items in the New York warehouse. Repeat these steps, adding 3 tumblers from the Detroit warehouse.
+
+
+
+Your Stock Location Info table should now look like the one pictured above.
+
+***
+"Backorderable" may or may not be checked for your individual Stock Locations, depending on how you configured them. Each Stock Location has defaults for this value, but you can change it on a variant-by-variant basis in this dialog.
+***
+
+You should be sure to read the [Stock Locations](configuring_inventory.html#stock-locations) and [Stock Movements](configuring_inventory.html#stock-movements) guides for further information on managing your store's inventory.
diff --git a/guides/src/content/user/products/deleting_products.md b/guides/src/content/user/products/deleting_products.md
new file mode 100644
index 00000000000..95005e8e89b
--- /dev/null
+++ b/guides/src/content/user/products/deleting_products.md
@@ -0,0 +1,26 @@
+---
+title: Deleting Products
+section: deleting_products
+---
+
+## Introduction
+
+To delete a product in your store, go into the Admin Interface and click the "Products" tab. A list of your store's product inventory will appear. Find the product that you would like to delete and click the "Delete" icon on the right to remove it from your store.
+
+
+
+A message will appear asking you to confirm that you want to delete the product. Click "OK".
+
+## Viewing
+
+Deleted products no longer appear in the customer-facing area of your store. However, they do remain in your product database. To view products that have been deleted from your store, go to the "Products" tab in the Admin Interface and find the "Search" section. Do not enter anything in the "Name" or "SKU" fields. Check the "Show Deleted" box and click "Search". The search results will return a list of your entire product inventory including active and deleted products.
+
+
+
+## Re-activating
+
+To re-activate a deleted product, find the product in your inventory following the steps [above](#viewing). Deleted products will only have the "Clone" icon next to them, whereas active products will have the "Edit", "Clone", and "Delete" icons next to them. Click the "Clone" icon to the right of the deleted product.
+
+
+
+This will create a copy of the product, and it will now appear in your product inventory list as "COPY OF" with the product name appended afterward. A new permalink and SKU will also be created for the product. Follow the instructions from the [Cloning guide](cloning_products) to modify the information for this product.
diff --git a/guides/src/content/user/products/editing_products.md b/guides/src/content/user/products/editing_products.md
new file mode 100644
index 00000000000..08a3d70421e
--- /dev/null
+++ b/guides/src/content/user/products/editing_products.md
@@ -0,0 +1,15 @@
+---
+title: Editing Products
+---
+
+## Introduction
+
+You will often need to modify the products in your store - prices may fluctuate, descriptions may change, you may take new photographs of your inventory. Spree makes it quick and easy to make such changes.
+
+## Editing Products
+
+To make changes to your product, go to your Admin Interface, click the "Products" tab, and click the "Edit" icon next to the product.
+
+
+
+The edit form is the exact same one you use when [creating products](creating_products). See that guide for explanation of any of the fields when editing your product.
diff --git a/guides/src/content/user/products/index.md b/guides/src/content/user/products/index.md
new file mode 100644
index 00000000000..d076260db51
--- /dev/null
+++ b/guides/src/content/user/products/index.md
@@ -0,0 +1,21 @@
+---
+title: Products
+---
+
+## Products
+
+Products are at the core of any e-commerce site. Selling them is the whole reason behind opening a store in the first place.
+
+From your store's Admin Interface, you can manage all of the common tasks associated with managing your products. To reach the Admin Interface, first log into your store with your admin user account, then go to the `/admin` directory of your site. Click the "Products" tab.
+
+
+
+From here you can:
+
+* [Create New Products](creating_products)
+* [Delete Existing Products](deleting_products)
+* [Edit Existing Products](editing_products)
+* [Clone Existing Products](cloning_products)
+* [Search Existing Products](searching_products)
+
+In addition, you can set up new [Product Option Types and Values](product_options); add, edit, and remove [Product Properties](product_properties); and work with [Product Prototypes](product_prototypes).
diff --git a/guides/src/content/user/products/product_options.md b/guides/src/content/user/products/product_options.md
new file mode 100644
index 00000000000..cbf898a471a
--- /dev/null
+++ b/guides/src/content/user/products/product_options.md
@@ -0,0 +1,45 @@
+---
+title: Product Options
+---
+
+## Option Types and Option Values
+
+Option Types are a way to help distinguish products in your store from one another. They are particularly useful when you have many products that are basically of the same general category (Tshirts or mugs, for example), but with characteristics that can vary, such as color, size, or logo.
+
+For each Option Type, you will need to create one or more corresponding Option Values. If you create a "Size" Option Type, then you would need Option Values for it like "Small", "Medium", and "Large".
+
+### Creating Option Types and Option Values
+
+Option Types and Option Values are created at the store level, not the product level. This means that you only have to create each Option Type and Option Value once. Once an Option Type and Option Value is created it can be associated with any product in your store. To create an Option Type, click "Products", then "Option Types", then "New Option Type".
+
+
+
+You are required to fill in two fields: "Name" and "Presentation". You will see this same pattern several places in the Admin Interface. "Name" generally is the short term (usually one or two words) for the option you want to store. "Presentation" is the wordier, more descriptive term that gives your site's visitors a little more detail.
+
+***
+NOTE: Sometimes the term "Display" is used instead of "Presentation" to indicate what is shown to the user on the Product Variant's page.
+***
+
+For our first Option Type - Size - enter "Size" for the Name and "Size of the Tumbler" as the Presentation. Click "Update".
+
+When the screen refreshes, you see that Spree has helpfully provided you with a blank row in which you can enter your first Option Value for the new Option Type.
+
+
+
+We're going to need two Option Values (Large and Small) for the Size Option Value, so go ahead and click the "Add Option Value" button. This gives you two blank rows to work with.
+
+"Name" is easy - "Large" for the first, and "Small" for the second. Let's input "24-ounce cup" in the "Display" field for the Large Option Value and "16-ounce cup" for the Small Option Value.
+
+
+
+When you click "Update", Spree saves the two new Option Values, associates them with the Size Option Type, and takes you to the list of all Option Types.
+
+### Associating Option Values with a Product
+
+Our Spree application now knows that we have an Option Type with corresponding Option Values,but it doesn't know which of our products should have those Option Types. We have to explicitly tell it about those associations. We can do so either when we create a new Product (if the options have already been created), or when we edit an existing product.
+
+At the bottom of the Product edit form is a text box labeled "Option Types". When you click in this box, a drop-down appears with all of the Option Types you have defined for your store. All you have to do is click one or more of them to associate them with your Product.
+
+
+
+Don't forget to click "Update" to save your changes.
diff --git a/guides/src/content/user/products/product_properties.md b/guides/src/content/user/products/product_properties.md
new file mode 100644
index 00000000000..7b8764205aa
--- /dev/null
+++ b/guides/src/content/user/products/product_properties.md
@@ -0,0 +1,34 @@
+---
+title: Product Properties
+---
+
+## Product Properties
+
+Depending on the nature of your store and the products you sell, you may want to add "Properties" to your product descriptions. Properties are typically used to provide additional information about a product to help the customer make a better purchase decision. Here is an example of how a product's properties would display on the customer-facing area of a store:
+
+
+
+### Adding a Product Property
+
+Follow these steps to add a product Property. In this example, we are going to add a Property called "Country of Origin" with a value of "USA".
+
+1. Click the "Products" tab in your Admin Interface.
+2. Click "Properties".
+3. Click the "New Property" button.
+4. Enter values for the "Name" and "Presentation" fields, such as "Origin" and "Country of Origin", respectively.
+5. Click the "Create" button.
+6. Navigate to the edit page for one of the products in your store.
+7. Click the "Product Properties" link.
+8. Click in the empty text box field under "Property" and start typing the name of the property you want to use: "Origin". After you type a few letters, the property name will display, and you can click it to select it.
+9. Enter a country name for the "Value" field, such as "USA".
+10. Click "Update".
+
+Now, when you navigate to the product's page in your store, you will see the new Country of Origin property in the "Properties" list.
+
+
+
+***
+You can add as many "Product Properties" to an individual "Product" as you like - just use the "Add Product Properties" button on the Product Properties page for an individual product.
+***
+
+You can also add "Product Properties" on the fly as you're editing a "Product" - you don't have to specify them ahead of time. Just be cautious of defining too many similar properties ("Origin", "Country Origin", "Country of Origin"). It's best to re-use existing properties wherever you can.
diff --git a/guides/src/content/user/products/product_prototypes.md b/guides/src/content/user/products/product_prototypes.md
new file mode 100644
index 00000000000..222a049691a
--- /dev/null
+++ b/guides/src/content/user/products/product_prototypes.md
@@ -0,0 +1,47 @@
+---
+title: Prototypes
+---
+
+## Introduction
+
+A Prototype is like a Product blueprint, useful for helping you add a group of similar new products to your store more quickly. The general procedure is that you create a Prototype which is associated with certain [Option Types](product_options) and [Properties](product_properties); then you create products based on that Prototype, and only need to fill in the values for those Option Types and Properties.
+
+Imagine that you've just received a new shipment of picture frames from your supplier. Your new stock encompasses a variety of brands, sizes, colors, and materials, but they are all basically the same type of product. This is a prime use case for prototypes.
+
+***
+This guide presumes you have already created the [Option Types](product_options) and [Properties](product_properties) you need for your new prototype. If you haven't, you should do that first before proceeding.
+***
+
+### Creating a Prototype
+
+To create a prototype, go to the Admin Interface and click "Products", then "Prototypes". Click the "New Prototype" button.
+
+
+
+Input a value for the "Name" field (such as "Picture Frames"), and choose the properties and options you want to associate with this type of product.
+
+
+
+Click the "Create" button. You should now see your new prototype in the "Prototypes" list.
+
+
+
+# Using a Prototype to Create Products
+
+To create a new product based on the new prototype, click "Products" from the Admin Interface, then click the "New Product" button. Select "Picture Frames" from the "Prototypes" drop-down menu.
+
+
+
+When you do so, the Spree system shows you values for both of the Option Types you entered, so that it can automatically create [Product Variants](creating_products#understanding-variants) for you for each of them.
+
+Let's create the Product and all Variants for the fictional "Hinkledink Picture Frame" product. Input the product's Name, SKU, a Master Price (remember, you can change this for each variant), and make sure to set the Available On date to today, so it will show up in your store. Check the boxes for the options this particular product has, and click "Create".
+
+***
+Clicking the box next to an Option Type title will automatically check all of its Option Values for you.
+***
+
+
+
+Proceed with [creating the product](creating-product) as you would normally, adding any missing fields not supplied by the prototype.
+
+Be sure to update each of the Variants with corresponding images, SKUs, and - if applicable - correct pricing.
diff --git a/guides/src/content/user/products/searching_products.md b/guides/src/content/user/products/searching_products.md
new file mode 100644
index 00000000000..97bb0b7a2c5
--- /dev/null
+++ b/guides/src/content/user/products/searching_products.md
@@ -0,0 +1,38 @@
+---
+title: Searching Products
+---
+
+# Searching Products
+
+When you click the **Products** tab, you will see dropdown with the **Products settings** on the Admin Interface. In the next step click **Products** tab in the **Products settings**. Now you are presented with a summary of all actual products in the shop.
+
+The list shows you the following information about each order:
+
+* **SKU Number** - Product's unique ID.
+* **Status** - Sets if the product is available in the shop.
+* **Name with image** - Actual name and the image visible on front.
+* **Master price** - Main price of the product (there might also be different price for the variants).
+
+Next to each product there are 3 buttons.
+
+* **Edit** - Allows you to make [additional edition in the product](editing_products).
+* **Clone** - Allows you to make a [duplicate of the certain product](cloning_products).
+* **Delete** - Allows you to [delete certain product](deleting_products).
+
+
+
+## Filtering products
+
+You may not always want to see all of the products in the shop - the Spree default. You may want to view only those products that you want to make additional changes. Default way of sorting products that are present is by the name, but there is also possibility to sort them by Master price.
+
+You can choose one or more of the following options to narrow your product search, then click the **Filter** button to extend filter options in dropdown.
+
+
+
+### Name
+
+You can filter particular product by its name.
+
+### SKU
+
+Every time you create a product you may want to assign an SKU Number to make a search much faster and simplify shipping flow. You can use that number to search product.
diff --git a/guides/src/content/user/promotions.md b/guides/src/content/user/promotions.md
new file mode 100644
index 00000000000..55f597304d7
--- /dev/null
+++ b/guides/src/content/user/promotions.md
@@ -0,0 +1,125 @@
+---
+title: Promotions
+---
+
+## Introduction
+
+The Spree cart's promotions functionality allows you to offer coupons and discount to your site's users, based on the conditions you choose. This guide will explain to you all of the options at hand.
+
+To reach the Promotions pane, go to your Admin Interface and click the "Promotions" tab.
+
+## Creating a New Promotion
+
+To create a new promotion, click the "New Promotion" button.
+
+
+
+The page that renders allows you to set several standard options that apply to all promotions. Each is explained below.
+
+Option | Description
+|---|---|
+Name | The name you assign for the promotion.
+Event Name | This is what must happen before the system will check to see if the promotion will apply to the order. Options are: **Add to cart** (any time an item is added to the cart), **Order contents changed** (an item is added to or removed from an order, or a quantity for an item in the order changes), **User signup** (a store visitor creates an account on the site), **Coupon code added** (a store visitor inputs a coupon code at checkout. The code has to match what you input for the code value if you select this option), or **Visit static content page** (a store visitor visits a path that you declare. This is often used to ensure that a customer has reviewed your store's policies or has been exposed to some other content that is important to your business model.)
+Advertise | Checking this box will make the promotion visible to site visitors as they shop your store.
+Description | A more thorough explanation of the promotion. The customer will be able to see this description at checkout.
+Usage Limit | The maximum total number of times the promotion can be used in your store across all users. If you don't input a value for this setting, the promotion can be used an unlimited number of times. Beneath this input field is a "Current Usage" counter, which is useful later when you're editing a promotion and need to know how many redemptions the promotion has had.
+Starts At | The date the promotion becomes valid.
+Expires At | The date after which the promotion is invalid.
+
+When you enter values for these fields and click "Create", a new screen is rendered, giving you access to even more options for fine-tuning your promotion.
+
+### Rules
+
+Rules represent the factors that must be met for a promotion to be applicable to an order. You can set one or more rules for a single promotion. When you set multiple rules, you have the option of either requiring that all of the rules must be met for the promotion to apply, or allowing a promotion to apply to an order if even one of the rules is met.
+
+There are five types of rules. You can only add one rule of each type to a single promotion. Each is explained in detail below.
+
+
+
+#### Item Total
+
+When you select "Item total" from the "Add Rule of Type" drop-down menu and click "Add", you are declaring an Item Total rule.
+
+
+
+You can then set the parameters for this type of rule. Specifically, you can establish whether an order's items must be **greater than** or **equal to or greater than** the amount you set. Click "Update".
+
+***
+To remove a rule from a promotion, click the cross icon next to it.
+
+
+***
+
+#### Products
+
+Using a rule of this type means the order must contain **at least one** or **all** of the products you declare.
+
+
+
+To create this kind of rule, just select "Product(s)" from the "Add Rule of Type" drop-down menu and click "Add". Start typing in the name of the product(s) you want to apply discounts to into the "Choose Products" box. Click on the correct variants. Choose either "at least one" or "all" from the selection box, and click "Update".
+
+#### User
+
+You can use the User rule type to restrict a promotion to only those customers you declare. To create this type of rule, select "User" from the "Add Rule of Type" drop-down menu and click "Add". Start typing in the name or email address of the user(s) you want to offer this promotion to. As the correct users are offered, click them to add them to the list. Click "Update".
+
+
+
+#### First Order
+
+Select "First order" from the "Add Rule of Type" drop-down menu and click "Add" then "Update" to add a rule of this type to your promotion. This rule will restrict the promotion to only those customers ordering from you for the first time.
+
+
+
+#### User Logged In
+
+Add a rule of this type to restrict the promotion only to logged-in users. Select "User Logged In" from the "Add Rule of Type" drop-down list, click "Add", then click "Update".
+
+
+
+### Actions
+
+Whereas [Rules](#rules) establish whether a promotion applies or not, Actions determine what happens when a promotion does apply to an order. There are two types of actions: [create adjustments](#create-adjustments) and [create line items](#create-line-items).
+
+#### Create Adjustments
+
+When you select "Create adjustment" from the "Add Action of Type" drop-down menu and click "Add", the system presents you with several calculator options. These are the same as the options you read about in the [calculators guide](calculators), except that instead of a [price sack calculator](calculators#price-sack), there are two additional calculators: percent per item and free shipping.
+
+
+
+By default, when you add a new "Create adjustment" calculator it sets it to a "Flat percent" calculator. You can change this by selecting the new calculator type from the "Calculator" drop-down menu, but you will need to click the "Update" button to get that calculator's specific additional required fields to display.
+
+Each calculator has its own set of required additional information fields.
+
+Calculator Type | Additional Data Required
+|---|---|
+Flat Percent | Percentage amount
+Flat Rate | Amount of discount, and currency
+Flexible Rate | The cost of the first item, the cost of each additional item, the maximum number of items included in the promotion, and the currency
+Percent Per Item | Percentage amount
+Free Shipping | No additional info required
+
+Enter all required information for your calculator type, then click "Update".
+
+#### Create Line Items
+
+This action type is a way of automatically adding items to an order when a promotion applies to an order. To add this action to your promotion, select "Create line items" from the "Add Action of Type" drop-down menu and click "Add".
+
+
+
+Select the quantity and variant you want automatically added to the customer's order from the product drop-down menu. Click "Update".
+
+!!!
+Product variants added through Line Item Action Promotions will be priced as usual. If your intention is to add a free product, you should do both a Line Item action to add the product, and an Adjustment action to discount the cost of that variant.
+!!!
+
+## Editing a Promotion
+
+To edit a promotion, first go to the Promotions list (from the Admin Interface, click "Promotions"). Click the "Edit" icon next to the promotion.
+
+
+
+## Removing a Promotion
+
+To remove a promotion, click the "Delete" icon next to the promotion in the Promotions list.
+
+
diff --git a/guides/src/content/user/reports.md b/guides/src/content/user/reports.md
new file mode 100644
index 00000000000..cf97fc3f96c
--- /dev/null
+++ b/guides/src/content/user/reports.md
@@ -0,0 +1,17 @@
+---
+title: Reports
+---
+
+## Introduction
+
+Within the Admin Interface is a "Reports" tab. Information within this tab helps you understand how your store's income is apportioned.
+
+### Sales Total
+
+From the Listing Reports page, click the "Sales Total" link. Here you, input a date range by selecting a Start date and an End date, then clicking "Search".
+
+
+
+The resulting report will show you - for each type of currency you accept - what your orders' item total, adjustment total, and sales total was.
+
+
diff --git a/guides/src/content/user/shipments/calculators.md b/guides/src/content/user/shipments/calculators.md
new file mode 100644
index 00000000000..3c7f422ea37
--- /dev/null
+++ b/guides/src/content/user/shipments/calculators.md
@@ -0,0 +1,76 @@
+---
+title: Calculators
+---
+
+## Calculators
+
+A Calculator is the component of the Spree shipping system responsible for calculating the shipping price for each available [Shipping Method](shipping_methods).
+
+Spree ships with 5 default calculators:
+
+* [Flat rate (per order)](#flat-rate-per-order)
+* [Flat rate (per item)](#flat-rate-per-item)
+* [Flat percent](#flat-percent)
+* [Flexible rate](#flexible-rate)
+* [Price sack](#price-sack)
+
+### Flat Rate (per order)
+
+The Flat Rate (per order) calculator allows you to charge the same shipping price per order regardless of the number of items in the order. You define the flat rate charged per order at the shipping method level.
+
+For example, if you have two shipping methods defined for your store ("UPS 1-Day" and "UPS 2-Day"), and have selected "Flat rate" as the calculator type for each, you could charge a $15 flat rate shipping cost for the UPS 1-Day orders and a $10 flat rate shipping cost for the UPS 2-Day orders.
+
+### Flat Rate (per item)
+
+The Flat Rate (per item/product) calculator allows you to determine the shipping costs based on the number of items in the order.
+
+For example, if there are 4 items in an order and the flat rate per item amount is set to $10 then the total shipping costs for the order would be $40.
+
+### Flat Percent
+
+The Flat Percent calculator allows you to calculate shipping costs as a percent of the total amount charged for the order. The amount is calculated as follows:
+
+```ruby
+[item total] x [flat percentage]```
+
+For example, if an order had an item total of $31 and the calculator was configured to have a flat percent amount of 10, the shipping cost would be $3.10, because $31 x 10% = $3.10.
+
+### Flexible Rate
+
+The Flexible Rate calculator is typically used for promotional discounts when you want to give a specific discount for the first product, and then subsequent discounts for other products, up to a certain amount.
+
+The Flexible Rate calculator takes four inputs:
+
+* First Item Cost: the amount of shipping charged for the first item in the order.
+* Additional Item Cost: the amount of shipping charged for items beyond the first item.
+* Max Items: the maximum number of items on which shipping will be calculated.
+* Currency: defaults to the currency you have configured for your store.
+
+For example, if you set First Item Cost to $10, Additional Item Cost to $5, and Max Items to 4, you could be charging $10 for the first item, $5 for the next 3 items, and $0 for items beyond the first 4. Thus, an order with 1 item would have a shipping cost of $10. An order with two items would cost $15 to ship, and an order of 7 items would cost $25 to ship.
+
+### Price Sack
+
+The Price Sack calculator is a way to offer discount shipping to orders over a certain dollar amount. The Price Sack calculator takes four inputs:
+
+* Minimal Amount
+* Normal Amount
+* Discount Amount
+* Currency (defaults to the currency you have configured for your store)
+
+Any order whose subtotal is under is less than what you set for Minimal Amount would be charged a shipping cost of Normal Amount. Orders whose subtotals are equal to or greater than the Minimal Amount would be charged the Discount Amount.
+
+For example, suppose you create a shipping calculator with these settings:
+
+* Minimal Amount - $50
+* Normal Amount - $15
+* Discount Amount - $5
+
+A customer whose order subtotal equals $35 would be offered a shipping cost of $15 using this shipping method. A different customer whose order subtotal equals $55 would be offered a shipping cost of only $5.
+
+### Custom Calculators
+
+You can define your own calculator if you have more complex needs. In that case, check out the [Calculators Guide](../developer/calculators.html).
+
+## Next Step
+
+If you have followed this guide series [from the beginning](shipments), your store is now stocked with [shipping categories](shipping_categories), [geographical shipping zones](zones), and calculators. The final step is to pull it all together into [shipping methods](shipping_methods), from which your customers can choose at checkout.
\ No newline at end of file
diff --git a/guides/src/content/user/shipments/index.md b/guides/src/content/user/shipments/index.md
new file mode 100644
index 00000000000..a2246248b3a
--- /dev/null
+++ b/guides/src/content/user/shipments/index.md
@@ -0,0 +1,16 @@
+---
+title: Shipments
+---
+
+## Shipments
+
+Spree uses a very flexible and effective system to calculate shipping. This set of guides explains how Spree renders shipping options to your customers at checkout, how it calculates expected costs, and how you can configure your store with your own shipping options to fit your needs.
+
+To properly leverage Spree’s shipping system’s flexibility you must understand a few key concepts:
+
+* [Shipping Categories](shipping_categories)
+* [Zones](zones)
+* [Calculators](calculators) (to determine shipping rates)
+* [Shipping Methods](shipping_methods)
+
+Let's begin by understanding what [Shipping Categories](shipping_categories) are and how you can use them to differentiate products in your store.
\ No newline at end of file
diff --git a/guides/src/content/user/shipments/shipping_categories.md b/guides/src/content/user/shipments/shipping_categories.md
new file mode 100644
index 00000000000..4733b60d33d
--- /dev/null
+++ b/guides/src/content/user/shipments/shipping_categories.md
@@ -0,0 +1,31 @@
+---
+title: Shipping Categories
+---
+
+## Shipping Categories
+
+Shipping Categories are used to address special shipping needs for one or more of your products. The most common use for shipping categories is when certain products cannot be shipped in the same box. This is often due to a size or material constraint.
+
+For example, if a customer purchases a jump rope and a treadmill from an online exercise equipment store, the treadmill would be considered an over-sized item by most shipping carriers and would require special shipping arrangements. The jump rope could be sent via standard shipping.
+
+To handle this use case in Spree you would define a "Default" shipping category for the jump rope and any other products that can use standard shipping methods, and an "Over-sized" shipping category for extremely large items like the treadmill. You would then assign the "Over-sized" shipping category to your treadmill product and the "Default" shipping category to your jump rope product.
+
+During checkout, the shipping categories assigned to the products in your customer's order will be a key factor in determining which shipping methods and costs your Spree store offers to your customer at checkout.
+
+### Creating a Shipping Category
+
+To create a new shipping category, go to the Admin Interface, click the "Configuration" tab, click the "Shipping Categories" link, and then click the "New Shipping Category" button. Enter a name for your new shipping category and click the "Create" button.
+
+
+
+### Adding a Shipping Category to a Product
+
+Once you've created your shipping categories you can assign the appropriate category to each of your products. To associate a shipping category with a product, go to the Admin Interface, and click the "Products" tab. Then, click on the product that you would like to edit from the list that appears.
+
+Once you are in edit mode for the product, select the shipping category you want to assign to the product from the "Shipping Categories" drop-down menu, and click "Update".
+
+
+
+## Next Step
+
+Now that you understand how Shipping Categories work, let's move on to the next piece of the Spree shipping system - shipping [zones](zones).
diff --git a/guides/src/content/user/shipments/shipping_methods.md b/guides/src/content/user/shipments/shipping_methods.md
new file mode 100644
index 00000000000..32a7eb44e2a
--- /dev/null
+++ b/guides/src/content/user/shipments/shipping_methods.md
@@ -0,0 +1,93 @@
+---
+title: Shipping Methods
+---
+
+## Shipping Methods
+
+Now that you have set up all of the pieces you need, it's time to put them together into the shipping options that the customer sees when they reach checkout. These options are called Shipping Methods - they are the carriers and services used to send your products.
+
+### Adding a Shipping Method
+
+To add a new shipping method to your store, go to the Admin Interface and click "Configuration", then "Shipping Methods". Click the "New Shipping Method" button to open the New Shipping Method form.
+
+
+
+#### Name
+
+Enter a name for the shipping method. This is the exact wording that the customer will see at checkout. This should include both the carrier (USPS, UPS, Fedex, DHL, etc.) as well as the service type (First Class Mail, Overnight, Ground, etc.) So it would be very common to need several shipping methods for your store, for example:
+
+* USPS First Class
+* USPS First Class International
+* USPS Priority
+* USPS MediaMail
+* UPS Two-Day
+* UPS Ground
+* Fedex Overnight
+
+Remember that you will need to associate one or more [zones](#zones) with each shipping method in order for it to appear as an option at checkout.
+
+#### Display
+
+From the "Display" drop-down box, choose whether you want to have the option display only on the backend, the frontend, or both.
+
+Shipping methods that are displayed on the frontend can be chosen by your store's customers at checkout time, as long as the products in the order can be shipped by that carrier and the shipping address is one the carrier serves.
+
+If a shipping method is available only on backend, then only your store's administrators can assign it to an order. Some examples of cases where you might want to use a backend-only shipping method:
+
+* You sell handmade wind chimes. You want to offer a "Pick-up in Store" option, but only to certain customers.
+* With your online produce market you provide personal delivery of goods, but only to your best local customers.
+* Yours is a photography studio. You usually sell prints that physical delivery, but for some clients you are willing to send electronic media that they can print themselves.
+
+#### Tracking URL
+
+You can optionally input a tracking URL for your new shipping method. This allows customers to track the progress of their package from your [Stock Location](configuring_inventory) to the order's shipping address. The string ":tracking" will be replaced with the tracking number you input once you actually process the order.
+
+You may need to check with the shipping carrier to see if they have a Shipping Confirmation URL that customers can use for this service. Some [commonly-used tracking URLs](http://verysimple.com/2011/07/06/ups-tracking-url/) are available online.
+
+!!!
+Please note that Spree Commerce, Inc. makes no claims of warranty or accuracy for the information presented on third-party websites. We strongly urge you to verify the information independently before you put it into production on your store.
+!!!
+
+#### Categories
+
+Some shipping methods may only apply to certain types of products in your store, regardless of where those items are being shipped. You may only want to send over-sized items via UPS Ground, for example, and not via USPS Priority. The options shown in the "Categories" section correspond to the [Shipping Categories](shipping_categories) you set up in an earlier section of this guide series.
+
+
+
+Check the boxes next to the categories you want served by your new shipping method.
+
+#### Zones
+
+In [a previous step to this guide](zones) you learned about how to set up geographical zones for your store. Within the form's "Zones" section, you need to specify which zones are served by this shipping method. The "EU_VAT" (European Value-Added Tax) zone could be served by USPS First Class International, but could _not_ be served by USPS Priority Mail.
+
+
+
+Check the boxes next to any zones you want served by this shipping method.
+
+#### Calculator
+
+Each shipping method is associated with one [Calculator](calculators). You can choose one of the built-in Spree calculators, or one you made yourself.
+
+
+
+Once you've made your calculator selection, click the "Create" button to finalize your new shipping method. The screen will refresh with one or more fields you'll use to set the parameters of your calculator. For example, creating a shipping method with a flat percent calculator will produce a screen like this:
+
+
+
+If necessary, you can re-read the [Calculators](calculators) portion of this guide series to better understand the options. Click the "Update" button, and your shipping method is now complete!
+
+### Editing a Shipping Method
+
+To edit an existing method, go to the Admin Interface and click "Configuration", then "Shipping Methods". Click the "Edit" icon next to any of the shipping methods in the list.
+
+
+
+The form and all options that come up are the same as those you used in creating your shipping methods.
+
+### Deleting a Shipping Method
+
+To delete a shipping method, go to the Admin Interface and click "Configuration", then "Shipping Methods". Click the "Delete" icon next to any of the shipping methods in the list.
+
+
+
+Confirm that you want to delete the shipping method by clicking "OK".
diff --git a/guides/src/content/user/shipments/zones.md b/guides/src/content/user/shipments/zones.md
new file mode 100644
index 00000000000..bcc2808ae8a
--- /dev/null
+++ b/guides/src/content/user/shipments/zones.md
@@ -0,0 +1,39 @@
+---
+title: Zones
+---
+
+## Zones
+
+Zones serve as a way to define shipping rules for a particular geographic area. A zone is made up of a set of either countries or states. Zones are used within Spree to define the rules for a [Shipping Method](shipping_methods).
+
+Each shipping method can be assigned to only one zone. For example, if one of the shipping methods for your store is UPS Ground (a US-only shipping carrier), then the zone for that shipping method should be defined as the United States.
+
+When the customer enters their shipping address during checkout, Spree uses that information to determine which zone the order is being delivered to, and only presents the shipping methods to the customer that are defined for that zone.
+
+### Creating a Zone
+
+To create a new zone, go to the Admin Interface, click the "Configuration" tab, click the "Zones" link, and then click the "New Zone" button. Enter a name and description for your new zone. Decide if you want it to be the default zone selected for the purposes of calculating sales tax. Choose whether you want the zone to be country-based or state-based. Click the "Create" button once complete.
+
+
+
+### Adding Members to a Zone
+
+Once you have a zone set up, you can associate either countries or states with it. To do this, go back to the Zones list (from the Admin Interface, click "Configuration", then "Zones"). Click the "Edit" icon next to the zone you just created.
+
+
+
+Choose a country or state from the drop-down box. Follow the same steps to add additional countries or states for the Zone.
+
+
+
+Click "Update" once complete.
+
+### Removing Members From a Zone
+
+It is easy to remove a state or country from one of your zones. Just go to your Admin Interface and click "Configuration", then "Zones". Click the "Edit" icon next to the zone you want to change. To remove a member of the zone, just click the X icon below its name.
+
+
+
+## Next Step
+
+Once you have set up all of the shipping zones you need, it's time to move on to the next Spree shipping component: [Calculators](calculators).
diff --git a/guides/src/content/user/users/creating_users.md b/guides/src/content/user/users/creating_users.md
new file mode 100644
index 00000000000..3b6fcbeb7fb
--- /dev/null
+++ b/guides/src/content/user/users/creating_users.md
@@ -0,0 +1,18 @@
+---
+title: Creating a New User
+section: creating_users
+---
+
+# Introduction
+
+To create a new user for your store, go into the Admin Interface, click the **Users** tab, and click the **New User button**.
+
+
+
+The three mandatory fields ("Email", "Password" and "Password Confirmation") and Roles checkboxes ("Admin" and "User" those are created by Spree default, there is possibility to extend Roles by backend work).
+
+
+
+## Editing existing user
+
+Once you have created an account you are redirected to User edition page about which you can find more [here](editing_users).
diff --git a/guides/src/content/user/users/deleting_users.md b/guides/src/content/user/users/deleting_users.md
new file mode 100644
index 00000000000..874333bd178
--- /dev/null
+++ b/guides/src/content/user/users/deleting_users.md
@@ -0,0 +1,13 @@
+---
+title: Deleting Users
+---
+
+# Introduction
+
+To delete a user in your store, go into the Admin Interface and click the **Users** tab. A list of your store's users will appear. Find the user that you would like to delete and click the "Delete" icon on the right to remove it from your store.
+
+
+
+A message will appear asking you to confirm that you want to delete the user. Click "OK".
+
+ "Please be aware that you cannot delete users that has placed any orders in the past"
diff --git a/guides/src/content/user/users/editing_users.md b/guides/src/content/user/users/editing_users.md
new file mode 100644
index 00000000000..1dabfb1e99f
--- /dev/null
+++ b/guides/src/content/user/users/editing_users.md
@@ -0,0 +1,120 @@
+---
+title: Editing Users
+---
+
+## Introduction.
+
+There is a possibility to edit existing users. Simply when you enter **Users** tab search for the certain user that you would like to edit and press **Edit icon**.
+
+
+
+## General Settings
+
+When you first open the account edition page you will see **General Settings**.
+
+
+
+In this page you can make additional changes to the user like email, roles and password. Also you can see, **Clear** and **Regenerate Key** API Key.
+
+
+
+Moreover there are **Lifetime Stats** collected by the Spree. You can see the following:
+
+* **Total Sales** - It defines how much the user has spent money in your shop.
+* **Orders** - Shows the number how many orders the user has bought.
+* **Average Order Value** - It shows the average price that the user spent in your shop.
+* **Store Credits** - You can get more information about Store Credits below.
+* **Member Since** - Shows the date when the user created an account on your page.
+
+
+
+### Addresses
+
+In this section you can manage addresses (**Shipping** and **Billing**) defined by the user during the checkout. There are also **Lifetime Stats** visible on the bottom of this page.
+When you make changes in addresses, you have to press **Update** button in order to save the changes, also if you don't want to save them simply press **Cancel** button.
+
+
+
+### Orders
+
+There is a simply review of all orders created by the user. There are also **Lifetime Stats** visible on the bottom of this page. Few options are worth to mention:
+
+* **Completed at** - This is the date when the user created certain order.
+* **Number** - This is an unique ID for a certain order, also you can move directly to the order when you click the ID.
+* **State** - It shows current state for the certain order, you can find more [about order state here](order_states).
+* **Total** - It shows total price for the certain order.
+
+
+
+### Items
+
+Very simlar to **Orders** tab, however, few more options are present here about the items purchased by the user:
+
+* **Completed at** - This is the date when the user created certain order for the item.
+* **Description** - States the information about certain item, full name of the product, its SKU number and image.
+* **Price** - Price without taxes and shipping cost.
+* **Quantity** - Total number of purchased product.
+* **Total** - It shows total price for the certain order.
+* **State** - As previously aformentioned above there is more information [here](order_states).
+* **Order #** - It shows order's unique ID that hyperlinks you to order's details.
+
+
+
+### Store Credits
+
+Firstly, to add store credits to the User, you have to create a **Category** which you can learn about more [here](configuring_store_credit_categories).
+Once you create a category you can assign Store Credits to the Users by simply, clicking **Edit** on certain User and pressing **Store Credit** in right panel.
+
+
+
+You will see **Store Credit** panel. Like in every other tab in the User's account you can see **Lifetime Stats**.
+
+
+
+To add Store Credits press **Add Store Credit**. At this point, you can choose value that the User will receive, set Category and describe something important within **Memo** field. Memo field is visible to all other admins that will edit Store Credits added by you. Then just press **Create** button to accept your changes or simply press **Cancel** to exit without saving the changes.
+
+
+
+Now you can see how the Store Credits are assigned to the user. New options are visible here:
+
+* **Credited** - Value that shows how much Store Credits has been added to the User account.
+* **Used** - Amount of Store Credits spent.
+* **Category** - Category that Store Credits were assigned to.
+* **Created By** - The Admin's email that added Store Credits to the certain User.
+* **Issued On** - Date of granting Store Credits.
+
+
+
+As an Admin you are able to edit or delete Store Credits previously assigned to the User.
+
+
+
+
+
+Editing Store Credits will present you the same options like adding them.
+
+
+
+Those Store Credits are visible to the User in few places during checkout. It's worth to mention that the User is not forced to use Store Credits during Payment step - the Spree default. Spree gives a user choice to pay full price with Credit Card or use Store Credits.
+
+
+
+Once the User decides to use the Store Credits there is a possibility to cancel this choice.
+
+
+
+If the user use Store Credits and the amount will not cover whole order's price, rest will be charged off the Credit Card or PayPal.
+
+
+
+Once the order has been placed there is recapitulation of the order. The user can see the following: **Billing Adress**, **Shipping Address**, **Shipment method** which is previously chosed by the user, **Payment Information** - here, the user can see if and how much of Store Credits has been spent on the order, **Items purchased** and information about order's payment.
+
+
+
+As an Admin you are able to check how the user paid for the order. Simply choose order that you would like to inspect and follow to **Payments** tab. If you don't know yet how to find this tab you can find out [here](entering_orders). Admin has to capture the payment manually by default. In order to enable Automatic Payment Capture for the future payments we strongly recommend to read about it [here](payment_methods).
+
+
+
+Also as an Admin you can observe used Store Credits in **Users -> Store Credits**.
+
+
diff --git a/guides/src/content/user/users/index.md b/guides/src/content/user/users/index.md
new file mode 100644
index 00000000000..4c321778718
--- /dev/null
+++ b/guides/src/content/user/users/index.md
@@ -0,0 +1,14 @@
+---
+title: Users
+---
+
+## Users
+
+The User page of the Admin interface is that area where you store all the data of user that have ever created an account on your page. In this tab you can create users, set their roles, edit and delete them.
+
+The guides in this section will walk you through the user tab and show you how to customize your Spree users:
+
+* [Create a User](creating_users)
+* [Search Users](searching_users)
+* [Edit an User](editing_users)
+* [Delete an User](deleting_users)
diff --git a/guides/src/content/user/users/searching_users.md b/guides/src/content/user/users/searching_users.md
new file mode 100644
index 00000000000..879e58208b0
--- /dev/null
+++ b/guides/src/content/user/users/searching_users.md
@@ -0,0 +1,39 @@
+---
+title: Searching Users
+---
+
+# Searching Users
+
+When you click the **Users** tab you will see **Filter dropdown**, **New User button** and all existing users that have ever created an account on your page.
+
+Next to each user there are 2 buttons and additionaly you can sort them by name by simply clicking **User** above the user's emails.
+
+* **Edit** - Allows you to make [additional edition in the user's settings](editing_users).
+* **Delete** - Allows you to [delete the user permanently](deleting_users).
+* **Sort by user's name** - Allows you to sort the users by name (A-Z, Z-A).
+
+
+
+## Filtering Users
+
+You may not always want to see all of the users - the Spree default. You may want to view only those users that you want to make additional changes. Default way of sorting the users that are present is by the name.
+
+You can choose one or more of the following options to narrow your user search, then click the **Filter** button to extend filter options in dropdown.
+
+
+
+### Email
+
+Allows you to search the user by the email.
+
+### First Name
+
+Allows you to search the user by the first name used during registration.
+
+### Last Name
+
+Allows you to search the user by the last name used during registration.
+
+### Company
+
+Allows you to search the user by the company.
diff --git a/guides/src/data/401.js b/guides/src/data/401.js
new file mode 100644
index 00000000000..fc5747ca62a
--- /dev/null
+++ b/guides/src/data/401.js
@@ -0,0 +1,3 @@
+export default {
+ error: 'You are not authorized to perform that action.'
+}
diff --git a/guides/src/data/404.js b/guides/src/data/404.js
new file mode 100644
index 00000000000..134304054c0
--- /dev/null
+++ b/guides/src/data/404.js
@@ -0,0 +1,3 @@
+export default {
+ error: 'The resource you were looking for could not be found.'
+}
diff --git a/guides/src/data/_no_api_key.js b/guides/src/data/_no_api_key.js
new file mode 100644
index 00000000000..9640a94f154
--- /dev/null
+++ b/guides/src/data/_no_api_key.js
@@ -0,0 +1 @@
+export default { error: 'You must specify an API key.' }
diff --git a/guides/src/data/address.js b/guides/src/data/address.js
new file mode 100644
index 00000000000..410377c2703
--- /dev/null
+++ b/guides/src/data/address.js
@@ -0,0 +1,22 @@
+import ADDRESS_COUNTRY from './address_country'
+import ADDRESS_STATE from './address_state'
+
+export default {
+ id: 1,
+ firstname: 'Spree',
+ lastname: 'Commerce',
+ full_name: 'Spree Commerce',
+ address1: '1 Someplace Lane',
+ address2: 'Suite 1',
+ city: 'Bethesda',
+ zipcode: '16804',
+ phone: '123.4567.890',
+ company: null,
+ alternative_phone: null,
+ country_id: 1,
+ state_id: 1,
+ state_name: null,
+ state_text: 'NY',
+ country: ADDRESS_COUNTRY,
+ state: ADDRESS_STATE
+}
diff --git a/guides/src/data/address_country.js b/guides/src/data/address_country.js
new file mode 100644
index 00000000000..445fed77c2b
--- /dev/null
+++ b/guides/src/data/address_country.js
@@ -0,0 +1,8 @@
+export default {
+ id: 1,
+ iso_name: 'UNITED STATES',
+ iso: 'US',
+ iso3: 'USA',
+ name: 'United States',
+ numcode: 1
+}
diff --git a/guides/src/data/address_state.js b/guides/src/data/address_state.js
new file mode 100644
index 00000000000..a7ee267b22d
--- /dev/null
+++ b/guides/src/data/address_state.js
@@ -0,0 +1,6 @@
+export default {
+ id: 1,
+ name: 'New York',
+ abbr: 'NY',
+ country_id: 1
+}
diff --git a/guides/src/data/checkout_steps.js b/guides/src/data/checkout_steps.js
new file mode 100644
index 00000000000..03dce0341b7
--- /dev/null
+++ b/guides/src/data/checkout_steps.js
@@ -0,0 +1 @@
+export default ['address,', 'delivery', 'complete']
diff --git a/guides/src/data/classification.js b/guides/src/data/classification.js
new file mode 100644
index 00000000000..3ecf598cfa9
--- /dev/null
+++ b/guides/src/data/classification.js
@@ -0,0 +1,7 @@
+import TAXON from './taxon'
+
+export default {
+ taxon_id: 3,
+ position: 1,
+ taxon: TAXON
+}
diff --git a/guides/src/data/countries.js b/guides/src/data/countries.js
new file mode 100644
index 00000000000..2d60a84ccd8
--- /dev/null
+++ b/guides/src/data/countries.js
@@ -0,0 +1,3 @@
+import COUNTRY from './country'
+
+export default { countries: [COUNTRY], count: 25, current_page: 1, pages: 5 }
diff --git a/guides/src/data/country.js b/guides/src/data/country.js
new file mode 100644
index 00000000000..445fed77c2b
--- /dev/null
+++ b/guides/src/data/country.js
@@ -0,0 +1,8 @@
+export default {
+ id: 1,
+ iso_name: 'UNITED STATES',
+ iso: 'US',
+ iso3: 'USA',
+ name: 'United States',
+ numcode: 1
+}
diff --git a/guides/src/data/country_with_state.js b/guides/src/data/country_with_state.js
new file mode 100644
index 00000000000..927196eac56
--- /dev/null
+++ b/guides/src/data/country_with_state.js
@@ -0,0 +1,5 @@
+import * as R from 'ramda'
+import COUNTRY from './country'
+import STATE from './state'
+
+export default R.merge(COUNTRY, { states: [STATE] })
diff --git a/guides/src/data/image.js b/guides/src/data/image.js
new file mode 100644
index 00000000000..05c0db3856c
--- /dev/null
+++ b/guides/src/data/image.js
@@ -0,0 +1,17 @@
+export default {
+ id: 1,
+ position: 1,
+ attachment_content_type: 'image/jpg',
+ attachment_file_name: 'ror_tote.jpeg',
+ type: 'Spree::Image',
+ attachment_updated_at: null,
+ attachment_width: 360,
+ attachment_height: 360,
+ alt: null,
+ viewable_type: 'Spree::Variant',
+ viewable_id: 1,
+ mini_url: '/spree/products/1/mini/file.png?1370533476',
+ small_url: '/spree/products/1/small/file.png?1370533476',
+ product_url: '/spree/products/1/product/file.png?1370533476',
+ large_url: '/spree/products/1/large/file.png?1370533476'
+}
diff --git a/guides/src/data/images.js b/guides/src/data/images.js
new file mode 100644
index 00000000000..8b6925b4d36
--- /dev/null
+++ b/guides/src/data/images.js
@@ -0,0 +1,3 @@
+import IMAGE from './image'
+
+export default { images: [IMAGE] }
diff --git a/guides/src/data/index.js b/guides/src/data/index.js
new file mode 100644
index 00000000000..77b95e91396
--- /dev/null
+++ b/guides/src/data/index.js
@@ -0,0 +1,101 @@
+import IMAGE from './image'
+import IMAGES from './images'
+import OPTION_TYPE from './option_type'
+import OPTION_TYPES from './option_types'
+import OPTION_VALUE from './option_value'
+import OPTION_VALUES from './option_values'
+import USER from './user'
+import USERS from './users'
+import VARIANT from './variant'
+import VARIANT_BIG from './variant_big'
+import VARIANTS_BIG from './variants_big'
+import ADDRESS from './address'
+import NEW_ORDER_SHOW from './new_order_show'
+import LINE_ITEM from './line_item'
+import ORDER_FAILED_TRANSITION from './order_failed_transition'
+import COUNTRY_WITH_STATE from './country_with_state'
+import COUNTRIES from './countries'
+import ZONE from './zone'
+import ZONES from './zones'
+import TAXONOMY from './taxonomy'
+import TAXONOMIES from './taxonomies'
+import NEW_TAXONOMY from './new_taxonomy'
+import TAXON from './taxon'
+import TAXON_WITH_CHILDREN from './taxon_with_children'
+import TAXONS_WITH_CHILDREN from './taxons_with_children'
+import STOCK_ITEM from './stock_item'
+import STOCK_ITEMS from './stock_items'
+import STOCK_MOVEMENT from './stock_movement'
+import STOCK_MOVEMENTS from './stock_movements'
+import STOCK_LOCATION from './stock_location'
+import STOCK_LOCATIONS from './stock_locations'
+import STATE from './state'
+import STATES from './states'
+import SHIPMENT_SMALL from './shipment_small'
+import SHIPMENTS from './shipments'
+import RETURN_AUTHORIZATION from './return_authorization'
+import RETURN_AUTHORIZATIONS from './return_authorizations'
+import PRODUCT from './product'
+import PRODUCTS from './products'
+import PRODUCT_PROPERTY from './product_property'
+import PRODUCT_PROPERTIES from './product_properties'
+import PAYMENT from './payment'
+import PAYMENTS from './payments'
+import ORDER_SHOW from './order_show'
+import ORDER_SHOW_2 from './order_show_2'
+import ORDERS from './orders'
+import _404 from './404'
+import _401 from './401'
+import _NO_API_KEY from './_no_api_key'
+
+export default {
+ address: ADDRESS,
+ image: IMAGE,
+ images: IMAGES,
+ option_type: OPTION_TYPE,
+ option_types: OPTION_TYPES,
+ option_value: OPTION_VALUE,
+ option_values: OPTION_VALUES,
+ user: USER,
+ users: USERS,
+ variant: VARIANT,
+ variant_big: VARIANT_BIG,
+ variants_big: VARIANTS_BIG,
+ new_order_show: NEW_ORDER_SHOW,
+ line_item: LINE_ITEM,
+ order_failed_transition: ORDER_FAILED_TRANSITION,
+ country_with_state: COUNTRY_WITH_STATE,
+ countries: COUNTRIES,
+ zone: ZONE,
+ zones: ZONES,
+ taxonomy: TAXONOMY,
+ taxonomies: TAXONOMIES,
+ new_taxonomy: NEW_TAXONOMY,
+ taxon: TAXON,
+ taxon_with_children: TAXON_WITH_CHILDREN,
+ taxons_with_children: TAXONS_WITH_CHILDREN,
+ stock_item: STOCK_ITEM,
+ stock_items: STOCK_ITEMS,
+ stock_movement: STOCK_MOVEMENT,
+ stock_movements: STOCK_MOVEMENTS,
+ stock_location: STOCK_LOCATION,
+ stock_locations: STOCK_LOCATIONS,
+ state: STATE,
+ states: STATES,
+ shipment_small: SHIPMENT_SMALL,
+ shipments: SHIPMENTS,
+ return_authorization: RETURN_AUTHORIZATION,
+ return_authorizations: RETURN_AUTHORIZATIONS,
+ product: PRODUCT,
+ products: PRODUCTS,
+ product_property: PRODUCT_PROPERTY,
+ product_properties: PRODUCT_PROPERTIES,
+ payment: PAYMENT,
+ payments: PAYMENTS,
+ order_show: ORDER_SHOW,
+ order_show_2: ORDER_SHOW_2,
+ orders: ORDERS,
+ 404: _404,
+ 401: _401,
+ no_api_key: _NO_API_KEY
+}
diff --git a/guides/src/data/inventory_unit.js b/guides/src/data/inventory_unit.js
new file mode 100644
index 00000000000..ff9ebeb9ffb
--- /dev/null
+++ b/guides/src/data/inventory_unit.js
@@ -0,0 +1,17 @@
+import * as R from 'ramda'
+
+import VARIANT from './variant'
+import LINE_ITEM from './line_item'
+
+const rejectList = ['variant', 'adjustments']
+const hasRejectedKey = list => R.includes(rejectList, R.keys(list))
+const filteredLineItem = R.reject(hasRejectedKey, LINE_ITEM)
+
+export default {
+ id: 1,
+ state: 'on_hand',
+ variant_id: 1,
+ shipment_id: 1,
+ variant: VARIANT,
+ line_item: filteredLineItem
+}
diff --git a/guides/src/data/line_item.js b/guides/src/data/line_item.js
new file mode 100644
index 00000000000..aa6703e6644
--- /dev/null
+++ b/guides/src/data/line_item.js
@@ -0,0 +1,14 @@
+import * as R from 'ramda'
+import VARIANT from './variant'
+
+export default {
+ id: 1,
+ quantity: 2,
+ price: '19.99',
+ variant_id: 1,
+ variant: R.merge(VARIANT, { product_id: 1 }),
+ adjustments: [],
+ single_display_amount: '$19.99',
+ display_total: '$39.99',
+ total: '39.99'
+}
diff --git a/guides/src/data/manifest.js b/guides/src/data/manifest.js
new file mode 100644
index 00000000000..6f64b4bf0b0
--- /dev/null
+++ b/guides/src/data/manifest.js
@@ -0,0 +1,7 @@
+import VARIANT from './variant'
+
+export default {
+ variant: VARIANT,
+ quantity: 1,
+ states: { on_hand: 1 }
+}
diff --git a/guides/src/data/new_order.js b/guides/src/data/new_order.js
new file mode 100644
index 00000000000..21f8894d1e1
--- /dev/null
+++ b/guides/src/data/new_order.js
@@ -0,0 +1,37 @@
+import CHECKOUT_STEPS from './checkout_steps'
+
+export default {
+ id: 1,
+ number: 'R335381310',
+ item_total: '0.0',
+ total: '0.0',
+ ship_total: '0.0',
+ state: 'cart',
+ adjustment_total: '0.0',
+ user_id: 1,
+ created_at: '2012-10-24T01:02:25Z',
+ updated_at: '2012-10-24T01:02:25Z',
+ completed_at: null,
+ payment_total: '0.0',
+ shipment_state: null,
+ payment_state: null,
+ email: 'spree@example.com',
+ special_instructions: null,
+ channel: 'spree',
+ included_tax_total: '0.0',
+ additional_tax_total: '0.0',
+ display_included_tax_total: '$0.0',
+ display_additional_tax_total: '$0.0',
+ tax_total: '0.0',
+ currency: 'USD',
+ considered_risky: false,
+ canceler_id: null,
+ display_item_total: '$0.00',
+ total_quantity: 0,
+ display_total: '$0.00',
+ display_ship_total: '$0.00',
+ display_tax_total: '$0.00',
+ display_adjustment_total: '$0.00',
+ token: 'abcdef123456',
+ checkout_steps: CHECKOUT_STEPS
+}
diff --git a/guides/src/data/new_order_show.js b/guides/src/data/new_order_show.js
new file mode 100644
index 00000000000..1e1a6173ebd
--- /dev/null
+++ b/guides/src/data/new_order_show.js
@@ -0,0 +1,13 @@
+import * as R from 'ramda'
+import NEW_ORDER from './new_order'
+
+export default R.merge(NEW_ORDER, {
+ bill_address: null,
+ ship_address: null,
+ line_items: [],
+ payments: [],
+ shipments: [],
+ adjustments: [],
+ credit_cards: [],
+ permissions: { can_update: true }
+})
diff --git a/guides/src/data/new_taxonomy.js b/guides/src/data/new_taxonomy.js
new file mode 100644
index 00000000000..c20672bd6a7
--- /dev/null
+++ b/guides/src/data/new_taxonomy.js
@@ -0,0 +1,7 @@
+import TAXON from './taxon'
+
+export default {
+ id: 1,
+ name: 'Brand',
+ root: TAXON
+}
diff --git a/guides/src/data/option_type.js b/guides/src/data/option_type.js
new file mode 100644
index 00000000000..806e2bb6817
--- /dev/null
+++ b/guides/src/data/option_type.js
@@ -0,0 +1,9 @@
+import OPTION_VALUE from './option_value'
+
+export default {
+ id: 1,
+ name: 'tshirt-size',
+ presentation: 'Size',
+ position: 1,
+ option_values: [OPTION_VALUE]
+}
diff --git a/guides/src/data/option_types.js b/guides/src/data/option_types.js
new file mode 100644
index 00000000000..4da2d7dae07
--- /dev/null
+++ b/guides/src/data/option_types.js
@@ -0,0 +1,3 @@
+import OPTION_TYPE from './option_type'
+
+export default [OPTION_TYPE]
diff --git a/guides/src/data/option_value.js b/guides/src/data/option_value.js
new file mode 100644
index 00000000000..712cc9ac1c9
--- /dev/null
+++ b/guides/src/data/option_value.js
@@ -0,0 +1,8 @@
+export default {
+ id: 1,
+ name: 'Small',
+ presentation: 'S',
+ option_type_name: 'tshirt-size',
+ option_type_id: 1,
+ option_type_presentation: 'S'
+}
diff --git a/guides/src/data/option_values.js b/guides/src/data/option_values.js
new file mode 100644
index 00000000000..50c55e64366
--- /dev/null
+++ b/guides/src/data/option_values.js
@@ -0,0 +1,3 @@
+import OPTION_VALUE from './option_value'
+
+export default [OPTION_VALUE]
diff --git a/guides/src/data/order.js b/guides/src/data/order.js
new file mode 100644
index 00000000000..234ed86f9f6
--- /dev/null
+++ b/guides/src/data/order.js
@@ -0,0 +1,37 @@
+import CHECKOUT_STEPS from './checkout_steps'
+
+export default {
+ id: 1,
+ number: 'R335381310',
+ item_total: '100.0',
+ total: '100.0',
+ ship_total: '0.0',
+ state: 'cart',
+ adjustment_total: '-12.0',
+ user_id: null,
+ created_at: '2012-10-24T01:02:25Z',
+ updated_at: '2012-10-24T01:02:25Z',
+ completed_at: null,
+ payment_total: '0.0',
+ shipment_state: null,
+ payment_state: null,
+ email: null,
+ special_instructions: null,
+ channel: 'spree',
+ included_tax_total: '0.0',
+ additional_tax_total: '0.0',
+ display_included_tax_total: '$0.0',
+ display_additional_tax_total: '$0.0',
+ tax_total: '0.0',
+ currency: 'USD',
+ considered_risky: false,
+ canceler_id: null,
+ display_item_total: '$100.00',
+ total_quantity: 1,
+ display_total: '$100.00',
+ display_ship_total: '$0.00',
+ display_tax_total: '$0.00',
+ display_adjustment_total: '$0.00',
+ token: 'abcdef123456',
+ checkout_steps: CHECKOUT_STEPS
+}
diff --git a/guides/src/data/order_failed_transition.js b/guides/src/data/order_failed_transition.js
new file mode 100644
index 00000000000..568ad414bae
--- /dev/null
+++ b/guides/src/data/order_failed_transition.js
@@ -0,0 +1,5 @@
+export default {
+ error:
+ 'The order could not be transitioned. Please fix the errors and try again.',
+ errors: { email: ["can't be blank"] }
+}
diff --git a/guides/src/data/order_show.js b/guides/src/data/order_show.js
new file mode 100644
index 00000000000..00357213600
--- /dev/null
+++ b/guides/src/data/order_show.js
@@ -0,0 +1,15 @@
+import * as R from 'ramda'
+
+import ORDER from './order'
+import LINE_ITEM from './line_item'
+
+export default R.merge(ORDER, {
+ bill_address: null,
+ ship_address: null,
+ line_items: [LINE_ITEM],
+ payments: [],
+ shipments: [],
+ adjustments: [],
+ credit_cards: [],
+ permissions: { can_update: true }
+})
diff --git a/guides/src/data/order_show_2.js b/guides/src/data/order_show_2.js
new file mode 100644
index 00000000000..0ac8329de19
--- /dev/null
+++ b/guides/src/data/order_show_2.js
@@ -0,0 +1,15 @@
+import * as R from 'ramda'
+
+import ORDER from './order'
+import LINE_ITEM from './line_item'
+
+export default R.merge(ORDER, {
+ bill_address: null,
+ ship_address: null,
+ line_items: [R.merge(LINE_ITEM, { quantity: 5 })],
+ payments: [],
+ shipments: [],
+ adjustments: [],
+ credit_cards: [],
+ permissions: { can_update: true }
+})
diff --git a/guides/src/data/orders.js b/guides/src/data/orders.js
new file mode 100644
index 00000000000..174b459f1ec
--- /dev/null
+++ b/guides/src/data/orders.js
@@ -0,0 +1,8 @@
+import ORDER from './orders'
+
+export default {
+ orders: [ORDER],
+ count: 25,
+ current_page: 1,
+ pages: 5
+}
diff --git a/guides/src/data/payment.js b/guides/src/data/payment.js
new file mode 100644
index 00000000000..ff041260968
--- /dev/null
+++ b/guides/src/data/payment.js
@@ -0,0 +1,13 @@
+export default {
+ id: 1,
+ source_type: 'Spree::CreditCard',
+ source_id: 1,
+ amount: '10.00',
+ display_amount: '$10.00',
+ payment_method_id: 1,
+ state: 'checkout',
+ avs_response: null,
+ created_at: '2012-10-24T23:26:23Z',
+ updated_at: '2012-10-24T23:26:23Z',
+ number: 'P58PJCXG'
+}
diff --git a/guides/src/data/payments.js b/guides/src/data/payments.js
new file mode 100644
index 00000000000..b9b14bc7b7c
--- /dev/null
+++ b/guides/src/data/payments.js
@@ -0,0 +1,3 @@
+import PAYMENT from './payments'
+
+export default { payments: [PAYMENT], count: 2, current_page: 1, pages: 2 }
diff --git a/guides/src/data/product.js b/guides/src/data/product.js
new file mode 100644
index 00000000000..c953d3fb47c
--- /dev/null
+++ b/guides/src/data/product.js
@@ -0,0 +1,27 @@
+import * as R from 'ramda'
+
+import VARIANT from './variant'
+import OPTION_TYPE from './option_type'
+import PRODUCT_PROPERTY from './product_property'
+import CLASSIFICATION from './classification'
+
+export default {
+ id: 1,
+ name: 'Example product',
+ description: 'Description',
+ price: '15.99',
+ display_price: '$15.99',
+ available_on: '2012-10-17T03:43:57Z',
+ slug: 'example-product',
+ meta_description: null,
+ meta_keywords: null,
+ shipping_category_id: 1,
+ taxon_ids: [1, 2, 3],
+ total_on_hand: 10,
+ master: R.merge(VARIANT, { is_master: true }),
+ variants: [R.merge(VARIANT, { is_master: false })],
+ option_types: [OPTION_TYPE],
+ product_properties: [PRODUCT_PROPERTY],
+ classifications: [CLASSIFICATION],
+ has_variants: true
+}
diff --git a/guides/src/data/product_properties.js b/guides/src/data/product_properties.js
new file mode 100644
index 00000000000..f32301b3892
--- /dev/null
+++ b/guides/src/data/product_properties.js
@@ -0,0 +1,8 @@
+import PRODUCT_PROPERTY from './product_property'
+
+export default {
+ product_properties: [PRODUCT_PROPERTY],
+ count: 10,
+ current_page: 1,
+ pages: 2
+}
diff --git a/guides/src/data/product_property.js b/guides/src/data/product_property.js
new file mode 100644
index 00000000000..0d6e2025608
--- /dev/null
+++ b/guides/src/data/product_property.js
@@ -0,0 +1,7 @@
+export default {
+ id: 1,
+ product_id: 1,
+ property_id: 1,
+ value: 'Tote',
+ property_name: 'bag_type'
+}
diff --git a/guides/src/data/products.js b/guides/src/data/products.js
new file mode 100644
index 00000000000..d6e36d4184e
--- /dev/null
+++ b/guides/src/data/products.js
@@ -0,0 +1,3 @@
+import PRODUCT from './product'
+
+export default { products: [PRODUCT], count: 25, pages: 5, current_page: 1 }
diff --git a/guides/src/data/return_authorization.js b/guides/src/data/return_authorization.js
new file mode 100644
index 00000000000..c82cd724f99
--- /dev/null
+++ b/guides/src/data/return_authorization.js
@@ -0,0 +1,9 @@
+export default {
+ id: 1,
+ number: 'RA010584183',
+ state: 'authorized',
+ order_id: 14,
+ memo: "Didn't fit",
+ created_at: '2012-10-24T23:26:23Z',
+ updated_at: '2012-10-24T23:26:23Z'
+}
diff --git a/guides/src/data/return_authorizations.js b/guides/src/data/return_authorizations.js
new file mode 100644
index 00000000000..5ca6aa5bb88
--- /dev/null
+++ b/guides/src/data/return_authorizations.js
@@ -0,0 +1,8 @@
+import RETURN_AUTHORIZATION from './return_authorization'
+
+export default {
+ return_authorizations: [RETURN_AUTHORIZATION],
+ count: 2,
+ current_page: 1,
+ pages: 1
+}
diff --git a/guides/src/data/secondary_taxon.js b/guides/src/data/secondary_taxon.js
new file mode 100644
index 00000000000..0f500e1e6b0
--- /dev/null
+++ b/guides/src/data/secondary_taxon.js
@@ -0,0 +1,11 @@
+export default {
+ id: 3,
+ name: 'T-Shirts',
+ pretty_name: 'T-Shirts',
+ permalink: 'brands/t-shirts',
+ parent_id: 1,
+ taxonomy_id: 1,
+ meta_title: 'T-Shirts',
+ meta_description: 'T-Shirts',
+ taxons: []
+}
diff --git a/guides/src/data/shipment.js b/guides/src/data/shipment.js
new file mode 100644
index 00000000000..91ee540ec68
--- /dev/null
+++ b/guides/src/data/shipment.js
@@ -0,0 +1,25 @@
+import * as R from 'ramda'
+
+import SHIPPING_RATE from './shipping_rate'
+import INVENTORY_UNIT from './inventory_unit'
+import ORDER from './order'
+import ADDRESS from './address'
+import PAYMENT from './payment'
+
+export default {
+ id: 1,
+ tracking: null,
+ number: 'H123456789',
+ cost: '5.0',
+ shipped_at: null,
+ state: 'pending',
+ selected_shipping_rate: SHIPPING_RATE,
+ inventory_units: [INVENTORY_UNIT],
+ order: R.merge(ORDER, {
+ state: 'payment',
+ bill_address: ADDRESS,
+ ship_address: ADDRESS,
+ adjustments: [],
+ payments: [PAYMENT]
+ })
+}
diff --git a/guides/src/data/shipment_small.js b/guides/src/data/shipment_small.js
new file mode 100644
index 00000000000..98739f83b09
--- /dev/null
+++ b/guides/src/data/shipment_small.js
@@ -0,0 +1,18 @@
+import SHIPPING_RATE from './shipping_rate'
+import SHIPPING_METHOD from './shipping_method'
+import MANIFEST from './manifest'
+
+export default {
+ id: 1,
+ tracking: null,
+ number: 'H71047039332',
+ cost: '5.0',
+ shipped_at: null,
+ state: 'pending',
+ shipping_rates: [SHIPPING_RATE],
+ selected_shipping_rate: [SHIPPING_RATE],
+ shipping_methods: [SHIPPING_METHOD],
+ manifest: [MANIFEST],
+ order_id: 1,
+ stock_location_name: 'default'
+}
diff --git a/guides/src/data/shipments.js b/guides/src/data/shipments.js
new file mode 100644
index 00000000000..5a98a505ad1
--- /dev/null
+++ b/guides/src/data/shipments.js
@@ -0,0 +1,3 @@
+import SHIPMENT from './shipment'
+
+export default [SHIPMENT]
diff --git a/guides/src/data/shipping_category.js b/guides/src/data/shipping_category.js
new file mode 100644
index 00000000000..ce2fe6bef5e
--- /dev/null
+++ b/guides/src/data/shipping_category.js
@@ -0,0 +1,4 @@
+export default {
+ id: 1,
+ name: 'Default category'
+}
diff --git a/guides/src/data/shipping_method.js b/guides/src/data/shipping_method.js
new file mode 100644
index 00000000000..ffa176e6cc1
--- /dev/null
+++ b/guides/src/data/shipping_method.js
@@ -0,0 +1,10 @@
+import ZONE from './zone'
+import SHIPPING_CATEGORY from './shipping_category'
+
+export default {
+ id: 1,
+ code: null,
+ name: 'UPS Ground',
+ zones: [ZONE],
+ shipping_categories: [SHIPPING_CATEGORY]
+}
diff --git a/guides/src/data/shipping_rate.js b/guides/src/data/shipping_rate.js
new file mode 100644
index 00000000000..bf276c2cd0a
--- /dev/null
+++ b/guides/src/data/shipping_rate.js
@@ -0,0 +1,9 @@
+export default {
+ id: 1,
+ name: 'UPS Ground (USD)',
+ cost: 5,
+ selected: true,
+ shipping_method_id: 5,
+ shipping_method_code: null,
+ display_cost: '$5.00'
+}
diff --git a/guides/src/data/state.js b/guides/src/data/state.js
new file mode 100644
index 00000000000..a7ee267b22d
--- /dev/null
+++ b/guides/src/data/state.js
@@ -0,0 +1,6 @@
+export default {
+ id: 1,
+ name: 'New York',
+ abbr: 'NY',
+ country_id: 1
+}
diff --git a/guides/src/data/states.js b/guides/src/data/states.js
new file mode 100644
index 00000000000..9516808ec62
--- /dev/null
+++ b/guides/src/data/states.js
@@ -0,0 +1,3 @@
+import STATE from './state'
+
+export default { states: [STATE], count: 25, pages: 5, current_page: 1 }
diff --git a/guides/src/data/status.js b/guides/src/data/status.js
new file mode 100644
index 00000000000..17ee9451411
--- /dev/null
+++ b/guides/src/data/status.js
@@ -0,0 +1,16 @@
+export default {
+ 200: '200 OK',
+ 201: '201 Created',
+ 202: '202 Accepted',
+ 204: '204 No Content',
+ 301: '301 Moved Permanently',
+ 302: '302 Found',
+ 307: '307 Temporary Redirect',
+ 304: '304 Not Modified',
+ 401: '401 Unauthorized',
+ 403: '403 Forbidden',
+ 404: '404 Not Found',
+ 409: '409 Conflict',
+ 422: '422 Unprocessable Entity',
+ 500: '500 Server Error'
+}
diff --git a/guides/src/data/stock_item.js b/guides/src/data/stock_item.js
new file mode 100644
index 00000000000..6cbe568b418
--- /dev/null
+++ b/guides/src/data/stock_item.js
@@ -0,0 +1,10 @@
+import VARIANT from './variant'
+
+export default {
+ id: 1,
+ count_on_hand: 10,
+ backorderable: true,
+ stock_location_id: 1,
+ variant_id: 1,
+ variant: VARIANT
+}
diff --git a/guides/src/data/stock_items.js b/guides/src/data/stock_items.js
new file mode 100644
index 00000000000..675b9797889
--- /dev/null
+++ b/guides/src/data/stock_items.js
@@ -0,0 +1,8 @@
+import STOCK_ITEM from './stock_item'
+
+export default {
+ stock_items: [STOCK_ITEM],
+ count: 25,
+ current_page: 1,
+ pages: 5
+}
diff --git a/guides/src/data/stock_location.js b/guides/src/data/stock_location.js
new file mode 100644
index 00000000000..7d06d2d021e
--- /dev/null
+++ b/guides/src/data/stock_location.js
@@ -0,0 +1,18 @@
+import COUNTRY from './country'
+import STATE from './state'
+
+export default {
+ id: 1,
+ name: 'default',
+ address1: '7735 Old Georgetown Road',
+ address2: 'Suite 510',
+ city: 'Bethesda',
+ state_id: 1,
+ state_name: null,
+ country_id: 1,
+ zipcode: '20814',
+ phone: '',
+ active: true,
+ country: COUNTRY,
+ state: STATE
+}
diff --git a/guides/src/data/stock_locations.js b/guides/src/data/stock_locations.js
new file mode 100644
index 00000000000..f65dc86847b
--- /dev/null
+++ b/guides/src/data/stock_locations.js
@@ -0,0 +1,8 @@
+import STOCK_LOCATION from './stock_location'
+
+export default {
+ stock_locations: [STOCK_LOCATION],
+ count: 5,
+ current_page: 1,
+ pages: 1
+}
diff --git a/guides/src/data/stock_movement.js b/guides/src/data/stock_movement.js
new file mode 100644
index 00000000000..12873a3350f
--- /dev/null
+++ b/guides/src/data/stock_movement.js
@@ -0,0 +1,8 @@
+import STOCK_ITEM from './stock_item'
+
+export default {
+ id: 1,
+ quantity: 10,
+ stock_item_id: 1,
+ stock_item: STOCK_ITEM
+}
diff --git a/guides/src/data/stock_movements.js b/guides/src/data/stock_movements.js
new file mode 100644
index 00000000000..e2b9f65c1eb
--- /dev/null
+++ b/guides/src/data/stock_movements.js
@@ -0,0 +1,8 @@
+import STOCK_MOVEMENT from './stock_movement'
+
+export default {
+ stock_movements: [STOCK_MOVEMENT],
+ count: 25,
+ current_page: 1,
+ pages: 5
+}
diff --git a/guides/src/data/taxon.js b/guides/src/data/taxon.js
new file mode 100644
index 00000000000..496e6c02fe7
--- /dev/null
+++ b/guides/src/data/taxon.js
@@ -0,0 +1,11 @@
+export default {
+ id: 2,
+ name: 'Ruby on Rails',
+ pretty_name: 'Ruby on Rails',
+ permalink: 'brands/ruby-on-rails',
+ parent_id: 1,
+ taxonomy_id: 1,
+ meta_title: 'Ruby on Rails',
+ meta_description: 'Ruby on Rails',
+ taxons: []
+}
diff --git a/guides/src/data/taxon_with_children.js b/guides/src/data/taxon_with_children.js
new file mode 100644
index 00000000000..ac338f6276c
--- /dev/null
+++ b/guides/src/data/taxon_with_children.js
@@ -0,0 +1,6 @@
+import * as R from 'ramda'
+
+import TAXON from './taxon'
+import SECONDARY_TAXON from './secondary_taxon'
+
+export default R.merge(TAXON, { taxons: [SECONDARY_TAXON] })
diff --git a/guides/src/data/taxonomies.js b/guides/src/data/taxonomies.js
new file mode 100644
index 00000000000..4427b369bd6
--- /dev/null
+++ b/guides/src/data/taxonomies.js
@@ -0,0 +1,3 @@
+import TAXONOMY from './taxonomy'
+
+export default { taxonomies: [TAXONOMY], count: 25, current_page: 1, pages: 5 }
diff --git a/guides/src/data/taxonomy.js b/guides/src/data/taxonomy.js
new file mode 100644
index 00000000000..0cf348e9a98
--- /dev/null
+++ b/guides/src/data/taxonomy.js
@@ -0,0 +1,7 @@
+import TAXON_WITH_CHILDREN from './taxon_with_children'
+
+export default {
+ id: 1,
+ name: 'Brand',
+ root: TAXON_WITH_CHILDREN
+}
diff --git a/guides/src/data/taxons_with_children.js b/guides/src/data/taxons_with_children.js
new file mode 100644
index 00000000000..4609eb7b7b8
--- /dev/null
+++ b/guides/src/data/taxons_with_children.js
@@ -0,0 +1,10 @@
+import TAXON_WITH_CHILDREN from './taxon_with_children'
+
+export default {
+ taxons: [TAXON_WITH_CHILDREN],
+ count: 7,
+ total_count: 7,
+ current_page: 1,
+ per_page: 25,
+ pages: 1
+}
diff --git a/guides/src/data/update_request.js b/guides/src/data/update_request.js
new file mode 100644
index 00000000000..9387e739238
--- /dev/null
+++ b/guides/src/data/update_request.js
@@ -0,0 +1,3 @@
+export default {
+ message_id: ':guid'
+}
diff --git a/guides/src/data/user.js b/guides/src/data/user.js
new file mode 100644
index 00000000000..b055538e7ea
--- /dev/null
+++ b/guides/src/data/user.js
@@ -0,0 +1,8 @@
+export default {
+ id: 1,
+ email: 'spree@example.com',
+ login: 'spree@example.com',
+ spree_api_key: null,
+ created_at: 'Fri, 01 Feb 2013 20:38:57 UTC +00:00',
+ updated_at: 'Fri, 01 Feb 2013 20:38:57 UTC +00:00'
+}
diff --git a/guides/src/data/users.js b/guides/src/data/users.js
new file mode 100644
index 00000000000..7981310c54e
--- /dev/null
+++ b/guides/src/data/users.js
@@ -0,0 +1,3 @@
+import USER from './user'
+
+export default { users: [USER], count: 25, pages: 5, current_page: 1 }
diff --git a/guides/src/data/variant.js b/guides/src/data/variant.js
new file mode 100644
index 00000000000..770b64b5306
--- /dev/null
+++ b/guides/src/data/variant.js
@@ -0,0 +1,27 @@
+import OPTION_VALUE from './option_value'
+import IMAGE from './image'
+
+export default {
+ id: 1,
+ name: 'Ruby on Rails Tote',
+ sku: 'ROR-00011',
+ price: '15.99',
+ weight: null,
+ height: null,
+ width: null,
+ depth: null,
+ is_master: true,
+ slug: 'ruby-on-rails-tote',
+ description: 'A text description of the product.',
+ track_inventory: true,
+ cost_price: null,
+ option_values: [OPTION_VALUE],
+ images: [IMAGE],
+ display_price: '$15.99',
+ options_text: '(Size: small, Colour: red)',
+ in_stock: true,
+ is_backorderable: true,
+ is_orderable: true,
+ total_on_hand: 10,
+ is_destroyed: false
+}
diff --git a/guides/src/data/variant_big.js b/guides/src/data/variant_big.js
new file mode 100644
index 00000000000..02d6798aecd
--- /dev/null
+++ b/guides/src/data/variant_big.js
@@ -0,0 +1,7 @@
+import STOCK_ITEM from './stock_item'
+import VARIANT from './variant'
+
+export default {
+ ...VARIANT,
+ stock_items: [STOCK_ITEM]
+}
diff --git a/guides/src/data/variants_big.js b/guides/src/data/variants_big.js
new file mode 100644
index 00000000000..f9af371a3b7
--- /dev/null
+++ b/guides/src/data/variants_big.js
@@ -0,0 +1,9 @@
+import VARIANT_BIG from './variant_big'
+
+export default {
+ variants: [VARIANT_BIG],
+ count: 25,
+ total_count: 25,
+ current_page: 1,
+ pages: 1
+}
diff --git a/guides/src/data/zone.js b/guides/src/data/zone.js
new file mode 100644
index 00000000000..92f5899c251
--- /dev/null
+++ b/guides/src/data/zone.js
@@ -0,0 +1,8 @@
+import ZONE_MEMBER from './zone_member'
+
+export default {
+ id: 1,
+ name: 'America',
+ description: 'The US',
+ zone_members: [ZONE_MEMBER]
+}
diff --git a/guides/src/data/zone_member.js b/guides/src/data/zone_member.js
new file mode 100644
index 00000000000..2b7e41e10c1
--- /dev/null
+++ b/guides/src/data/zone_member.js
@@ -0,0 +1,5 @@
+export default {
+ id: 1,
+ zoneable_type: 'Spree::Country',
+ zoneable_id: 1
+}
diff --git a/guides/src/data/zones.js b/guides/src/data/zones.js
new file mode 100644
index 00000000000..b8be54a6d64
--- /dev/null
+++ b/guides/src/data/zones.js
@@ -0,0 +1,8 @@
+import ZONE from './zone'
+
+export default {
+ zones: [ZONE],
+ count: 25,
+ current_page: 1,
+ pages: 5
+}
diff --git a/guides/src/html.js b/guides/src/html.js
new file mode 100644
index 00000000000..85ad62b435b
--- /dev/null
+++ b/guides/src/html.js
@@ -0,0 +1,46 @@
+/* eslint-disable react/prop-types */
+import * as React from 'react'
+
+const JS_NPM_URLS = [
+ 'https://unpkg.com/docsearch.js@2.4.1/dist/cdn/docsearch.min.js'
+]
+
+export default class HTML extends React.Component {
+ render() {
+ return (
+
+
+
+
+ {JS_NPM_URLS.map(url => (
+
+ ))}
+
+
+
+
+ {this.props.headComponents}
+
+
+
+ {this.props.postBodyComponents}
+ {JS_NPM_URLS.map(url => (
+
+ ))}
+
+
+ )
+ }
+}
diff --git a/guides/src/images/developer/change_ssl_setting.png b/guides/src/images/developer/change_ssl_setting.png
new file mode 100755
index 00000000000..28d5f67cf6b
Binary files /dev/null and b/guides/src/images/developer/change_ssl_setting.png differ
diff --git a/guides/src/images/developer/core/new_stock_transfer.png b/guides/src/images/developer/core/new_stock_transfer.png
new file mode 100755
index 00000000000..9d9db6caf00
Binary files /dev/null and b/guides/src/images/developer/core/new_stock_transfer.png differ
diff --git a/guides/src/images/developer/core/payment_flow.jpg b/guides/src/images/developer/core/payment_flow.jpg
new file mode 100755
index 00000000000..d254d10e3a9
Binary files /dev/null and b/guides/src/images/developer/core/payment_flow.jpg differ
diff --git a/guides/src/images/developer/core/shipment_flow.jpg b/guides/src/images/developer/core/shipment_flow.jpg
new file mode 100755
index 00000000000..3ee0b063d0b
Binary files /dev/null and b/guides/src/images/developer/core/shipment_flow.jpg differ
diff --git a/guides/src/images/developer/core/split_shipments_checkout.png b/guides/src/images/developer/core/split_shipments_checkout.png
new file mode 100755
index 00000000000..724801dabbc
Binary files /dev/null and b/guides/src/images/developer/core/split_shipments_checkout.png differ
diff --git a/guides/src/images/developer/core/stock_movements.png b/guides/src/images/developer/core/stock_movements.png
new file mode 100755
index 00000000000..b825ef244f3
Binary files /dev/null and b/guides/src/images/developer/core/stock_movements.png differ
diff --git a/guides/src/images/developer/core/stock_transfers.png b/guides/src/images/developer/core/stock_transfers.png
new file mode 100755
index 00000000000..ef23e526283
Binary files /dev/null and b/guides/src/images/developer/core/stock_transfers.png differ
diff --git a/guides/src/images/developer/mail_server_settings.png b/guides/src/images/developer/mail_server_settings.png
new file mode 100755
index 00000000000..4c95e074894
Binary files /dev/null and b/guides/src/images/developer/mail_server_settings.png differ
diff --git a/guides/src/images/developer/new-admin-interface.png b/guides/src/images/developer/new-admin-interface.png
new file mode 100755
index 00000000000..ef22329136d
Binary files /dev/null and b/guides/src/images/developer/new-admin-interface.png differ
diff --git a/guides/src/images/developer/overview.png b/guides/src/images/developer/overview.png
new file mode 100755
index 00000000000..7b61fb9d0a0
Binary files /dev/null and b/guides/src/images/developer/overview.png differ
diff --git a/guides/src/images/developer/spree_welcome.png b/guides/src/images/developer/spree_welcome.png
new file mode 100755
index 00000000000..c7d3c07f63a
Binary files /dev/null and b/guides/src/images/developer/spree_welcome.png differ
diff --git a/guides/src/images/logo-spark.png b/guides/src/images/logo-spark.png
new file mode 100644
index 00000000000..ae5d7304f80
Binary files /dev/null and b/guides/src/images/logo-spark.png differ
diff --git a/guides/src/images/logo.png b/guides/src/images/logo.png
new file mode 100644
index 00000000000..948ef617211
Binary files /dev/null and b/guides/src/images/logo.png differ
diff --git a/guides/src/images/user/config/add_new_style.jpg b/guides/src/images/user/config/add_new_style.jpg
new file mode 100755
index 00000000000..307a3dd6686
Binary files /dev/null and b/guides/src/images/user/config/add_new_style.jpg differ
diff --git a/guides/src/images/user/config/add_reimbursement_types.jpg b/guides/src/images/user/config/add_reimbursement_types.jpg
new file mode 100755
index 00000000000..6449cb1a351
Binary files /dev/null and b/guides/src/images/user/config/add_reimbursement_types.jpg differ
diff --git a/guides/src/images/user/config/add_reimbursement_types_inside.jpg b/guides/src/images/user/config/add_reimbursement_types_inside.jpg
new file mode 100755
index 00000000000..74ed6cc101d
Binary files /dev/null and b/guides/src/images/user/config/add_reimbursement_types_inside.jpg differ
diff --git a/guides/src/images/user/config/add_taxon_to_taxon.jpg b/guides/src/images/user/config/add_taxon_to_taxon.jpg
new file mode 100755
index 00000000000..4cfca1f9c1b
Binary files /dev/null and b/guides/src/images/user/config/add_taxon_to_taxon.jpg differ
diff --git a/guides/src/images/user/config/add_taxon_to_taxonomy.jpg b/guides/src/images/user/config/add_taxon_to_taxonomy.jpg
new file mode 100755
index 00000000000..a191b2aa5c9
Binary files /dev/null and b/guides/src/images/user/config/add_taxon_to_taxonomy.jpg differ
diff --git a/guides/src/images/user/config/add_taxons_to_product.jpg b/guides/src/images/user/config/add_taxons_to_product.jpg
new file mode 100755
index 00000000000..008ba240f23
Binary files /dev/null and b/guides/src/images/user/config/add_taxons_to_product.jpg differ
diff --git a/guides/src/images/user/config/amazon_access_keys.jpg b/guides/src/images/user/config/amazon_access_keys.jpg
new file mode 100755
index 00000000000..8bc5d45aa7c
Binary files /dev/null and b/guides/src/images/user/config/amazon_access_keys.jpg differ
diff --git a/guides/src/images/user/config/amazons3.jpg b/guides/src/images/user/config/amazons3.jpg
new file mode 100755
index 00000000000..bf66080f921
Binary files /dev/null and b/guides/src/images/user/config/amazons3.jpg differ
diff --git a/guides/src/images/user/config/auto_capture_payment_method.jpg b/guides/src/images/user/config/auto_capture_payment_method.jpg
new file mode 100755
index 00000000000..e79901070aa
Binary files /dev/null and b/guides/src/images/user/config/auto_capture_payment_method.jpg differ
diff --git a/guides/src/images/user/config/category_delete_icon.jpg b/guides/src/images/user/config/category_delete_icon.jpg
new file mode 100755
index 00000000000..791727590fd
Binary files /dev/null and b/guides/src/images/user/config/category_delete_icon.jpg differ
diff --git a/guides/src/images/user/config/category_edit_icon.jpg b/guides/src/images/user/config/category_edit_icon.jpg
new file mode 100755
index 00000000000..67e68aaf02e
Binary files /dev/null and b/guides/src/images/user/config/category_edit_icon.jpg differ
diff --git a/guides/src/images/user/config/choose_currency.jpg b/guides/src/images/user/config/choose_currency.jpg
new file mode 100755
index 00000000000..1c139ea6b76
Binary files /dev/null and b/guides/src/images/user/config/choose_currency.jpg differ
diff --git a/guides/src/images/user/config/complex_taxonomy_tree.jpg b/guides/src/images/user/config/complex_taxonomy_tree.jpg
new file mode 100755
index 00000000000..8c0aac92971
Binary files /dev/null and b/guides/src/images/user/config/complex_taxonomy_tree.jpg differ
diff --git a/guides/src/images/user/config/countries.jpg b/guides/src/images/user/config/countries.jpg
new file mode 100755
index 00000000000..70cded52035
Binary files /dev/null and b/guides/src/images/user/config/countries.jpg differ
diff --git a/guides/src/images/user/config/countries_drop_down.jpg b/guides/src/images/user/config/countries_drop_down.jpg
new file mode 100755
index 00000000000..11b23771bc8
Binary files /dev/null and b/guides/src/images/user/config/countries_drop_down.jpg differ
diff --git a/guides/src/images/user/config/currency_settings.jpg b/guides/src/images/user/config/currency_settings.jpg
new file mode 100755
index 00000000000..b6fc5bbdcde
Binary files /dev/null and b/guides/src/images/user/config/currency_settings.jpg differ
diff --git a/guides/src/images/user/config/delete_role_icon.jpg b/guides/src/images/user/config/delete_role_icon.jpg
new file mode 100755
index 00000000000..5594495ba6d
Binary files /dev/null and b/guides/src/images/user/config/delete_role_icon.jpg differ
diff --git a/guides/src/images/user/config/delete_stock_location_icon.jpg b/guides/src/images/user/config/delete_stock_location_icon.jpg
new file mode 100755
index 00000000000..eddff7e5af6
Binary files /dev/null and b/guides/src/images/user/config/delete_stock_location_icon.jpg differ
diff --git a/guides/src/images/user/config/delete_style.jpg b/guides/src/images/user/config/delete_style.jpg
new file mode 100755
index 00000000000..0cdcfc471a8
Binary files /dev/null and b/guides/src/images/user/config/delete_style.jpg differ
diff --git a/guides/src/images/user/config/delete_tax_category_link.jpg b/guides/src/images/user/config/delete_tax_category_link.jpg
new file mode 100755
index 00000000000..42c1b120831
Binary files /dev/null and b/guides/src/images/user/config/delete_tax_category_link.jpg differ
diff --git a/guides/src/images/user/config/delete_taxonomy_icon.jpg b/guides/src/images/user/config/delete_taxonomy_icon.jpg
new file mode 100755
index 00000000000..f1d2a6f1fec
Binary files /dev/null and b/guides/src/images/user/config/delete_taxonomy_icon.jpg differ
diff --git a/guides/src/images/user/config/deleting_state_icon.jpg b/guides/src/images/user/config/deleting_state_icon.jpg
new file mode 100755
index 00000000000..455f4a52d53
Binary files /dev/null and b/guides/src/images/user/config/deleting_state_icon.jpg differ
diff --git a/guides/src/images/user/config/edit_country_icon.jpg b/guides/src/images/user/config/edit_country_icon.jpg
new file mode 100755
index 00000000000..8a4ddb15f1b
Binary files /dev/null and b/guides/src/images/user/config/edit_country_icon.jpg differ
diff --git a/guides/src/images/user/config/edit_role_icon.jpg b/guides/src/images/user/config/edit_role_icon.jpg
new file mode 100755
index 00000000000..d8a579c5392
Binary files /dev/null and b/guides/src/images/user/config/edit_role_icon.jpg differ
diff --git a/guides/src/images/user/config/edit_state_icon.jpg b/guides/src/images/user/config/edit_state_icon.jpg
new file mode 100755
index 00000000000..c8cf854af92
Binary files /dev/null and b/guides/src/images/user/config/edit_state_icon.jpg differ
diff --git a/guides/src/images/user/config/edit_stock_location_icon.jpg b/guides/src/images/user/config/edit_stock_location_icon.jpg
new file mode 100755
index 00000000000..61efd3d060d
Binary files /dev/null and b/guides/src/images/user/config/edit_stock_location_icon.jpg differ
diff --git a/guides/src/images/user/config/edit_tax_category_link.jpg b/guides/src/images/user/config/edit_tax_category_link.jpg
new file mode 100755
index 00000000000..4f4d22bedf5
Binary files /dev/null and b/guides/src/images/user/config/edit_tax_category_link.jpg differ
diff --git a/guides/src/images/user/config/edit_taxon.jpg b/guides/src/images/user/config/edit_taxon.jpg
new file mode 100755
index 00000000000..dabe3820762
Binary files /dev/null and b/guides/src/images/user/config/edit_taxon.jpg differ
diff --git a/guides/src/images/user/config/edit_taxonomy.jpg b/guides/src/images/user/config/edit_taxonomy.jpg
new file mode 100755
index 00000000000..1835532cde4
Binary files /dev/null and b/guides/src/images/user/config/edit_taxonomy.jpg differ
diff --git a/guides/src/images/user/config/edit_taxonomy_icon.jpg b/guides/src/images/user/config/edit_taxonomy_icon.jpg
new file mode 100755
index 00000000000..4e352656cad
Binary files /dev/null and b/guides/src/images/user/config/edit_taxonomy_icon.jpg differ
diff --git a/guides/src/images/user/config/editing_country.jpg b/guides/src/images/user/config/editing_country.jpg
new file mode 100755
index 00000000000..dd6e1073be6
Binary files /dev/null and b/guides/src/images/user/config/editing_country.jpg differ
diff --git a/guides/src/images/user/config/editing_state.jpg b/guides/src/images/user/config/editing_state.jpg
new file mode 100755
index 00000000000..961036263c4
Binary files /dev/null and b/guides/src/images/user/config/editing_state.jpg differ
diff --git a/guides/src/images/user/config/general_settings.jpg b/guides/src/images/user/config/general_settings.jpg
new file mode 100755
index 00000000000..4b1f5818ae6
Binary files /dev/null and b/guides/src/images/user/config/general_settings.jpg differ
diff --git a/guides/src/images/user/config/image_settings.jpg b/guides/src/images/user/config/image_settings.jpg
new file mode 100755
index 00000000000..1de9bafd2e6
Binary files /dev/null and b/guides/src/images/user/config/image_settings.jpg differ
diff --git a/guides/src/images/user/config/mail_method_settings.jpg b/guides/src/images/user/config/mail_method_settings.jpg
new file mode 100755
index 00000000000..7248373f11d
Binary files /dev/null and b/guides/src/images/user/config/mail_method_settings.jpg differ
diff --git a/guides/src/images/user/config/new_node.jpg b/guides/src/images/user/config/new_node.jpg
new file mode 100755
index 00000000000..8e551bff37f
Binary files /dev/null and b/guides/src/images/user/config/new_node.jpg differ
diff --git a/guides/src/images/user/config/new_rma_reason.jpg b/guides/src/images/user/config/new_rma_reason.jpg
new file mode 100755
index 00000000000..2962e53d6e7
Binary files /dev/null and b/guides/src/images/user/config/new_rma_reason.jpg differ
diff --git a/guides/src/images/user/config/new_rma_reason_created.jpg b/guides/src/images/user/config/new_rma_reason_created.jpg
new file mode 100755
index 00000000000..d4632c04fb3
Binary files /dev/null and b/guides/src/images/user/config/new_rma_reason_created.jpg differ
diff --git a/guides/src/images/user/config/new_role_button.jpg b/guides/src/images/user/config/new_role_button.jpg
new file mode 100755
index 00000000000..32e19be6239
Binary files /dev/null and b/guides/src/images/user/config/new_role_button.jpg differ
diff --git a/guides/src/images/user/config/new_role_inside.jpg b/guides/src/images/user/config/new_role_inside.jpg
new file mode 100755
index 00000000000..0a6ab01b0d7
Binary files /dev/null and b/guides/src/images/user/config/new_role_inside.jpg differ
diff --git a/guides/src/images/user/config/new_sc_category.jpg b/guides/src/images/user/config/new_sc_category.jpg
new file mode 100755
index 00000000000..a8b607370ff
Binary files /dev/null and b/guides/src/images/user/config/new_sc_category.jpg differ
diff --git a/guides/src/images/user/config/new_sc_category_name.jpg b/guides/src/images/user/config/new_sc_category_name.jpg
new file mode 100755
index 00000000000..0a192f23ff5
Binary files /dev/null and b/guides/src/images/user/config/new_sc_category_name.jpg differ
diff --git a/guides/src/images/user/config/new_state_form.jpg b/guides/src/images/user/config/new_state_form.jpg
new file mode 100755
index 00000000000..bd927df7c2c
Binary files /dev/null and b/guides/src/images/user/config/new_state_form.jpg differ
diff --git a/guides/src/images/user/config/new_stock_location.jpg b/guides/src/images/user/config/new_stock_location.jpg
new file mode 100755
index 00000000000..996c4e7ef3d
Binary files /dev/null and b/guides/src/images/user/config/new_stock_location.jpg differ
diff --git a/guides/src/images/user/config/new_stock_transfer.jpg b/guides/src/images/user/config/new_stock_transfer.jpg
new file mode 100755
index 00000000000..de41d4846c0
Binary files /dev/null and b/guides/src/images/user/config/new_stock_transfer.jpg differ
diff --git a/guides/src/images/user/config/new_tax_category_form.jpg b/guides/src/images/user/config/new_tax_category_form.jpg
new file mode 100755
index 00000000000..b4147e2fc4b
Binary files /dev/null and b/guides/src/images/user/config/new_tax_category_form.jpg differ
diff --git a/guides/src/images/user/config/new_tax_rate.jpg b/guides/src/images/user/config/new_tax_rate.jpg
new file mode 100755
index 00000000000..5b0c33dc9c7
Binary files /dev/null and b/guides/src/images/user/config/new_tax_rate.jpg differ
diff --git a/guides/src/images/user/config/new_taxon.jpg b/guides/src/images/user/config/new_taxon.jpg
new file mode 100755
index 00000000000..c69264843da
Binary files /dev/null and b/guides/src/images/user/config/new_taxon.jpg differ
diff --git a/guides/src/images/user/config/new_taxonomy.jpg b/guides/src/images/user/config/new_taxonomy.jpg
new file mode 100755
index 00000000000..1c827f9959e
Binary files /dev/null and b/guides/src/images/user/config/new_taxonomy.jpg differ
diff --git a/guides/src/images/user/config/parent_into_parent_taxon_merge.jpg b/guides/src/images/user/config/parent_into_parent_taxon_merge.jpg
new file mode 100755
index 00000000000..9569e0e7140
Binary files /dev/null and b/guides/src/images/user/config/parent_into_parent_taxon_merge.jpg differ
diff --git a/guides/src/images/user/config/reimbursement_delete_icon.jpg b/guides/src/images/user/config/reimbursement_delete_icon.jpg
new file mode 100755
index 00000000000..56890341607
Binary files /dev/null and b/guides/src/images/user/config/reimbursement_delete_icon.jpg differ
diff --git a/guides/src/images/user/config/reimbursement_edit_icon.jpg b/guides/src/images/user/config/reimbursement_edit_icon.jpg
new file mode 100755
index 00000000000..326fd157a14
Binary files /dev/null and b/guides/src/images/user/config/reimbursement_edit_icon.jpg differ
diff --git a/guides/src/images/user/config/reimbursement_types.jpg b/guides/src/images/user/config/reimbursement_types.jpg
new file mode 100755
index 00000000000..f2d085074b4
Binary files /dev/null and b/guides/src/images/user/config/reimbursement_types.jpg differ
diff --git a/guides/src/images/user/config/reimbursement_types_dropdown.jpg b/guides/src/images/user/config/reimbursement_types_dropdown.jpg
new file mode 100755
index 00000000000..619b6a09674
Binary files /dev/null and b/guides/src/images/user/config/reimbursement_types_dropdown.jpg differ
diff --git a/guides/src/images/user/config/remove_taxon.jpg b/guides/src/images/user/config/remove_taxon.jpg
new file mode 100755
index 00000000000..91ff5e076c5
Binary files /dev/null and b/guides/src/images/user/config/remove_taxon.jpg differ
diff --git a/guides/src/images/user/config/reorder_taxons.jpg b/guides/src/images/user/config/reorder_taxons.jpg
new file mode 100755
index 00000000000..b846351d725
Binary files /dev/null and b/guides/src/images/user/config/reorder_taxons.jpg differ
diff --git a/guides/src/images/user/config/resulting_stock_movements.jpg b/guides/src/images/user/config/resulting_stock_movements.jpg
new file mode 100755
index 00000000000..033a8c091d6
Binary files /dev/null and b/guides/src/images/user/config/resulting_stock_movements.jpg differ
diff --git a/guides/src/images/user/config/return_autho_reasons.jpg b/guides/src/images/user/config/return_autho_reasons.jpg
new file mode 100755
index 00000000000..6e84b6202b5
Binary files /dev/null and b/guides/src/images/user/config/return_autho_reasons.jpg differ
diff --git a/guides/src/images/user/config/rma_reason_delete_icon.jpg b/guides/src/images/user/config/rma_reason_delete_icon.jpg
new file mode 100755
index 00000000000..c83cc122ecb
Binary files /dev/null and b/guides/src/images/user/config/rma_reason_delete_icon.jpg differ
diff --git a/guides/src/images/user/config/rma_reason_edit_icon.jpg b/guides/src/images/user/config/rma_reason_edit_icon.jpg
new file mode 100755
index 00000000000..974a37d32e5
Binary files /dev/null and b/guides/src/images/user/config/rma_reason_edit_icon.jpg differ
diff --git a/guides/src/images/user/config/rma_reason_edit_inside.jpg b/guides/src/images/user/config/rma_reason_edit_inside.jpg
new file mode 100755
index 00000000000..a232f2e5c99
Binary files /dev/null and b/guides/src/images/user/config/rma_reason_edit_inside.jpg differ
diff --git a/guides/src/images/user/config/seo_title_override.jpg b/guides/src/images/user/config/seo_title_override.jpg
new file mode 100755
index 00000000000..dd5f4fec0ff
Binary files /dev/null and b/guides/src/images/user/config/seo_title_override.jpg differ
diff --git a/guides/src/images/user/config/show_currency.jpg b/guides/src/images/user/config/show_currency.jpg
new file mode 100755
index 00000000000..4a43f5b8c27
Binary files /dev/null and b/guides/src/images/user/config/show_currency.jpg differ
diff --git a/guides/src/images/user/config/site_name_in_title.jpg b/guides/src/images/user/config/site_name_in_title.jpg
new file mode 100755
index 00000000000..96ef7966c9a
Binary files /dev/null and b/guides/src/images/user/config/site_name_in_title.jpg differ
diff --git a/guides/src/images/user/config/state_added.jpg b/guides/src/images/user/config/state_added.jpg
new file mode 100755
index 00000000000..a76b60aaf8e
Binary files /dev/null and b/guides/src/images/user/config/state_added.jpg differ
diff --git a/guides/src/images/user/config/stock_movements_link.jpg b/guides/src/images/user/config/stock_movements_link.jpg
new file mode 100755
index 00000000000..038b32cb4dc
Binary files /dev/null and b/guides/src/images/user/config/stock_movements_link.jpg differ
diff --git a/guides/src/images/user/config/stock_transfer.jpg b/guides/src/images/user/config/stock_transfer.jpg
new file mode 100755
index 00000000000..43eeb19601a
Binary files /dev/null and b/guides/src/images/user/config/stock_transfer.jpg differ
diff --git a/guides/src/images/user/config/stock_transfer_complete.jpg b/guides/src/images/user/config/stock_transfer_complete.jpg
new file mode 100755
index 00000000000..84b1d8d9dc6
Binary files /dev/null and b/guides/src/images/user/config/stock_transfer_complete.jpg differ
diff --git a/guides/src/images/user/config/store_credit_categories.jpg b/guides/src/images/user/config/store_credit_categories.jpg
new file mode 100755
index 00000000000..1ef7249a333
Binary files /dev/null and b/guides/src/images/user/config/store_credit_categories.jpg differ
diff --git a/guides/src/images/user/config/tax_categories.jpg b/guides/src/images/user/config/tax_categories.jpg
new file mode 100755
index 00000000000..a5e3bb0d9c6
Binary files /dev/null and b/guides/src/images/user/config/tax_categories.jpg differ
diff --git a/guides/src/images/user/config/tax_rates.jpg b/guides/src/images/user/config/tax_rates.jpg
new file mode 100755
index 00000000000..6b950f8cda7
Binary files /dev/null and b/guides/src/images/user/config/tax_rates.jpg differ
diff --git a/guides/src/images/user/config/tax_settings.jpg b/guides/src/images/user/config/tax_settings.jpg
new file mode 100755
index 00000000000..03bbd45d1b5
Binary files /dev/null and b/guides/src/images/user/config/tax_settings.jpg differ
diff --git a/guides/src/images/user/config/taxonomy_tree.jpg b/guides/src/images/user/config/taxonomy_tree.jpg
new file mode 100755
index 00000000000..dbc859cfc68
Binary files /dev/null and b/guides/src/images/user/config/taxonomy_tree.jpg differ
diff --git a/guides/src/images/user/config/us_states_list.jpg b/guides/src/images/user/config/us_states_list.jpg
new file mode 100755
index 00000000000..86512232335
Binary files /dev/null and b/guides/src/images/user/config/us_states_list.jpg differ
diff --git a/guides/src/images/user/orders/close_adjustment_icon.jpg b/guides/src/images/user/orders/close_adjustment_icon.jpg
new file mode 100755
index 00000000000..8bd7f784f7f
Binary files /dev/null and b/guides/src/images/user/orders/close_adjustment_icon.jpg differ
diff --git a/guides/src/images/user/orders/closed_adjustment.jpg b/guides/src/images/user/orders/closed_adjustment.jpg
new file mode 100755
index 00000000000..17e686191af
Binary files /dev/null and b/guides/src/images/user/orders/closed_adjustment.jpg differ
diff --git a/guides/src/images/user/orders/completed_payment.jpg b/guides/src/images/user/orders/completed_payment.jpg
new file mode 100755
index 00000000000..8b19138d179
Binary files /dev/null and b/guides/src/images/user/orders/completed_payment.jpg differ
diff --git a/guides/src/images/user/orders/create_new_order.jpg b/guides/src/images/user/orders/create_new_order.jpg
new file mode 100755
index 00000000000..caff1b517b8
Binary files /dev/null and b/guides/src/images/user/orders/create_new_order.jpg differ
diff --git a/guides/src/images/user/orders/create_reimbursement_button.jpg b/guides/src/images/user/orders/create_reimbursement_button.jpg
new file mode 100755
index 00000000000..96f0698bc81
Binary files /dev/null and b/guides/src/images/user/orders/create_reimbursement_button.jpg differ
diff --git a/guides/src/images/user/orders/customer_return_form.jpg b/guides/src/images/user/orders/customer_return_form.jpg
new file mode 100755
index 00000000000..5537f6e666d
Binary files /dev/null and b/guides/src/images/user/orders/customer_return_form.jpg differ
diff --git a/guides/src/images/user/orders/customer_return_link.jpg b/guides/src/images/user/orders/customer_return_link.jpg
new file mode 100755
index 00000000000..375d6436938
Binary files /dev/null and b/guides/src/images/user/orders/customer_return_link.jpg differ
diff --git a/guides/src/images/user/orders/delete_adjustment_icon.jpg b/guides/src/images/user/orders/delete_adjustment_icon.jpg
new file mode 100755
index 00000000000..b17acc73a4d
Binary files /dev/null and b/guides/src/images/user/orders/delete_adjustment_icon.jpg differ
diff --git a/guides/src/images/user/orders/edit_adjustment_icon.jpg b/guides/src/images/user/orders/edit_adjustment_icon.jpg
new file mode 100755
index 00000000000..930b914db7e
Binary files /dev/null and b/guides/src/images/user/orders/edit_adjustment_icon.jpg differ
diff --git a/guides/src/images/user/orders/edit_order_link.jpg b/guides/src/images/user/orders/edit_order_link.jpg
new file mode 100755
index 00000000000..d73fb253a90
Binary files /dev/null and b/guides/src/images/user/orders/edit_order_link.jpg differ
diff --git a/guides/src/images/user/orders/edit_shipping_on_order_link.jpg b/guides/src/images/user/orders/edit_shipping_on_order_link.jpg
new file mode 100755
index 00000000000..95cfd180d73
Binary files /dev/null and b/guides/src/images/user/orders/edit_shipping_on_order_link.jpg differ
diff --git a/guides/src/images/user/orders/edit_shipping_options.jpg b/guides/src/images/user/orders/edit_shipping_options.jpg
new file mode 100755
index 00000000000..4e75e195347
Binary files /dev/null and b/guides/src/images/user/orders/edit_shipping_options.jpg differ
diff --git a/guides/src/images/user/orders/filter_options.jpg b/guides/src/images/user/orders/filter_options.jpg
new file mode 100755
index 00000000000..c03b7305685
Binary files /dev/null and b/guides/src/images/user/orders/filter_options.jpg differ
diff --git a/guides/src/images/user/orders/list_of_orders.jpg b/guides/src/images/user/orders/list_of_orders.jpg
new file mode 100755
index 00000000000..48a0c3ebf96
Binary files /dev/null and b/guides/src/images/user/orders/list_of_orders.jpg differ
diff --git a/guides/src/images/user/orders/manual_order_with_product.jpg b/guides/src/images/user/orders/manual_order_with_product.jpg
new file mode 100755
index 00000000000..2012e18d782
Binary files /dev/null and b/guides/src/images/user/orders/manual_order_with_product.jpg differ
diff --git a/guides/src/images/user/orders/mass_open_close_adjustments.jpg b/guides/src/images/user/orders/mass_open_close_adjustments.jpg
new file mode 100755
index 00000000000..dbe32d24663
Binary files /dev/null and b/guides/src/images/user/orders/mass_open_close_adjustments.jpg differ
diff --git a/guides/src/images/user/orders/new_adjustment_button.jpg b/guides/src/images/user/orders/new_adjustment_button.jpg
new file mode 100755
index 00000000000..6b5f813f3ec
Binary files /dev/null and b/guides/src/images/user/orders/new_adjustment_button.jpg differ
diff --git a/guides/src/images/user/orders/new_adjustment_form.jpg b/guides/src/images/user/orders/new_adjustment_form.jpg
new file mode 100755
index 00000000000..83ab1b6489a
Binary files /dev/null and b/guides/src/images/user/orders/new_adjustment_form.jpg differ
diff --git a/guides/src/images/user/orders/new_payment_method_link.jpg b/guides/src/images/user/orders/new_payment_method_link.jpg
new file mode 100755
index 00000000000..1293b1cec34
Binary files /dev/null and b/guides/src/images/user/orders/new_payment_method_link.jpg differ
diff --git a/guides/src/images/user/orders/open_adjustment_icon.jpg b/guides/src/images/user/orders/open_adjustment_icon.jpg
new file mode 100755
index 00000000000..d10f69d81de
Binary files /dev/null and b/guides/src/images/user/orders/open_adjustment_icon.jpg differ
diff --git a/guides/src/images/user/orders/order_adjustments.jpg b/guides/src/images/user/orders/order_adjustments.jpg
new file mode 100755
index 00000000000..0c7efff5a4a
Binary files /dev/null and b/guides/src/images/user/orders/order_adjustments.jpg differ
diff --git a/guides/src/images/user/orders/order_customer_details.jpg b/guides/src/images/user/orders/order_customer_details.jpg
new file mode 100755
index 00000000000..ad20d8ce566
Binary files /dev/null and b/guides/src/images/user/orders/order_customer_details.jpg differ
diff --git a/guides/src/images/user/orders/order_details_link.jpg b/guides/src/images/user/orders/order_details_link.jpg
new file mode 100755
index 00000000000..f5ea569c4a2
Binary files /dev/null and b/guides/src/images/user/orders/order_details_link.jpg differ
diff --git a/guides/src/images/user/orders/order_edit.jpg b/guides/src/images/user/orders/order_edit.jpg
new file mode 100755
index 00000000000..6a87c0e0038
Binary files /dev/null and b/guides/src/images/user/orders/order_edit.jpg differ
diff --git a/guides/src/images/user/orders/order_product_added.jpg b/guides/src/images/user/orders/order_product_added.jpg
new file mode 100755
index 00000000000..57677308af2
Binary files /dev/null and b/guides/src/images/user/orders/order_product_added.jpg differ
diff --git a/guides/src/images/user/orders/order_product_search.jpg b/guides/src/images/user/orders/order_product_search.jpg
new file mode 100755
index 00000000000..ad07c165701
Binary files /dev/null and b/guides/src/images/user/orders/order_product_search.jpg differ
diff --git a/guides/src/images/user/orders/order_shipped.jpg b/guides/src/images/user/orders/order_shipped.jpg
new file mode 100755
index 00000000000..d620a3a013a
Binary files /dev/null and b/guides/src/images/user/orders/order_shipped.jpg differ
diff --git a/guides/src/images/user/orders/order_to_process.jpg b/guides/src/images/user/orders/order_to_process.jpg
new file mode 100755
index 00000000000..4f8bccaa701
Binary files /dev/null and b/guides/src/images/user/orders/order_to_process.jpg differ
diff --git a/guides/src/images/user/orders/payment_to_process.jpg b/guides/src/images/user/orders/payment_to_process.jpg
new file mode 100755
index 00000000000..2ec64b2264f
Binary files /dev/null and b/guides/src/images/user/orders/payment_to_process.jpg differ
diff --git a/guides/src/images/user/orders/payments_link.jpg b/guides/src/images/user/orders/payments_link.jpg
new file mode 100755
index 00000000000..eac08364981
Binary files /dev/null and b/guides/src/images/user/orders/payments_link.jpg differ
diff --git a/guides/src/images/user/orders/reimbursement_complete.jpg b/guides/src/images/user/orders/reimbursement_complete.jpg
new file mode 100755
index 00000000000..9e333209e41
Binary files /dev/null and b/guides/src/images/user/orders/reimbursement_complete.jpg differ
diff --git a/guides/src/images/user/orders/reimbursement_form.jpg b/guides/src/images/user/orders/reimbursement_form.jpg
new file mode 100755
index 00000000000..6878caa561e
Binary files /dev/null and b/guides/src/images/user/orders/reimbursement_form.jpg differ
diff --git a/guides/src/images/user/orders/return_autho_delete.jpg b/guides/src/images/user/orders/return_autho_delete.jpg
new file mode 100755
index 00000000000..13b19925573
Binary files /dev/null and b/guides/src/images/user/orders/return_autho_delete.jpg differ
diff --git a/guides/src/images/user/orders/return_autho_edit.jpg b/guides/src/images/user/orders/return_autho_edit.jpg
new file mode 100755
index 00000000000..e6b6432d085
Binary files /dev/null and b/guides/src/images/user/orders/return_autho_edit.jpg differ
diff --git a/guides/src/images/user/orders/return_autho_inside.jpg b/guides/src/images/user/orders/return_autho_inside.jpg
new file mode 100755
index 00000000000..41385a6be2d
Binary files /dev/null and b/guides/src/images/user/orders/return_autho_inside.jpg differ
diff --git a/guides/src/images/user/orders/return_authorizations_link.jpg b/guides/src/images/user/orders/return_authorizations_link.jpg
new file mode 100755
index 00000000000..f829f14230e
Binary files /dev/null and b/guides/src/images/user/orders/return_authorizations_link.jpg differ
diff --git a/guides/src/images/user/orders/rma_form.jpg b/guides/src/images/user/orders/rma_form.jpg
new file mode 100755
index 00000000000..c3189e63d18
Binary files /dev/null and b/guides/src/images/user/orders/rma_form.jpg differ
diff --git a/guides/src/images/user/orders/select_shipping.jpg b/guides/src/images/user/orders/select_shipping.jpg
new file mode 100755
index 00000000000..e8d9b893a2d
Binary files /dev/null and b/guides/src/images/user/orders/select_shipping.jpg differ
diff --git a/guides/src/images/user/orders/ship_it.jpg b/guides/src/images/user/orders/ship_it.jpg
new file mode 100755
index 00000000000..a5b94ee9800
Binary files /dev/null and b/guides/src/images/user/orders/ship_it.jpg differ
diff --git a/guides/src/images/user/orders/tracking_input.jpg b/guides/src/images/user/orders/tracking_input.jpg
new file mode 100755
index 00000000000..61aed280424
Binary files /dev/null and b/guides/src/images/user/orders/tracking_input.jpg differ
diff --git a/guides/src/images/user/payments/add_payment_provider.jpg b/guides/src/images/user/payments/add_payment_provider.jpg
new file mode 100755
index 00000000000..1ebd6bd27b4
Binary files /dev/null and b/guides/src/images/user/payments/add_payment_provider.jpg differ
diff --git a/guides/src/images/user/payments/auto_capture_payment_method.jpg b/guides/src/images/user/payments/auto_capture_payment_method.jpg
new file mode 100755
index 00000000000..fc1b581a928
Binary files /dev/null and b/guides/src/images/user/payments/auto_capture_payment_method.jpg differ
diff --git a/guides/src/images/user/payments/edit_payment_method.jpg b/guides/src/images/user/payments/edit_payment_method.jpg
new file mode 100755
index 00000000000..b9778eb5ece
Binary files /dev/null and b/guides/src/images/user/payments/edit_payment_method.jpg differ
diff --git a/guides/src/images/user/payments/new_payment_method.jpg b/guides/src/images/user/payments/new_payment_method.jpg
new file mode 100755
index 00000000000..42432207e7b
Binary files /dev/null and b/guides/src/images/user/payments/new_payment_method.jpg differ
diff --git a/guides/src/images/user/payments/payment_capture.jpg b/guides/src/images/user/payments/payment_capture.jpg
new file mode 100755
index 00000000000..e0e1796ba79
Binary files /dev/null and b/guides/src/images/user/payments/payment_capture.jpg differ
diff --git a/guides/src/images/user/payments/payment_details.jpg b/guides/src/images/user/payments/payment_details.jpg
new file mode 100755
index 00000000000..bd778d5823f
Binary files /dev/null and b/guides/src/images/user/payments/payment_details.jpg differ
diff --git a/guides/src/images/user/payments/payment_edit_button.jpg b/guides/src/images/user/payments/payment_edit_button.jpg
new file mode 100755
index 00000000000..7ccd14a60b9
Binary files /dev/null and b/guides/src/images/user/payments/payment_edit_button.jpg differ
diff --git a/guides/src/images/user/payments/payment_look_up.jpg b/guides/src/images/user/payments/payment_look_up.jpg
new file mode 100755
index 00000000000..4487ed894e1
Binary files /dev/null and b/guides/src/images/user/payments/payment_look_up.jpg differ
diff --git a/guides/src/images/user/payments/payment_method_name.jpg b/guides/src/images/user/payments/payment_method_name.jpg
new file mode 100755
index 00000000000..a0916b4ad3c
Binary files /dev/null and b/guides/src/images/user/payments/payment_method_name.jpg differ
diff --git a/guides/src/images/user/payments/payment_void.jpg b/guides/src/images/user/payments/payment_void.jpg
new file mode 100755
index 00000000000..e067baadbea
Binary files /dev/null and b/guides/src/images/user/payments/payment_void.jpg differ
diff --git a/guides/src/images/user/payments/payments_look_up.jpg b/guides/src/images/user/payments/payments_look_up.jpg
new file mode 100755
index 00000000000..1f7cb920d7f
Binary files /dev/null and b/guides/src/images/user/payments/payments_look_up.jpg differ
diff --git a/guides/src/images/user/products/clone_deleted_product.jpg b/guides/src/images/user/products/clone_deleted_product.jpg
new file mode 100755
index 00000000000..e81712217f5
Binary files /dev/null and b/guides/src/images/user/products/clone_deleted_product.jpg differ
diff --git a/guides/src/images/user/products/clone_product.jpg b/guides/src/images/user/products/clone_product.jpg
new file mode 100755
index 00000000000..835890acc10
Binary files /dev/null and b/guides/src/images/user/products/clone_product.jpg differ
diff --git a/guides/src/images/user/products/delete_products_icon.jpg b/guides/src/images/user/products/delete_products_icon.jpg
new file mode 100755
index 00000000000..b7e7bdc2949
Binary files /dev/null and b/guides/src/images/user/products/delete_products_icon.jpg differ
diff --git a/guides/src/images/user/products/edit_cloned_product.jpg b/guides/src/images/user/products/edit_cloned_product.jpg
new file mode 100755
index 00000000000..dc9e9508112
Binary files /dev/null and b/guides/src/images/user/products/edit_cloned_product.jpg differ
diff --git a/guides/src/images/user/products/edit_product_link.jpg b/guides/src/images/user/products/edit_product_link.jpg
new file mode 100755
index 00000000000..9b933075369
Binary files /dev/null and b/guides/src/images/user/products/edit_product_link.jpg differ
diff --git a/guides/src/images/user/products/example_cloned_product.jpg b/guides/src/images/user/products/example_cloned_product.jpg
new file mode 100755
index 00000000000..ab2d128ce3a
Binary files /dev/null and b/guides/src/images/user/products/example_cloned_product.jpg differ
diff --git a/guides/src/images/user/products/filtering_products.jpg b/guides/src/images/user/products/filtering_products.jpg
new file mode 100755
index 00000000000..83e36a7ecaa
Binary files /dev/null and b/guides/src/images/user/products/filtering_products.jpg differ
diff --git a/guides/src/images/user/products/large_small_option_values.jpg b/guides/src/images/user/products/large_small_option_values.jpg
new file mode 100755
index 00000000000..13755149bc3
Binary files /dev/null and b/guides/src/images/user/products/large_small_option_values.jpg differ
diff --git a/guides/src/images/user/products/new_image_form.jpg b/guides/src/images/user/products/new_image_form.jpg
new file mode 100755
index 00000000000..3f00585a00d
Binary files /dev/null and b/guides/src/images/user/products/new_image_form.jpg differ
diff --git a/guides/src/images/user/products/new_option_form.jpg b/guides/src/images/user/products/new_option_form.jpg
new file mode 100755
index 00000000000..37b1bf3151d
Binary files /dev/null and b/guides/src/images/user/products/new_option_form.jpg differ
diff --git a/guides/src/images/user/products/new_option_type.jpg b/guides/src/images/user/products/new_option_type.jpg
new file mode 100755
index 00000000000..c88c0c1dca3
Binary files /dev/null and b/guides/src/images/user/products/new_option_type.jpg differ
diff --git a/guides/src/images/user/products/new_option_value.jpg b/guides/src/images/user/products/new_option_value.jpg
new file mode 100755
index 00000000000..132ad1232fa
Binary files /dev/null and b/guides/src/images/user/products/new_option_value.jpg differ
diff --git a/guides/src/images/user/products/new_product_entry_form.jpg b/guides/src/images/user/products/new_product_entry_form.jpg
new file mode 100755
index 00000000000..d03a6f574cf
Binary files /dev/null and b/guides/src/images/user/products/new_product_entry_form.jpg differ
diff --git a/guides/src/images/user/products/new_prototype.jpg b/guides/src/images/user/products/new_prototype.jpg
new file mode 100755
index 00000000000..be7c9132b58
Binary files /dev/null and b/guides/src/images/user/products/new_prototype.jpg differ
diff --git a/guides/src/images/user/products/new_variant.jpg b/guides/src/images/user/products/new_variant.jpg
new file mode 100755
index 00000000000..c71dde2ee8a
Binary files /dev/null and b/guides/src/images/user/products/new_variant.jpg differ
diff --git a/guides/src/images/user/products/option_types_dropdown.jpg b/guides/src/images/user/products/option_types_dropdown.jpg
new file mode 100755
index 00000000000..3e0d0ed4ad8
Binary files /dev/null and b/guides/src/images/user/products/option_types_dropdown.jpg differ
diff --git a/guides/src/images/user/products/picture_frame_prototype.jpg b/guides/src/images/user/products/picture_frame_prototype.jpg
new file mode 100755
index 00000000000..207b8e239a3
Binary files /dev/null and b/guides/src/images/user/products/picture_frame_prototype.jpg differ
diff --git a/guides/src/images/user/products/product_edit_form.jpg b/guides/src/images/user/products/product_edit_form.jpg
new file mode 100755
index 00000000000..75622f90ea3
Binary files /dev/null and b/guides/src/images/user/products/product_edit_form.jpg differ
diff --git a/guides/src/images/user/products/product_from_prototype.jpg b/guides/src/images/user/products/product_from_prototype.jpg
new file mode 100755
index 00000000000..0e1281c293f
Binary files /dev/null and b/guides/src/images/user/products/product_from_prototype.jpg differ
diff --git a/guides/src/images/user/products/products_admin.jpg b/guides/src/images/user/products/products_admin.jpg
new file mode 100755
index 00000000000..c1160ba4068
Binary files /dev/null and b/guides/src/images/user/products/products_admin.jpg differ
diff --git a/guides/src/images/user/products/products_landing.jpg b/guides/src/images/user/products/products_landing.jpg
new file mode 100755
index 00000000000..390420e2b17
Binary files /dev/null and b/guides/src/images/user/products/products_landing.jpg differ
diff --git a/guides/src/images/user/products/properties_example.jpg b/guides/src/images/user/products/properties_example.jpg
new file mode 100755
index 00000000000..bcc1176813f
Binary files /dev/null and b/guides/src/images/user/products/properties_example.jpg differ
diff --git a/guides/src/images/user/products/properties_list.jpg b/guides/src/images/user/products/properties_list.jpg
new file mode 100755
index 00000000000..8b0fd30a5ac
Binary files /dev/null and b/guides/src/images/user/products/properties_list.jpg differ
diff --git a/guides/src/images/user/products/prototype_product_with_options.jpg b/guides/src/images/user/products/prototype_product_with_options.jpg
new file mode 100755
index 00000000000..ce60bd159e2
Binary files /dev/null and b/guides/src/images/user/products/prototype_product_with_options.jpg differ
diff --git a/guides/src/images/user/products/prototypes.jpg b/guides/src/images/user/products/prototypes.jpg
new file mode 100755
index 00000000000..996d47252d3
Binary files /dev/null and b/guides/src/images/user/products/prototypes.jpg differ
diff --git a/guides/src/images/user/products/show_deleted_products.jpg b/guides/src/images/user/products/show_deleted_products.jpg
new file mode 100755
index 00000000000..8faed9e1186
Binary files /dev/null and b/guides/src/images/user/products/show_deleted_products.jpg differ
diff --git a/guides/src/images/user/products/stock_location_info.jpg b/guides/src/images/user/products/stock_location_info.jpg
new file mode 100755
index 00000000000..de92852bc99
Binary files /dev/null and b/guides/src/images/user/products/stock_location_info.jpg differ
diff --git a/guides/src/images/user/products/stock_management.jpg b/guides/src/images/user/products/stock_management.jpg
new file mode 100755
index 00000000000..dfc033655c8
Binary files /dev/null and b/guides/src/images/user/products/stock_management.jpg differ
diff --git a/guides/src/images/user/products/variants_list.jpg b/guides/src/images/user/products/variants_list.jpg
new file mode 100755
index 00000000000..7404d1b6e5f
Binary files /dev/null and b/guides/src/images/user/products/variants_list.jpg differ
diff --git a/guides/src/images/user/promotions/create_adjustment.jpg b/guides/src/images/user/promotions/create_adjustment.jpg
new file mode 100755
index 00000000000..8e950f09d74
Binary files /dev/null and b/guides/src/images/user/promotions/create_adjustment.jpg differ
diff --git a/guides/src/images/user/promotions/create_line_item.jpg b/guides/src/images/user/promotions/create_line_item.jpg
new file mode 100755
index 00000000000..91cb6db6282
Binary files /dev/null and b/guides/src/images/user/promotions/create_line_item.jpg differ
diff --git a/guides/src/images/user/promotions/delete_promotion_icon.jpg b/guides/src/images/user/promotions/delete_promotion_icon.jpg
new file mode 100755
index 00000000000..252ba6a7b4e
Binary files /dev/null and b/guides/src/images/user/promotions/delete_promotion_icon.jpg differ
diff --git a/guides/src/images/user/promotions/delete_rule_icon.jpg b/guides/src/images/user/promotions/delete_rule_icon.jpg
new file mode 100755
index 00000000000..39c4b507daf
Binary files /dev/null and b/guides/src/images/user/promotions/delete_rule_icon.jpg differ
diff --git a/guides/src/images/user/promotions/edit_promotion_icon.jpg b/guides/src/images/user/promotions/edit_promotion_icon.jpg
new file mode 100755
index 00000000000..b00509ab340
Binary files /dev/null and b/guides/src/images/user/promotions/edit_promotion_icon.jpg differ
diff --git a/guides/src/images/user/promotions/first_order_rule.jpg b/guides/src/images/user/promotions/first_order_rule.jpg
new file mode 100755
index 00000000000..3196f932b6b
Binary files /dev/null and b/guides/src/images/user/promotions/first_order_rule.jpg differ
diff --git a/guides/src/images/user/promotions/item_total_rule.jpg b/guides/src/images/user/promotions/item_total_rule.jpg
new file mode 100755
index 00000000000..44b21ee71b2
Binary files /dev/null and b/guides/src/images/user/promotions/item_total_rule.jpg differ
diff --git a/guides/src/images/user/promotions/logged_in_rule.jpg b/guides/src/images/user/promotions/logged_in_rule.jpg
new file mode 100755
index 00000000000..61ec80adfe4
Binary files /dev/null and b/guides/src/images/user/promotions/logged_in_rule.jpg differ
diff --git a/guides/src/images/user/promotions/new_promotion.jpg b/guides/src/images/user/promotions/new_promotion.jpg
new file mode 100755
index 00000000000..c1d13782d68
Binary files /dev/null and b/guides/src/images/user/promotions/new_promotion.jpg differ
diff --git a/guides/src/images/user/promotions/products_rule.jpg b/guides/src/images/user/promotions/products_rule.jpg
new file mode 100755
index 00000000000..61371c1b159
Binary files /dev/null and b/guides/src/images/user/promotions/products_rule.jpg differ
diff --git a/guides/src/images/user/promotions/rules_and_actions.jpg b/guides/src/images/user/promotions/rules_and_actions.jpg
new file mode 100755
index 00000000000..b6e56b035fa
Binary files /dev/null and b/guides/src/images/user/promotions/rules_and_actions.jpg differ
diff --git a/guides/src/images/user/promotions/rules_options.jpg b/guides/src/images/user/promotions/rules_options.jpg
new file mode 100755
index 00000000000..6ecd0d5807e
Binary files /dev/null and b/guides/src/images/user/promotions/rules_options.jpg differ
diff --git a/guides/src/images/user/promotions/user_rule.jpg b/guides/src/images/user/promotions/user_rule.jpg
new file mode 100755
index 00000000000..b65b69aa1e8
Binary files /dev/null and b/guides/src/images/user/promotions/user_rule.jpg differ
diff --git a/guides/src/images/user/sales_total_dates.jpg b/guides/src/images/user/sales_total_dates.jpg
new file mode 100644
index 00000000000..9a6d7a6a3b6
Binary files /dev/null and b/guides/src/images/user/sales_total_dates.jpg differ
diff --git a/guides/src/images/user/sales_total_report.jpg b/guides/src/images/user/sales_total_report.jpg
new file mode 100644
index 00000000000..d3614563a7c
Binary files /dev/null and b/guides/src/images/user/sales_total_report.jpg differ
diff --git a/guides/src/images/user/shipments/add_multi_to_zone.jpg b/guides/src/images/user/shipments/add_multi_to_zone.jpg
new file mode 100755
index 00000000000..97da4d8ac3f
Binary files /dev/null and b/guides/src/images/user/shipments/add_multi_to_zone.jpg differ
diff --git a/guides/src/images/user/shipments/delete_shipping_method.jpg b/guides/src/images/user/shipments/delete_shipping_method.jpg
new file mode 100755
index 00000000000..f8676767bc0
Binary files /dev/null and b/guides/src/images/user/shipments/delete_shipping_method.jpg differ
diff --git a/guides/src/images/user/shipments/edit_shipping_method.jpg b/guides/src/images/user/shipments/edit_shipping_method.jpg
new file mode 100755
index 00000000000..33fbc548ba9
Binary files /dev/null and b/guides/src/images/user/shipments/edit_shipping_method.jpg differ
diff --git a/guides/src/images/user/shipments/edit_zone.jpg b/guides/src/images/user/shipments/edit_zone.jpg
new file mode 100755
index 00000000000..ae63118f934
Binary files /dev/null and b/guides/src/images/user/shipments/edit_zone.jpg differ
diff --git a/guides/src/images/user/shipments/new_shipping_category.jpg b/guides/src/images/user/shipments/new_shipping_category.jpg
new file mode 100755
index 00000000000..591ee852bd0
Binary files /dev/null and b/guides/src/images/user/shipments/new_shipping_category.jpg differ
diff --git a/guides/src/images/user/shipments/new_shipping_method.jpg b/guides/src/images/user/shipments/new_shipping_method.jpg
new file mode 100755
index 00000000000..ab31f3c4d3f
Binary files /dev/null and b/guides/src/images/user/shipments/new_shipping_method.jpg differ
diff --git a/guides/src/images/user/shipments/new_zone.jpg b/guides/src/images/user/shipments/new_zone.jpg
new file mode 100755
index 00000000000..88f655de820
Binary files /dev/null and b/guides/src/images/user/shipments/new_zone.jpg differ
diff --git a/guides/src/images/user/shipments/remove_zone_member.jpg b/guides/src/images/user/shipments/remove_zone_member.jpg
new file mode 100755
index 00000000000..c37ab4a1088
Binary files /dev/null and b/guides/src/images/user/shipments/remove_zone_member.jpg differ
diff --git a/guides/src/images/user/shipments/select_shipping_category.jpg b/guides/src/images/user/shipments/select_shipping_category.jpg
new file mode 100755
index 00000000000..556279289e0
Binary files /dev/null and b/guides/src/images/user/shipments/select_shipping_category.jpg differ
diff --git a/guides/src/images/user/shipments/shipping_method_calculator.jpg b/guides/src/images/user/shipments/shipping_method_calculator.jpg
new file mode 100755
index 00000000000..0bed9f32111
Binary files /dev/null and b/guides/src/images/user/shipments/shipping_method_calculator.jpg differ
diff --git a/guides/src/images/user/shipments/shipping_method_categories.jpg b/guides/src/images/user/shipments/shipping_method_categories.jpg
new file mode 100755
index 00000000000..cc6540c8a34
Binary files /dev/null and b/guides/src/images/user/shipments/shipping_method_categories.jpg differ
diff --git a/guides/src/images/user/shipments/shipping_method_flat_percent.jpg b/guides/src/images/user/shipments/shipping_method_flat_percent.jpg
new file mode 100755
index 00000000000..f9949fa3b4a
Binary files /dev/null and b/guides/src/images/user/shipments/shipping_method_flat_percent.jpg differ
diff --git a/guides/src/images/user/shipments/shipping_method_zones.jpg b/guides/src/images/user/shipments/shipping_method_zones.jpg
new file mode 100755
index 00000000000..1fa11f40f58
Binary files /dev/null and b/guides/src/images/user/shipments/shipping_method_zones.jpg differ
diff --git a/guides/src/images/user/users/add_new_user.jpg b/guides/src/images/user/users/add_new_user.jpg
new file mode 100755
index 00000000000..04c03801fef
Binary files /dev/null and b/guides/src/images/user/users/add_new_user.jpg differ
diff --git a/guides/src/images/user/users/store_credit_categories.jpg b/guides/src/images/user/users/store_credit_categories.jpg
new file mode 100755
index 00000000000..f2a6e74d523
Binary files /dev/null and b/guides/src/images/user/users/store_credit_categories.jpg differ
diff --git a/guides/src/images/user/users/store_credit_categories_delete.jpg b/guides/src/images/user/users/store_credit_categories_delete.jpg
new file mode 100755
index 00000000000..52594adf23a
Binary files /dev/null and b/guides/src/images/user/users/store_credit_categories_delete.jpg differ
diff --git a/guides/src/images/user/users/store_credit_categories_edit.jpg b/guides/src/images/user/users/store_credit_categories_edit.jpg
new file mode 100755
index 00000000000..55f483dd1e9
Binary files /dev/null and b/guides/src/images/user/users/store_credit_categories_edit.jpg differ
diff --git a/guides/src/images/user/users/store_credit_categories_edit_inside.jpg b/guides/src/images/user/users/store_credit_categories_edit_inside.jpg
new file mode 100755
index 00000000000..fcc9f04b969
Binary files /dev/null and b/guides/src/images/user/users/store_credit_categories_edit_inside.jpg differ
diff --git a/guides/src/images/user/users/store_credit_categories_new.jpg b/guides/src/images/user/users/store_credit_categories_new.jpg
new file mode 100755
index 00000000000..4806c1e2e98
Binary files /dev/null and b/guides/src/images/user/users/store_credit_categories_new.jpg differ
diff --git a/guides/src/images/user/users/store_credit_front_applied.jpg b/guides/src/images/user/users/store_credit_front_applied.jpg
new file mode 100755
index 00000000000..ff549f2fdc8
Binary files /dev/null and b/guides/src/images/user/users/store_credit_front_applied.jpg differ
diff --git a/guides/src/images/user/users/store_credit_front_apply.jpg b/guides/src/images/user/users/store_credit_front_apply.jpg
new file mode 100755
index 00000000000..c67b85c64f4
Binary files /dev/null and b/guides/src/images/user/users/store_credit_front_apply.jpg differ
diff --git a/guides/src/images/user/users/store_credit_front_confirm.jpg b/guides/src/images/user/users/store_credit_front_confirm.jpg
new file mode 100755
index 00000000000..24cb5252afb
Binary files /dev/null and b/guides/src/images/user/users/store_credit_front_confirm.jpg differ
diff --git a/guides/src/images/user/users/store_credit_front_placed_order.jpg b/guides/src/images/user/users/store_credit_front_placed_order.jpg
new file mode 100755
index 00000000000..4021c6e37c1
Binary files /dev/null and b/guides/src/images/user/users/store_credit_front_placed_order.jpg differ
diff --git a/guides/src/images/user/users/store_credit_order_paid.jpg b/guides/src/images/user/users/store_credit_order_paid.jpg
new file mode 100755
index 00000000000..c4c7d036b24
Binary files /dev/null and b/guides/src/images/user/users/store_credit_order_paid.jpg differ
diff --git a/guides/src/images/user/users/store_credit_user.jpg b/guides/src/images/user/users/store_credit_user.jpg
new file mode 100755
index 00000000000..166ab7463c5
Binary files /dev/null and b/guides/src/images/user/users/store_credit_user.jpg differ
diff --git a/guides/src/images/user/users/store_credit_user_add.jpg b/guides/src/images/user/users/store_credit_user_add.jpg
new file mode 100755
index 00000000000..1981e0f670a
Binary files /dev/null and b/guides/src/images/user/users/store_credit_user_add.jpg differ
diff --git a/guides/src/images/user/users/store_credit_user_added.jpg b/guides/src/images/user/users/store_credit_user_added.jpg
new file mode 100755
index 00000000000..f4adeab08d8
Binary files /dev/null and b/guides/src/images/user/users/store_credit_user_added.jpg differ
diff --git a/guides/src/images/user/users/store_credit_user_added_delete.jpg b/guides/src/images/user/users/store_credit_user_added_delete.jpg
new file mode 100755
index 00000000000..7a52af548ce
Binary files /dev/null and b/guides/src/images/user/users/store_credit_user_added_delete.jpg differ
diff --git a/guides/src/images/user/users/store_credit_user_added_edit.jpg b/guides/src/images/user/users/store_credit_user_added_edit.jpg
new file mode 100755
index 00000000000..038b59d5388
Binary files /dev/null and b/guides/src/images/user/users/store_credit_user_added_edit.jpg differ
diff --git a/guides/src/images/user/users/store_credit_user_added_edit_inside.jpg b/guides/src/images/user/users/store_credit_user_added_edit_inside.jpg
new file mode 100755
index 00000000000..2cfdb511c47
Binary files /dev/null and b/guides/src/images/user/users/store_credit_user_added_edit_inside.jpg differ
diff --git a/guides/src/images/user/users/store_credit_user_categories_dropdown.jpg b/guides/src/images/user/users/store_credit_user_categories_dropdown.jpg
new file mode 100755
index 00000000000..6ffcd693cb6
Binary files /dev/null and b/guides/src/images/user/users/store_credit_user_categories_dropdown.jpg differ
diff --git a/guides/src/images/user/users/store_credit_user_new.jpg b/guides/src/images/user/users/store_credit_user_new.jpg
new file mode 100755
index 00000000000..4f58cb88d44
Binary files /dev/null and b/guides/src/images/user/users/store_credit_user_new.jpg differ
diff --git a/guides/src/images/user/users/store_credit_user_paid.jpg b/guides/src/images/user/users/store_credit_user_paid.jpg
new file mode 100755
index 00000000000..28ddb6f44b4
Binary files /dev/null and b/guides/src/images/user/users/store_credit_user_paid.jpg differ
diff --git a/guides/src/images/user/users/user_delete_option.jpg b/guides/src/images/user/users/user_delete_option.jpg
new file mode 100755
index 00000000000..003b703057b
Binary files /dev/null and b/guides/src/images/user/users/user_delete_option.jpg differ
diff --git a/guides/src/images/user/users/user_edit_inside.jpg b/guides/src/images/user/users/user_edit_inside.jpg
new file mode 100755
index 00000000000..955b703a97e
Binary files /dev/null and b/guides/src/images/user/users/user_edit_inside.jpg differ
diff --git a/guides/src/images/user/users/user_edit_inside_address.jpg b/guides/src/images/user/users/user_edit_inside_address.jpg
new file mode 100755
index 00000000000..94826bd450c
Binary files /dev/null and b/guides/src/images/user/users/user_edit_inside_address.jpg differ
diff --git a/guides/src/images/user/users/user_edit_inside_api.jpg b/guides/src/images/user/users/user_edit_inside_api.jpg
new file mode 100755
index 00000000000..dd2379c26b5
Binary files /dev/null and b/guides/src/images/user/users/user_edit_inside_api.jpg differ
diff --git a/guides/src/images/user/users/user_edit_inside_items.jpg b/guides/src/images/user/users/user_edit_inside_items.jpg
new file mode 100755
index 00000000000..30b1fab3cf3
Binary files /dev/null and b/guides/src/images/user/users/user_edit_inside_items.jpg differ
diff --git a/guides/src/images/user/users/user_edit_inside_lifetimestats.jpg b/guides/src/images/user/users/user_edit_inside_lifetimestats.jpg
new file mode 100755
index 00000000000..3c655a777c6
Binary files /dev/null and b/guides/src/images/user/users/user_edit_inside_lifetimestats.jpg differ
diff --git a/guides/src/images/user/users/user_edit_option.jpg b/guides/src/images/user/users/user_edit_option.jpg
new file mode 100755
index 00000000000..5be1c089af6
Binary files /dev/null and b/guides/src/images/user/users/user_edit_option.jpg differ
diff --git a/guides/src/images/user/users/user_edit_orders.jpg b/guides/src/images/user/users/user_edit_orders.jpg
new file mode 100755
index 00000000000..4e9fe70b826
Binary files /dev/null and b/guides/src/images/user/users/user_edit_orders.jpg differ
diff --git a/guides/src/images/user/users/users_search_option.jpg b/guides/src/images/user/users/users_search_option.jpg
new file mode 100755
index 00000000000..4e5ad64e12c
Binary files /dev/null and b/guides/src/images/user/users/users_search_option.jpg differ
diff --git a/guides/src/images/user/users/users_tab.jpg b/guides/src/images/user/users/users_tab.jpg
new file mode 100755
index 00000000000..8a7d3e6707f
Binary files /dev/null and b/guides/src/images/user/users/users_tab.jpg differ
diff --git a/guides/src/pages/404.js b/guides/src/pages/404.js
new file mode 100644
index 00000000000..46077a3aeff
--- /dev/null
+++ b/guides/src/pages/404.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import Layout from '../components/Layout'
+
+const NotFoundPage = () => (
+
+
NOT FOUND
+
You just hit a route that doesn't exist... the sadness.
+
+)
+
+export default NotFoundPage
diff --git a/guides/src/pages/api/overview.js b/guides/src/pages/api/overview.js
new file mode 100644
index 00000000000..58065cc1dae
--- /dev/null
+++ b/guides/src/pages/api/overview.js
@@ -0,0 +1,19 @@
+import * as React from 'react'
+
+import Layout from '../../components/Layout'
+import Button from '../../components/base/Button'
+
+const IndexPage = () => (
+
+
+ Spree Commerce is a complete
+ modular, API-driven open source e-commerce solution built with Ruby on
+ Rails.
+
+
+
+
+
+ The REST API is designed to give developers a convenient way to
+ access data contained within Spree. With a standard read/write
+ interface to store data, it is now very simple to write third party
+ applications (eg. iPhone) that can talk to your Spree store.
+
+
+
+ This part of Spree’s documentation covers the technical aspects of
+ Spree. If you are working with Rails and are building a Spree store,
+ this is the documentation for you.
+
+
+
+
+
+ This documentation is intended for business owners and site
+ administrators of Spree e-commerce sites. Everything you need to
+ know to configure and manage your Spree store can be found here.
+
+
+
+ Each major new release of Spree has an accompanying set of release
+ notes. The purpose of these notes is to provide a high level
+ overview of what has changed since the previous version of Spree.
+
+