├── .ebextensions └── 00-packages.config ├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── .pullreview.yml ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── .simplecov ├── .travis.yml ├── .tx └── config ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ ├── .keep │ │ ├── background.png │ │ ├── bucky-box-powered.png │ │ ├── close-icon.png │ │ ├── facebook-icon.png │ │ ├── fallbacks │ │ │ └── box │ │ │ │ └── box_image │ │ │ │ └── webstore_default.png │ │ ├── map-marker-icon.png │ │ ├── phone-icon.png │ │ └── webstore-background.png │ ├── javascripts │ │ ├── application.js │ │ ├── map.js │ │ ├── nprogress-config.js │ │ └── webstore.js │ └── stylesheets │ │ ├── application.sass │ │ ├── map.sass │ │ └── webstore │ │ └── _base.sass ├── controllers │ ├── application_controller.rb │ ├── authentication_controller.rb │ ├── checkout_controller.rb │ ├── completed_controller.rb │ ├── concerns │ │ └── .keep │ ├── customise_order_controller.rb │ ├── delivery_options_controller.rb │ ├── map_controller.rb │ ├── payment_options_controller.rb │ ├── session_controller.rb │ └── store_controller.rb ├── decorators │ ├── cart_decorator.rb │ ├── customer_decorator.rb │ ├── delivery_service_decorator.rb │ ├── order_decorator.rb │ └── product_decorator.rb ├── helpers │ └── application_helper.rb ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── cart.rb │ ├── cart_persistence.rb │ ├── checkout │ │ ├── authentication.rb │ │ ├── checkout.rb │ │ ├── completed.rb │ │ ├── customise_order.rb │ │ ├── delivery_options.rb │ │ ├── home.rb │ │ └── payment_options.rb │ ├── concerns │ │ └── .keep │ ├── customer.rb │ ├── customer_factory.rb │ ├── factory.rb │ ├── form.rb │ ├── order.rb │ ├── order_factory.rb │ ├── order_price.rb │ └── product.rb ├── services │ └── api.rb └── views │ ├── application │ ├── _order.html.haml │ ├── _payment_instructions.html.haml │ └── _sidebar.html.haml │ ├── authentication │ └── authentication.html.haml │ ├── completed │ └── completed.html.haml │ ├── customise_order │ ├── _extra.html.haml │ └── customise_order.html.haml │ ├── delivery_options │ ├── _delivery_service.html.haml │ └── delivery_options.html.haml │ ├── layouts │ ├── _banner.html.haml │ ├── _bugsnag.html.haml │ ├── _common_head.html.haml │ ├── _google_analytics.html.haml │ ├── _i18n.html.haml │ ├── _pingdom.html.haml │ └── application.html.haml │ ├── map │ └── index.html.haml │ ├── payment_options │ └── payment_options.html.haml │ ├── payments │ ├── _bank_deposit.html.haml │ ├── _cash_on_delivery.html.haml │ └── _paypal.html.haml │ ├── session │ └── new.html.haml │ └── store │ ├── _product.html.haml │ └── home.html.haml ├── bin ├── bundle ├── check_i18n ├── ci ├── rails ├── rake └── setup ├── config.ru ├── config ├── application.rb ├── application.yml.example ├── boot.rb ├── cucumber.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── bugsnag.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── naught.rb │ ├── redis.rb │ ├── secure_headers.rb │ ├── session_store.rb │ ├── simple_form.rb │ ├── simple_form_bootstrap.rb │ └── wrap_parameters.rb ├── locales │ ├── de │ │ ├── common.yml │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ └── webstore.yml │ ├── en │ │ ├── common.yml │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ └── webstore.yml │ ├── fr │ │ ├── common.yml │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ └── webstore.yml │ ├── it │ │ ├── common.yml │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ └── webstore.yml │ ├── nl │ │ ├── common.yml │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ └── webstore.yml │ ├── pt-BR │ │ ├── common.yml │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ └── webstore.yml │ ├── pt_BR │ └── zh │ │ ├── common.yml │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ └── webstore.yml ├── puma.rb ├── routes.rb └── secrets.yml ├── doc └── screenshot.jpg ├── features ├── step_definitions │ ├── .gitkeep │ └── webstore_steps.rb ├── support │ └── env.rb ├── webstore_authenticated.feature └── webstore_unauthenticated.feature ├── lib ├── address.rb ├── assets │ └── .keep ├── paypal_form.rb ├── phone_collection.rb ├── schedule_rule.rb └── tasks │ ├── .keep │ └── assets.rake ├── log └── .keep ├── public ├── 404.html ├── 422.html ├── 500.html ├── 502.html ├── 504.html ├── apple-touch-icon.png ├── assets │ ├── .sprockets-manifest-2487fc3d0a0e3a3fa7950f702299bdac.json │ ├── application-731c774331642949afafd49563b1ad64e3ee06bdc5e886d3188c43247c00709a.js │ ├── application-731c774331642949afafd49563b1ad64e3ee06bdc5e886d3188c43247c00709a.js.gz │ ├── application-f38798986a4c88227b916d75d74dbedc35a7b60b67f4a1a40fc92ca6cdceea02.css │ ├── application-f38798986a4c88227b916d75d74dbedc35a7b60b67f4a1a40fc92ca6cdceea02.css.gz │ ├── background-5f61dbdbbda4f607223a5b6b855ea75ccfada574f3a4a5f3c3f2df744e7469d1.png │ ├── bucky-box-powered-7d986f2ee6ad772d8880f6fbddc0a2ae2e6366c872a4934d472db593a11625bb.png │ ├── close-icon-397a02e804aad172d9186b3f92da72c5d323618f4965917901230bb9bf12ca8a.png │ ├── facebook-icon-1f1ef4d63dffd8ad069e72c67ed5b635a62c3103b954d823aa54d475e3437ef3.png │ ├── fallbacks │ │ └── box │ │ │ └── box_image │ │ │ └── webstore_default-f6ac6c6001ef9b277031a76f571ead446e230037099e0a92b76b6c6dd38b20cd.png │ ├── glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png │ ├── glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png │ ├── layers-1dbbe9d028e292f36fcba8f8b3a28d5e8932754fc2215b9ac69e4cdecf5107c6.png │ ├── layers-2x-066daca850d8ffbef007af00b06eac0015728dee279c51f3cb6c716df7c42edf.png │ ├── leaflet-src-c3e1d834d7ca30c12e8b9950e698797b2dcf5c35c401d347622a0e177b50fde9.map │ ├── map-4aa8a25cbf7a3b1c49089b140a359ac8bae65503825dd6b38dd7be725b323a0e.js │ ├── map-4aa8a25cbf7a3b1c49089b140a359ac8bae65503825dd6b38dd7be725b323a0e.js.gz │ ├── map-9a88d1a7714d73d7ecc1c1ea35031b3537b7b7b1167634702b923257f5ed6fdf.css │ ├── map-9a88d1a7714d73d7ecc1c1ea35031b3537b7b7b1167634702b923257f5ed6fdf.css.gz │ ├── map-marker-icon-cee88d7d4f8b842112804ca8f8a718f893d5b05f9f925e84553c61bc81244191.png │ ├── marker-icon-2x-2d77a2e4c2f08bbac41808324ef946b9a2fe61b6150480d011b72b379c3b238d.png │ ├── marker-icon-574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437.png │ ├── marker-shadow-264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da.png │ ├── phone-icon-2456db717faf8f3bee70c50bf2ece7e7a4d0ea4f613fac6dd8cf3a897bfd8a7c.png │ ├── select2-d6b5d8d83dbc18fb8d77c8761d331cd9e5123c9684950bab0406e98a24ac5ae8.png │ ├── select2-spinner-f6ecff617ec2ba7f559e6f535cad9b70a3f91120737535dab4d4548a6c83576c.gif │ ├── select2x2-6fe28d687dc0ed4d96016238c608ba1e7198c9c9accfa0b360b78018b9fb9bc2.png │ └── webstore-background-25dc57c5899bd5f11d50dfe04ed11577654bdddacaa138725ccfd158e472728d.png ├── favicon-gray.ico ├── favicon-red.ico ├── favicon.ico └── robots.txt ├── spec ├── lib │ └── schedule_rule_spec.rb ├── models │ ├── cart_spec.rb │ ├── checkout │ │ ├── authentication_spec.rb │ │ ├── checkout_spec.rb │ │ ├── customise_order_spec.rb │ │ ├── delivery_options_spec.rb │ │ ├── home_spec.rb │ │ └── payment_options_spec.rb │ ├── form_spec.rb │ └── order_spec.rb ├── requests │ ├── application_controller_spec.rb │ └── map_controller_spec.rb ├── spec_helper.rb └── support │ └── capybara │ └── select2_helper.rb └── vendor ├── assets ├── javascripts │ ├── .keep │ ├── fetch.js │ ├── jquery.imagesloaded.min.js │ ├── jquery.masonry.min.js │ └── nprogress.js └── stylesheets │ ├── .keep │ └── nprogress.css └── bin ├── install_phantomjs.sh └── yamlkeysdiff /.ebextensions/00-packages.config: -------------------------------------------------------------------------------- 1 | packages: 2 | yum: 3 | git: [] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /coverage/ 12 | /log/*.log 13 | /public/dev-assets/ 14 | /tmp/ 15 | 16 | # Redis 17 | /dump.rdb 18 | 19 | # Figaro config 20 | /config/application.yml 21 | 22 | # Elastic Beanstalk 23 | /.elasticbeanstalk/* 24 | !/.elasticbeanstalk/*.cfg.yml 25 | !/.elasticbeanstalk/*.global.yml 26 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # Official language image. Look for the different tagged releases at: 2 | # https://hub.docker.com/r/library/ruby/tags/ 3 | image: ruby:2.5 4 | 5 | services: 6 | - redis:latest 7 | 8 | variables: 9 | REDIS_URL: redis://redis:6379/ 10 | 11 | cache: 12 | paths: 13 | - vendor/ruby # cache gems in between builds 14 | 15 | before_script: 16 | - ruby -v 17 | - gem install bundler --no-document 18 | - bundle install -j $(nproc) --path vendor 19 | 20 | integration: 21 | script: 22 | - ./vendor/bin/install_phantomjs.sh 23 | - ./bin/ci 24 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/.gitmodules -------------------------------------------------------------------------------- /.pullreview.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | ignore: 4 | - missing_class_documentation 5 | - missing_method_documentation 6 | - prefer_single_quoted_strings 7 | 8 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | 3 | RSpec/NestedGroups: 4 | Enabled: false 5 | 6 | RSpec/ReturnFromStub: 7 | Enabled: false 8 | 9 | # Bundler 10 | 11 | Bundler/OrderedGems: 12 | Enabled: false 13 | 14 | # Layout 15 | 16 | Layout/AccessModifierIndentation: 17 | EnforcedStyle: outdent 18 | 19 | Layout/ElseAlignment: 20 | Enabled: false 21 | 22 | Layout/EndAlignment: 23 | Enabled: false 24 | 25 | Layout/IndentationWidth: 26 | Enabled: false 27 | 28 | # Lint 29 | 30 | Lint/AmbiguousRegexpLiteral: 31 | Exclude: 32 | - features/step_definitions/*.rb 33 | 34 | # Metrics 35 | 36 | Metrics: 37 | Enabled: false 38 | 39 | # Naming 40 | Naming/PredicateName: 41 | Enabled: false 42 | 43 | # Rails 44 | 45 | Rails: 46 | Enabled: true 47 | 48 | Rails/FilePath: 49 | Enabled: false 50 | 51 | # Style 52 | 53 | Style/Documentation: 54 | Enabled: false 55 | 56 | Style/GlobalVars: 57 | AllowedVariables: [$redis] 58 | 59 | Style/SafeNavigation: 60 | Enabled: false 61 | 62 | Style/StringLiterals: 63 | EnforcedStyle: double_quotes 64 | 65 | Style/TrailingCommaInArguments: 66 | EnforcedStyleForMultiline: comma 67 | 68 | Style/TrailingCommaInArrayLiteral: 69 | EnforcedStyleForMultiline: comma 70 | 71 | Style/TrailingCommaInHashLiteral: 72 | EnforcedStyleForMultiline: comma 73 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.8 2 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- 1 | SimpleCov.start do 2 | track_files "**/*.rb" 3 | 4 | add_filter "/config/environments/" 5 | add_filter "/config/puma.rb" 6 | add_filter "/config/unicorn.rb" 7 | add_filter "/features/support/env.rb" 8 | add_filter "/spec/" 9 | add_filter "/vendor/" 10 | end 11 | 12 | # vim: ft=ruby 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | services: 3 | - redis-server 4 | script: ./bin/ci 5 | after_script: '[ "$TRAVIS_RUBY_VERSION" != "2.5.8" ] || bundle exec rake coveralls:push' 6 | rvm: 7 | - 2.5.8 8 | - 2.6.6 9 | matrix: 10 | allow_failures: 11 | - rvm: 2.6.6 12 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [buckybox-core.common] 5 | file_filter = config/locales//common.yml 6 | source_file = config/locales/en/common.yml 7 | source_lang = en 8 | type = YML 9 | 10 | [buckybox-core.simple_form] 11 | file_filter = config/locales//simple_form.yml 12 | source_file = config/locales/en/simple_form.yml 13 | source_lang = en 14 | type = YML 15 | 16 | [buckybox-webstore.webstore] 17 | file_filter = config/locales//webstore.yml 18 | source_file = config/locales/en/webstore.yml 19 | source_lang = en 20 | type = YML 21 | 22 | [buckybox-webstore.javascript] 23 | file_filter = config/locales//javascript.yml 24 | source_file = config/locales/en/javascript.yml 25 | source_lang = en 26 | type = YML 27 | 28 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "rails", "~> 4.2" 6 | gem "puma", "~> 3.12.6" 7 | gem "sass-rails" 8 | gem "uglifier" # Use Uglifier as compressor for JavaScript assets 9 | gem "coffee-rails" # Use CoffeeScript for .coffee assets and views 10 | gem "rails-html-sanitizer" # Use Rails Html Sanitizer for HTML sanitization 11 | 12 | gem "therubyracer" # JS runtime 13 | gem "jquery-rails" # Use jquery as the JavaScript library 14 | # gem 'turbolinks' # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks 15 | # gem 'jquery-turbolinks' 16 | 17 | # Our additional Gems are listed below 18 | gem "secure_headers", "< 4" # TODO: upgrade 19 | gem "rails-i18n" 20 | gem "crazy_money" 21 | gem "ordinalize_full", require: "ordinalize_full/integer" 22 | gem "buckybox-api" # to use the git version: git: "https://github.com/buckybox/buckybox-api-ruby" 23 | gem "hashie", "3.4.4" # TODO: 3.4.6 is broken 24 | gem "fast_blank" 25 | gem "figaro" 26 | 27 | gem "redis" # for WebstorePersistence 28 | gem "hiredis" # https://github.com/redis/redis-rb#hiredis 29 | 30 | gem "haml-rails" 31 | gem "bootstrap-sass", "< 3" # TODO: upgrade to 3 32 | gem "autoprefixer-rails" # Add browser vendor prefixes automatically 33 | gem "select2-rails", "< 4" # TODO: https://github.com/select2/select2/blob/master/docs/announcements-4.0.html 34 | gem "leaflet-rails" 35 | 36 | gem "simple_form" 37 | gem "virtus" 38 | gem "draper" 39 | gem "naught" 40 | 41 | # Optional Gems are listed below 42 | gem "bugsnag" 43 | 44 | group :development, :test do 45 | gem "byebug", platform: :mri 46 | gem "better_errors" 47 | gem "binding_of_caller" # to get REPL for better_errors 48 | end 49 | 50 | group :development do 51 | gem "bundler-audit", require: false 52 | gem "rubocop", require: false 53 | gem "rubocop-rspec", require: false 54 | gem "simplecov", "< 0.15", require: false # TODO: upgrade when https://travis-ci.org/buckybox/webstore/builds/308318715 is resolved 55 | gem "coveralls", require: false 56 | gem "brakeman", require: false 57 | end 58 | 59 | group :test do 60 | gem "rspec-rails", require: false 61 | gem "cucumber-rails", require: false 62 | gem "capybara", require: false 63 | gem "capybara-screenshot" 64 | gem "poltergeist", require: false 65 | gem "webmock", require: false 66 | gem "vcr", require: false 67 | end 68 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Bucky Box Web Store 2 | Copyright (C) 2014-2019 Bucky Box 3 | 4 | GPLv3+ License 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bucky Box Web Store 2 | 3 | [![Build Status](https://gitlab.com/buckybox/webstore/badges/master/build.svg)](https://gitlab.com/buckybox/webstore/commits/master) 4 | [![Build Status](https://travis-ci.org/buckybox/webstore.svg?branch=master)](https://travis-ci.org/buckybox/webstore) 5 | [![Coverage Status](https://coveralls.io/repos/buckybox/webstore/badge.svg?branch=master&service=github)](https://coveralls.io/github/buckybox/webstore?branch=master) 6 | [![Code Climate](https://codeclimate.com/github/buckybox/webstore/badges/gpa.svg)](https://codeclimate.com/github/buckybox/webstore) 7 | 8 | Bucky Box Web Store is part of the [Bucky Box](http://www.buckybox.com/) platform. 9 | It allows customers to place orders using the [Bucky Box API](https://api.buckybox.com/docs/). 10 | 11 | ![Screenshot](doc/screenshot.jpg) 12 | 13 | ## Configuration 14 | 15 | See [config/application.yml](https://github.com/buckybox/webstore/blob/master/config/application.yml.example). 16 | The required settings are `BUCKYBOX_API_KEY`, `BUCKYBOX_API_SECRET` and `SECRET_KEY_BASE`. You can leave the rest blank. 17 | 18 | ## Services 19 | 20 | No database is required but you must have [Redis](http://redis.io/) running to store carts. 21 | 22 | ## Deployment instructions 23 | 24 | ```bash 25 | cp config/application.yml.example config/application.yml 26 | RAILS_ENV=development bundle exec puma -p 5000 --config config/puma.rb --log-requests 27 | ``` 28 | 29 | ## Contributing 30 | 31 | Any bug fix or tweak is welcomed but if you have bigger plans, please drop us a line at `support AT buckybox.com` first. 32 | 33 | ## Translation 34 | 35 | You can help translate it into your favorite language. 36 | We use [Transifex](https://www.transifex.com/projects/p/buckybox-webstore/). 37 | New translations can be fetched with `tx pull -af`. 38 | 39 | ## Tests 40 | 41 | ```bash 42 | ./bin/ci 43 | # or 44 | git commit && gitlab-ci-multi-runner exec docker integration 45 | ``` 46 | 47 | ## License 48 | 49 | GPLv3+ 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require File.expand_path("config/application", __dir__) 7 | 8 | Rails.application.load_tasks 9 | 10 | if Rails.env.development? || Rails.env.test? 11 | 12 | require "rspec/core/rake_task" 13 | RSpec::Core::RakeTask.new(:spec) 14 | 15 | task default: :spec 16 | 17 | require "coveralls/rake/task" 18 | Coveralls::RakeTask.new 19 | 20 | end 21 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/assets/images/background.png -------------------------------------------------------------------------------- /app/assets/images/bucky-box-powered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/assets/images/bucky-box-powered.png -------------------------------------------------------------------------------- /app/assets/images/close-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/assets/images/close-icon.png -------------------------------------------------------------------------------- /app/assets/images/facebook-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/assets/images/facebook-icon.png -------------------------------------------------------------------------------- /app/assets/images/fallbacks/box/box_image/webstore_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/assets/images/fallbacks/box/box_image/webstore_default.png -------------------------------------------------------------------------------- /app/assets/images/map-marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/assets/images/map-marker-icon.png -------------------------------------------------------------------------------- /app/assets/images/phone-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/assets/images/phone-icon.png -------------------------------------------------------------------------------- /app/assets/images/webstore-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/assets/images/webstore-background.png -------------------------------------------------------------------------------- /app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | // require jquery.turbolinks 15 | //= require jquery_ujs 16 | //= require bootstrap 17 | //= require select2 18 | // require turbolinks 19 | 20 | //= require_tree ../../../vendor/assets/javascripts 21 | 22 | //= require ./nprogress-config 23 | //= require ./webstore 24 | 25 | -------------------------------------------------------------------------------- /app/assets/javascripts/map.js: -------------------------------------------------------------------------------- 1 | //= require fetch 2 | //= require leaflet 3 | 4 | (function() { 5 | "use strict"; 6 | 7 | /// LIGHTBOX 8 | 9 | // assume there is only one lightbox present at a time 10 | var lightbox = document.querySelectorAll(".lightbox")[0]; 11 | 12 | if (document.cookie.indexOf("skip_lightbox=1") !== -1) { 13 | lightbox.remove(); 14 | } else { 15 | lightbox.addEventListener("click", function(e) { this.remove(); }); 16 | 17 | (function fadeInElements() { 18 | var elements = lightbox.querySelectorAll(".fade-in"); 19 | for (var i = 0; i < elements.length; i++) { 20 | var el = elements[i]; 21 | setTimeout(function(el) { el.style.opacity = 0.9; }, i*1000, el); 22 | } 23 | })(); 24 | } 25 | 26 | 27 | /// MAP 28 | 29 | function MapData() { 30 | this.userLocation = null; 31 | this.stores = null; 32 | } 33 | 34 | var mapData = new MapData(); 35 | 36 | addEventListener("userLocationAcquired", function(e) { 37 | mapData.userLocation = e.detail; 38 | tryToPan(); 39 | }); 40 | 41 | addEventListener("storesAcquired", function(e) { 42 | mapData.stores = e.detail; 43 | tryToPan(); 44 | }); 45 | 46 | var map = L.map("map").setView([20, 40], 2); 47 | 48 | detectLocation(map); 49 | fetchStores(map); 50 | setFooter(map); 51 | L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png").addTo(map); 52 | 53 | // functions below: 54 | 55 | function tryToPan() { 56 | if (!mapData.userLocation || !mapData.stores) return; 57 | 58 | var userLocation = mapData.userLocation, 59 | stores = mapData.stores, 60 | minDistance = Number.MAX_VALUE, 61 | closestStore = null; 62 | 63 | stores.forEach(function(store) { 64 | if (!store.ll[0]) { 65 | console.warn("No location for store " + store.name); 66 | return; 67 | } 68 | 69 | var storeLocation = L.latLng(store.ll), 70 | distance = storeLocation.distanceTo(userLocation); 71 | 72 | if (distance < minDistance) { 73 | minDistance = distance; 74 | closestStore = store; 75 | } 76 | }); 77 | 78 | map.fitBounds([userLocation, closestStore.ll], { maxZoom: 8 }); 79 | } 80 | 81 | function setFooter(map) { 82 | map.attributionControl.setPrefix('Map powered by OpenStreetMap and Leaflet'); 83 | 84 | var FooterControl = L.Control.extend({ 85 | options: { position: "bottomleft" }, 86 | onAdd: function (map) { 87 | var container = L.DomUtil.create("div", "leaflet-control-attribution"); 88 | 89 | container.innerHTML = 'Map of stores powered by ' + 90 | 'Bucky Box, ' + 91 | 'an ordering system for local food organisations'; 92 | 93 | return container; 94 | } 95 | }); 96 | 97 | map.addControl(new FooterControl()); 98 | } 99 | 100 | function detectLocation(map) { 101 | var request = new Request("https://api.buckybox.com/v1/geoip", { 102 | method: "GET", 103 | mode: "cors" 104 | }); 105 | 106 | fetch(request).then(checkStatus).then(parseJSON).then(function(json) { 107 | 108 | var userLocation = L.latLng(json.latitude, json.longitude); 109 | L.circle(userLocation, 50*1E3, { clickable: false, stroke: false }).addTo(map); 110 | 111 | var event = new CustomEvent("userLocationAcquired", { 'detail': userLocation }); 112 | dispatchEvent(event); 113 | 114 | }).catch(function(err) { 115 | console.error(err); 116 | }); 117 | } 118 | 119 | function fetchStores(map) { 120 | var request = new Request("https://api.buckybox.com/v1/webstores", { 121 | method: "GET", 122 | mode: "cors" 123 | }); 124 | 125 | fetch(request).then(checkStatus).then(parseJSON).then(function(json) { 126 | 127 | var event = new CustomEvent("storesAcquired", { 'detail': json }); 128 | dispatchEvent(event); 129 | 130 | var icon = new L.Icon.Default(); 131 | 132 | json.forEach(function(store) { 133 | var ll = store.ll; 134 | 135 | if (ll[0] && ll[1]) { // if we have valid coordinates 136 | var marker = L.marker(store.ll, {alt: store.name, icon: icon}).addTo(map); 137 | marker.bindPopup("" + store.name + " - " + store.postal_address); 138 | } 139 | }); 140 | 141 | }).catch(function(err) { 142 | console.error(err); 143 | }); 144 | } 145 | 146 | function checkStatus(response) { 147 | if (response.status >= 200 && response.status < 300) { 148 | return response; 149 | } else { 150 | var error = new Error(response.statusText); 151 | error.response = response; 152 | throw error; 153 | } 154 | } 155 | 156 | function parseJSON(response) { 157 | return response.json(); 158 | } 159 | 160 | })(); 161 | -------------------------------------------------------------------------------- /app/assets/javascripts/nprogress-config.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | NProgress.configure({ minimum: 0.2, trickleRate: 0.1, trickleSpeed: 500 }); 3 | 4 | $('a:not([target="_blank"])').click(function() { NProgress.start(); }); 5 | $('form').submit(function() { NProgress.start(); }); 6 | 7 | $(window).load(function() { NProgress.done(); }); 8 | }); 9 | 10 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.sass: -------------------------------------------------------------------------------- 1 | // Gems 2 | @import "bootstrap" 3 | @import "bootstrap-responsive" 4 | @import "select2" 5 | 6 | // Vendor 7 | @import "nprogress" 8 | 9 | // App 10 | @import "webstore/base" 11 | 12 | -------------------------------------------------------------------------------- /app/assets/stylesheets/map.sass: -------------------------------------------------------------------------------- 1 | @import "leaflet" 2 | 3 | // Leaflet overrides 4 | 5 | .leaflet-container 6 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif 7 | 8 | .leaflet-control-attribution 9 | color: #008C8C 10 | a 11 | text-decoration: underline 12 | 13 | 14 | // Our own CSS 15 | 16 | html, body 17 | margin: 0 18 | padding: 0 19 | height: 100% 20 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif 21 | 22 | .lightbox 23 | position: fixed 24 | width: 100% 25 | height: 100% 26 | z-index: 1E4 // looks like Leaflet uses z-index up to 1E3 27 | text-align: center 28 | background: #000 29 | opacity: 0.8 30 | color: #FFF 31 | display: flex 32 | align-items: center 33 | justify-content: center 34 | 35 | .wrapper 36 | display: flex 37 | flex-direction: column 38 | align-items: flex-end 39 | justify-content: center 40 | 41 | .content 42 | font-size: 14px 43 | 44 | ul 45 | list-style: none 46 | text-align: right 47 | border-right: 1px solid #FFF 48 | padding-right: 10px 49 | padding-top: 20px 50 | margin: 0 2px 0 51 | 52 | .fade-in 53 | opacity: 0.2 54 | transition: opacity 1s 55 | 56 | &.invisible 57 | opacity: 0 58 | 59 | .close 60 | position: absolute 61 | right: 16px 62 | top: 16px 63 | cursor: pointer 64 | 65 | a 66 | color: #FFF 67 | 68 | h1 69 | font-size: 32px 70 | margin: 0 71 | img 72 | margin-bottom: -10px 73 | margin-right: 10px 74 | 75 | footer 76 | position: absolute 77 | left: 0 78 | bottom: 10px 79 | width: 100% 80 | font-size: 18px 81 | 82 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | protect_from_forgery with: :exception 5 | 6 | force_ssl if: :ssl_configured?, except: :ping 7 | 8 | def ping 9 | render text: "Pong!" 10 | end 11 | 12 | private 13 | 14 | def ssl_configured? 15 | !Rails.env.development? && !Rails.env.test? 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/authentication_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AuthenticationController < CheckoutController 4 | def authentication 5 | render "authentication", locals: { 6 | order: current_order, 7 | authentication: Authentication.new(cart: current_cart), 8 | } 9 | end 10 | 11 | def save_authentication 12 | args = { cart: current_cart }.merge(params[:authentication]) 13 | return if cart_expired?(args) 14 | authentication = Authentication.new(args) 15 | authentication.sign_in_attempt? ? try_sign_in(authentication) : save_credentials(authentication) 16 | end 17 | 18 | private 19 | 20 | def try_sign_in(authentication) 21 | customers = API.authenticate_customer( 22 | email: authentication.email, password: authentication.password, 23 | ) 24 | 25 | session[:current_customers] = customers.to_json 26 | customer = customers.find { |c| c.webstore_id == current_webstore_id } 27 | 28 | if customer 29 | current_webstore_customer.associate_real_customer(customer.id) 30 | save_credentials(authentication) 31 | else 32 | failed_authentication(authentication) 33 | end 34 | end 35 | 36 | def save_credentials(authentication) 37 | authentication.save ? successful_authentication : failed_authentication(authentication) 38 | end 39 | 40 | def successful_authentication 41 | redirect_to next_step 42 | end 43 | 44 | def failed_authentication(authentication) 45 | flash.now[:alert] = t("authentication.bad_email_password") 46 | render "authentication", locals: { 47 | order: current_order, 48 | authentication: authentication, 49 | } 50 | end 51 | 52 | def next_step 53 | delivery_options_path 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /app/controllers/checkout_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CheckoutController < ApplicationController 4 | before_action :set_current_webstore 5 | before_action :set_locale 6 | before_action :webstore_active? 7 | before_action :setup_webstore 8 | before_action :customer_valid? 9 | before_action :cart_missing? 10 | before_action :cart_completed? 11 | 12 | protected 13 | 14 | helper_method def current_webstore 15 | @current_webstore ||= API.webstore(current_webstore_id) 16 | end 17 | 18 | helper_method def current_customers 19 | json = session[:current_customers] || "[]" 20 | customers = JSON.parse(json) 21 | customers.map { |customer| OpenStruct.new(customer).freeze } 22 | end 23 | 24 | helper_method def current_customer 25 | webstore_id = current_webstore_id 26 | current_customers.find { |customer| customer.webstore_id == webstore_id } 27 | end 28 | 29 | helper_method def current_cart 30 | @current_cart ||= begin 31 | current_cart = Cart.find(session[:cart_id]) 32 | current_cart.decorate(decorator_context) if current_cart 33 | end 34 | end 35 | 36 | helper_method def customer_can_switch_account? 37 | !current_cart 38 | end 39 | 40 | def sign_out 41 | session[:current_customers] = nil 42 | redirect_to webstore_path 43 | end 44 | 45 | def flush_current_cart! 46 | cart = current_cart.dup if current_cart 47 | 48 | session.delete(:cart_id) 49 | session.delete(:form_cart_id) 50 | @current_cart = nil 51 | 52 | cart 53 | end 54 | 55 | def current_order 56 | @current_order ||= current_cart.order.decorate(decorator_context) 57 | end 58 | 59 | def current_webstore_customer 60 | @current_webstore_customer ||= current_cart.customer.decorate 61 | end 62 | 63 | def cart_expired?(args) 64 | current_form_cart_id = args.fetch("cart_id") 65 | 66 | if !form_cart_id 67 | session[:form_cart_id] = current_form_cart_id 68 | 69 | elsif form_cart_id != current_form_cart_id 70 | flush_current_cart! 71 | 72 | redirect_to(webstore_path, 73 | alert: "Sorry, this order has expired, please start a new one.") && (return true) 74 | end 75 | 76 | false 77 | end 78 | 79 | def form_cart_id 80 | session[:form_cart_id] 81 | end 82 | 83 | private 84 | 85 | def webstore_active? 86 | return if current_webstore&.active 87 | 88 | redirect_to Figaro.env.marketing_site_url 89 | end 90 | 91 | def setup_webstore 92 | Time.zone = current_webstore.time_zone 93 | end 94 | 95 | def customer_valid? 96 | return unless current_customer && current_customer.webstore_id != current_webstore.id 97 | 98 | sign_out 99 | end 100 | 101 | def cart_missing? 102 | return if current_cart 103 | 104 | redirect_to webstore_path, alert: t("no_ongoing_order") 105 | end 106 | 107 | def cart_completed? 108 | return unless current_cart&.completed? 109 | 110 | redirect_to webstore_path, 111 | alert: "This order has been completed, please start a new one." 112 | end 113 | 114 | def set_current_webstore 115 | current_webstore # fetch the actual web store and make sure it exists 116 | rescue BuckyBox::API::NotFoundError 117 | render layout: false, file: "public/404.html", status: :not_found 118 | end 119 | 120 | def current_webstore_id 121 | params[:webstore_id] 122 | end 123 | 124 | def set_locale 125 | I18n.locale = current_webstore.locale 126 | end 127 | 128 | def decorator_context 129 | { 130 | context: { currency: current_webstore.currency }, 131 | } 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /app/controllers/completed_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CompletedController < CheckoutController 4 | skip_before_action :cart_completed?, only: :completed 5 | 6 | def completed 7 | cart = flush_current_cart! 8 | 9 | if cart 10 | render "completed", locals: { 11 | completed: Completed.new(cart: cart), 12 | cart: cart.decorate( 13 | context: { currency: current_webstore.currency }, 14 | ), 15 | order: cart.order.decorate( 16 | context: { currency: current_webstore.currency }, 17 | ), 18 | } 19 | 20 | else # they likely refreshed the page 21 | redirect_to customer_dashboard_path 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/customise_order_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CustomiseOrderController < CheckoutController 4 | def customise_order 5 | render "customise_order", locals: { 6 | order: current_order, 7 | customise_order: CustomiseOrder.new(cart: current_cart), 8 | extras_list: current_cart.extras_list, 9 | } 10 | end 11 | 12 | def save_order_customisation 13 | args = { cart: current_cart }.merge(params[:customise_order]) 14 | return if cart_expired?(args) 15 | customise_order = CustomiseOrder.new(args) 16 | customise_order.save ? successful_order_customisation : failed_order_customisation(customise_order) 17 | end 18 | 19 | private 20 | 21 | def successful_order_customisation 22 | redirect_to next_step 23 | end 24 | 25 | def next_step 26 | if current_webstore_customer.guest? 27 | authentication_path 28 | else 29 | delivery_options_path 30 | end 31 | end 32 | 33 | def failed_order_customisation(customise_order) 34 | flash[:alert] = t("oops") << t("colon") << 35 | customise_order.errors.values.join(", ").downcase 36 | 37 | render "customise_order", locals: { 38 | order: current_order, 39 | customise_order: customise_order, 40 | extras_list: current_cart.extras_list, 41 | } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/delivery_options_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeliveryOptionsController < CheckoutController 4 | def delivery_options 5 | delivery_options = DeliveryOptions.new(cart: current_cart) 6 | render "delivery_options", locals: { 7 | order: current_order, 8 | delivery_services: DeliveryServiceDecorator.decorate_collection(delivery_options.delivery_services), 9 | delivery_options: delivery_options, 10 | } 11 | end 12 | 13 | def save_delivery_options 14 | args = { cart: current_cart }.merge(params[:delivery_options]) 15 | return if cart_expired?(args) 16 | delivery_options = DeliveryOptions.new(args) 17 | delivery_options.save ? successful_delivery_options : failed_delivery_options(delivery_options) 18 | end 19 | 20 | private 21 | 22 | def successful_delivery_options 23 | redirect_to next_step 24 | end 25 | 26 | def failed_delivery_options(delivery_options) 27 | flash[:alert] = t("oops") << t("colon") << 28 | delivery_options.errors.full_messages.join(", ").downcase 29 | 30 | render "delivery_options", locals: { 31 | order: current_order, 32 | delivery_services: DeliveryServiceDecorator.decorate_collection(delivery_options.delivery_services), 33 | delivery_options: delivery_options, 34 | } 35 | end 36 | 37 | def next_step 38 | payment_options_path 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/map_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class MapController < ApplicationController 4 | layout false 5 | 6 | def index 7 | # show lightbox once a week 8 | return if cookies[:skip_lightbox].to_i == 1 9 | 10 | cookies[:skip_lightbox] = { 11 | value: cookies[:skip_lightbox].nil? ? 0 : 1, 12 | expires: 1.week.from_now, 13 | } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/payment_options_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PaymentOptionsController < CheckoutController 4 | def payment_options 5 | render "payment_options", locals: { 6 | order: current_order, 7 | payment_options: PaymentOptions.new(cart: current_cart), 8 | cart: current_cart, 9 | } 10 | end 11 | 12 | def save_payment_options 13 | args = { cart: current_cart }.merge(params[:payment_options]) 14 | return if cart_expired?(args) 15 | payment_options = PaymentOptions.new(args) 16 | payment_options.save ? successful_payment_options : failed_payment_options(payment_options) 17 | end 18 | 19 | private 20 | 21 | def successful_payment_options 22 | current_cart.run_factory 23 | # webstore_factory = current_cart.run_factory 24 | # customer_sign_in(webstore_factory.customer) # TODO: authenticate new customers here 25 | 26 | redirect_to next_step, notice: t("order_placed") 27 | end 28 | 29 | def failed_payment_options(payment_options) 30 | flash[:alert] = t("oops") << t("colon") << 31 | payment_options.errors.full_messages.join(", ").downcase 32 | 33 | render "payment_options", locals: { 34 | order: current_order, 35 | payment_options: payment_options, 36 | cart: current_cart, 37 | } 38 | end 39 | 40 | def next_step 41 | completed_path 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/session_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SessionController < CheckoutController 4 | skip_before_action :cart_missing? 5 | 6 | def new 7 | redirect_to customer_dashboard_path if current_customer 8 | end 9 | 10 | def create 11 | credentials = params[:session] 12 | result = API.authenticate_customer(credentials) 13 | 14 | if result.empty? 15 | redirect_to customer_sign_in_path, alert: t("authentication.bad_email_password") 16 | else 17 | session[:current_customers] = result.to_json 18 | redirect_to webstore_path 19 | end 20 | end 21 | 22 | def destroy 23 | sign_out 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/store_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class StoreController < CheckoutController 4 | skip_before_action :cart_missing?, only: %i[home start_checkout] 5 | skip_before_action :cart_completed?, only: :home 6 | before_action :cart_present?, only: :home 7 | 8 | def home 9 | home = Home.new( 10 | webstore: current_webstore, 11 | existing_customer: current_customer, 12 | ) 13 | 14 | @current_webstore_customer = home.customer.decorate 15 | products = ProductDecorator.decorate_collection(home.products, context: { webstore: current_webstore }) 16 | 17 | render "home", locals: { products: products } 18 | end 19 | 20 | def start_checkout 21 | checkout = Checkout.new( 22 | webstore_id: current_webstore.id, 23 | existing_customer: current_customer, 24 | ) 25 | 26 | return if cart_expired?("cart_id" => checkout.cart_id) 27 | 28 | @current_webstore_customer = checkout.customer.decorate 29 | 30 | product_id = params[:product_id] 31 | checkout.add_product!(product_id) ? successful_new_checkout(checkout) : failed_new_checkout 32 | end 33 | 34 | private 35 | 36 | def cart_present? 37 | return unless current_cart 38 | 39 | flush_current_cart! 40 | flash.now[:notice] = t("cancelled_order") 41 | end 42 | 43 | def successful_new_checkout(checkout) 44 | session[:cart_id] = checkout.cart_id 45 | redirect_to(*next_step) 46 | end 47 | 48 | def failed_new_checkout 49 | flash[:alert] = t("oops") 50 | redirect_to webstore_path 51 | end 52 | 53 | def next_step 54 | return webstore_path, alert: t("oops") if current_order.invalid? 55 | return customise_order_path if current_order.customisable? 56 | 57 | current_webstore_customer.guest? ? authentication_path : delivery_options_path 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/decorators/cart_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | require_relative "../models/cart" 5 | 6 | class CartDecorator < Draper::Decorator 7 | delegate_all 8 | 9 | def current_balance 10 | object.current_balance.with_currency(context[:currency]) 11 | end 12 | 13 | def closing_balance 14 | object.closing_balance.with_currency(context[:currency]) 15 | end 16 | 17 | def order_price 18 | object.order_price.with_currency(context[:currency]) 19 | end 20 | 21 | def amount_due 22 | object.amount_due.with_currency(context[:currency]) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/decorators/customer_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | require_relative "../models/customer" 5 | 6 | class CustomerDecorator < Draper::Decorator 7 | delegate_all 8 | 9 | GUEST_NAME = "Guest" 10 | 11 | def id 12 | object.existing_customer_id 13 | end 14 | 15 | def balance_threshold 16 | object.balance_threshold.format 17 | end 18 | 19 | def name 20 | object.guest? ? GUEST_NAME : object.name 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/decorators/delivery_service_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | 5 | class DeliveryServiceDecorator < Draper::Decorator 6 | delegate_all 7 | 8 | def instructions 9 | text = object.instructions.delete("\r").gsub(/\n+/, "\n") # we don't want multiple

10 | h.simple_format(text) 11 | end 12 | 13 | def schedule_input_id 14 | "delivery_service-schedule-inputs-#{object.id}" 15 | end 16 | 17 | def start_dates 18 | object.start_dates.map do |human_date, iso_date, attributes| 19 | [human_date, iso_date, attributes.to_h] 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/decorators/order_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | require_relative "../models/order" 5 | 6 | class OrderDecorator < Draper::Decorator 7 | delegate_all 8 | 9 | def product_price 10 | product_price = object.product_price(with_discount: false) 11 | product_price.zero? ? "" : product_price.with_currency(context[:currency]) 12 | end 13 | 14 | def extras_price 15 | object.extras_price(with_discount: false).with_currency(context[:currency]) 16 | end 17 | 18 | def delivery_service_fee 19 | object.delivery_service_fee.with_currency(context[:currency]) 20 | end 21 | 22 | def discount 23 | object.discount.opposite.with_currency(context[:currency]) 24 | end 25 | 26 | def total 27 | total = object.total 28 | total.zero? ? "" : total.with_currency(context[:currency]) 29 | end 30 | 31 | def extras 32 | object.extras_as_objects 33 | end 34 | 35 | def exclusions 36 | object.exclusion_line_items.map(&:name).join(", ") 37 | end 38 | 39 | def substitutions 40 | object.substitution_line_items.map(&:name).join(", ") 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/decorators/product_decorator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | require "crazy_money" 5 | require_relative "../models/product" 6 | 7 | class ProductDecorator < Draper::Decorator 8 | delegate_all 9 | 10 | def price 11 | price = CrazyMoney.new(object.price) 12 | price.zero? ? "" : price.with_currency(context[:webstore].currency) 13 | end 14 | 15 | def order_link 16 | h.start_checkout_path(context[:webstore].id, id) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | def render_flash_messages(flash) 5 | flash.map do |type, message| 6 | content_tag(:div, class: "alert #{alert_class_for(type)}") do 7 | link_to("×", "javascript:void(0)", class: "close", data: { dismiss: "alert" }) << message 8 | end 9 | end.join.html_safe # rubocop:disable Rails/OutputSafety 10 | end 11 | 12 | private def alert_class_for(flash_type) 13 | { 14 | success: "alert-success", 15 | error: "alert-danger", 16 | alert: "alert-warning", 17 | notice: "alert-info", 18 | }[flash_type.to_sym] || flash_type.to_s 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/mailers/.keep -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/models/.keep -------------------------------------------------------------------------------- /app/models/cart.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | require_relative "order" 5 | require_relative "customer" 6 | 7 | class Cart 8 | include Draper::Decoratable 9 | 10 | attr_reader :id 11 | attr_reader :order 12 | attr_reader :customer 13 | attr_reader :webstore_id 14 | 15 | delegate :delivery_service, to: :customer 16 | delegate :existing_customer, to: :customer 17 | 18 | delegate :add_product, to: :order 19 | delegate :extras_list, to: :order 20 | delegate :product, to: :order 21 | delegate :has_extras?, to: :order 22 | delegate :payment_method, to: :order 23 | 24 | def self.find(id, persistence_class = CartPersistence) 25 | persistence = persistence_class.find(id) 26 | persistence.cart if persistence 27 | end 28 | 29 | def initialize(args = {}) 30 | @id = args[:id] 31 | @order = new_order(args) 32 | @customer = new_customer(args) 33 | @webstore_id = args.fetch(:webstore_id) 34 | @persistence_class = args.fetch(:persistence_class, CartPersistence) 35 | end 36 | 37 | def new? 38 | id.nil? 39 | end 40 | 41 | def completed? 42 | @completed 43 | end 44 | 45 | def ==(other) 46 | if new? 47 | object_id == other.object_id 48 | else 49 | id == other.id 50 | end 51 | end 52 | 53 | def save 54 | persistence = find_or_create_persistence 55 | persistence.save(self) and self.id = persistence.id # rubocop:disable Style/AndOr 56 | end 57 | 58 | def webstore 59 | API.webstore(webstore_id) 60 | end 61 | 62 | def stock_list 63 | webstore.line_items 64 | end 65 | 66 | def add_order_information(information) 67 | order.add_information(information) 68 | end 69 | 70 | def payment_list 71 | webstore.payment_options 72 | end 73 | 74 | def has_payment_options? 75 | payment_list.present? 76 | end 77 | 78 | def negative_closing_balance? 79 | closing_balance.negative? 80 | end 81 | 82 | def closing_balance 83 | current_balance - order_price 84 | end 85 | 86 | def current_balance 87 | customer.account_balance 88 | end 89 | 90 | def order_price 91 | order.total 92 | end 93 | 94 | def amount_due 95 | [order_price - current_balance, CrazyMoney.zero].max 96 | end 97 | 98 | def run_factory(factory_class = Factory) 99 | factory = factory_class.assemble(cart: self) 100 | customer_id = factory.customer.id 101 | customer.associate_real_customer(customer_id) 102 | completed! 103 | factory 104 | end 105 | 106 | private 107 | 108 | attr_reader :persistence_class 109 | attr_writer :id 110 | 111 | def new_model(model, args) 112 | args = args.fetch(model, {}) 113 | args = args.merge(cart: self) 114 | 115 | model.to_s.humanize.constantize.new(args) 116 | end 117 | 118 | def new_order(args) 119 | new_model(:order, args) 120 | end 121 | 122 | def new_customer(args) 123 | new_model(:customer, args) 124 | end 125 | 126 | def find_or_create_persistence 127 | persistence_class.find(id) || persistence_class.new 128 | end 129 | 130 | def completed! 131 | @completed = true 132 | save 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /app/models/cart_persistence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "cart" 4 | 5 | class CartPersistence 6 | attr_accessor :serialized_cart, :id 7 | 8 | def self.find(id) 9 | serialized_cart = $redis.get redis_key(id) 10 | return unless serialized_cart 11 | 12 | new(id: id, serialized_cart: serialized_cart) 13 | end 14 | 15 | def self.redis_key(id) 16 | "webstore:CartPersistence:#{id}" 17 | end 18 | 19 | def initialize(args = {}) 20 | args.each { |k, v| send("#{k}=", v) } 21 | end 22 | 23 | def cart 24 | Marshal.load serialized_cart # rubocop:disable Security/MarshalLoad 25 | end 26 | 27 | def save(cart) 28 | self.serialized_cart = Marshal.dump cart 29 | self.id = SecureRandom.uuid unless id 30 | 31 | ttl = 1.week.to_i 32 | $redis.setex redis_key, ttl, serialized_cart 33 | end 34 | 35 | def redis_key 36 | self.class.redis_key id 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/models/checkout/authentication.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../form" 4 | 5 | class Authentication < Form 6 | NEW_CUSTOMER = "new" 7 | EXISTING_CUSTOMER = "returning" 8 | 9 | attribute :email, String 10 | attribute :registered, String, default: NEW_CUSTOMER 11 | attribute :password, String 12 | 13 | validates :email, presence: true, format: /.+@.+\..+/i 14 | 15 | delegate :webstore_id, to: :cart 16 | 17 | def options 18 | [ 19 | [I18n.t("authentication.new_customer"), NEW_CUSTOMER], 20 | [I18n.t("authentication.existing_customer"), EXISTING_CUSTOMER], 21 | ] 22 | end 23 | 24 | def sign_in_attempt? 25 | registered == EXISTING_CUSTOMER || Customer.exists?(email: email) 26 | end 27 | 28 | def to_h 29 | { 30 | email: email, 31 | } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/checkout/checkout.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../cart" 4 | 5 | class Checkout 6 | attr_reader :cart 7 | 8 | delegate :customer, to: :cart 9 | delegate :id, to: :cart, prefix: true 10 | 11 | def initialize(args = {}) 12 | args = defaults.merge(args) 13 | @existing_customer = args.fetch(:existing_customer) 14 | @cart = args[:cart_class].new( 15 | webstore_id: args[:webstore_id], 16 | customer: customer_hash, 17 | ) 18 | 19 | cart.save 20 | end 21 | 22 | def add_product!(product_id) 23 | cart.add_product(product_id) 24 | cart.save 25 | end 26 | 27 | private 28 | 29 | attr_reader :webstore_id 30 | attr_reader :existing_customer 31 | 32 | def customer_hash 33 | { existing_customer_id: existing_customer && existing_customer.id } 34 | end 35 | 36 | def defaults 37 | { cart_class: Cart } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/models/checkout/completed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../form" 4 | 5 | class Completed < Form 6 | delegate :webstore, to: :cart 7 | delegate :customer, to: :cart 8 | delegate :name, to: :customer, prefix: true 9 | delegate :email, to: :customer, prefix: true 10 | delegate :number, to: :customer, prefix: true 11 | delegate :payment_method, to: :cart 12 | delegate :amount_due, to: :cart 13 | delegate :bank_information, to: :webstore 14 | delegate :bank_name, to: :bank_information 15 | delegate :paypal_email, to: :webstore 16 | delegate :currency, to: :webstore 17 | 18 | def customer_address 19 | customer.address.join("
") 20 | end 21 | 22 | def product_name 23 | cart.order.product.name 24 | end 25 | 26 | def payment_recurring? 27 | cart.order.recurring? 28 | end 29 | 30 | def order_frequency 31 | cart.order.frequency 32 | end 33 | 34 | def amount_due_without_symbol 35 | undecorated_cart = cart.decorated? ? cart.object : cart 36 | undecorated_cart.amount_due 37 | end 38 | 39 | def payment_title 40 | method = payment_method.underscore 41 | method = "paypal_cc" if method == "paypal" # XXX: terrible hack, can't be fucked with that now 42 | 43 | I18n.t(method) 44 | end 45 | 46 | def payment_message 47 | case payment_method 48 | when "bank_deposit" 49 | bank_information.customer_message 50 | when "cash_on_delivery" 51 | webstore.cod_payment_message 52 | end 53 | end 54 | 55 | def bank_account_name 56 | bank_information.account_name 57 | end 58 | 59 | def bank_account_number 60 | bank_information.account_number 61 | end 62 | 63 | def note 64 | bank_information.customer_message 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /app/models/checkout/customise_order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../form" 4 | 5 | class CustomiseOrder < Form 6 | attribute :has_customisations, Boolean 7 | attribute :dislikes, Array[Integer] 8 | attribute :likes, Array[Integer] 9 | attribute :extras, Hash[Integer => Integer] 10 | attribute :add_extra, Boolean 11 | 12 | validate :validate_number_of_exclusions 13 | validate :validate_number_of_substitutions 14 | validate :validate_number_of_extras 15 | 16 | delegate :stock_list, to: :cart 17 | delegate :extras_list, to: :cart 18 | delegate :exclusions_limit, to: :product 19 | delegate :substitutions_limit, to: :product 20 | delegate :extras_limit, to: :product 21 | 22 | def exclusions? 23 | product.dislikes 24 | end 25 | 26 | def substitutions? 27 | product.likes 28 | end 29 | 30 | def extras_allowed? 31 | product.extras_allowed 32 | end 33 | 34 | def extras_unlimited? 35 | product.extras_unlimited 36 | end 37 | 38 | def exclusions_unlimited? 39 | product.exclusions_unlimited 40 | end 41 | 42 | def substitutions_unlimited? 43 | product.substitutions_unlimited 44 | end 45 | 46 | def to_h 47 | { 48 | dislikes: dislikes, 49 | likes: likes, 50 | extras: extras, 51 | } 52 | end 53 | 54 | protected 55 | 56 | def sanitize_attributes(attributes) 57 | attributes.fetch("dislikes", []).delete("") 58 | attributes.fetch("likes", []).delete("") 59 | attributes.fetch("extras", {}).delete_if do |_key, value| 60 | !value.to_i.between?(1, 1000) 61 | end 62 | 63 | attributes 64 | end 65 | 66 | def product 67 | cart.product 68 | end 69 | 70 | def exclusions_count 71 | dislikes.size || 0 72 | end 73 | 74 | def substitutions_count 75 | likes.size || 0 76 | end 77 | 78 | def extras_count 79 | extras.values.reduce(:+) || 0 80 | end 81 | 82 | private 83 | 84 | def validate_number_of_exclusions 85 | validate_number_of(:exclusions) 86 | end 87 | 88 | def validate_number_of_substitutions 89 | validate_number_of(:substitutions) 90 | end 91 | 92 | def validate_number_of_extras 93 | validate_number_of(:extras) 94 | end 95 | 96 | def validate_number_of(items) 97 | items_count = send("#{items}_count") 98 | items_unlimited = send("#{items}_unlimited?") 99 | items_limit = send("#{items}_limit") 100 | 101 | return if items_unlimited || items_count <= items_limit 102 | 103 | errors.add(items, "you have too many #{items}, the maximum is #{items_limit}") 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /app/models/checkout/delivery_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../form" 4 | 5 | class DeliveryOptions < Form 6 | attribute :delivery_service, Integer 7 | attribute :start_date, Date 8 | attribute :frequency, String 9 | attribute :days, Hash[Integer => Integer] 10 | attribute :extra_frequency, Boolean 11 | 12 | validates :delivery_service, :start_date, :frequency, presence: true 13 | validates :days, presence: true, if: -> { frequency != "single" } 14 | 15 | delegate :has_extras?, to: :cart, prefix: true 16 | 17 | def existing_delivery_service_id 18 | customer.delivery_service_id 19 | end 20 | 21 | def can_change_delivery_service? 22 | !existing_delivery_service_id 23 | end 24 | 25 | def delivery_services 26 | API.delivery_services 27 | end 28 | 29 | def delivery_service_list 30 | delivery_services.map { |delivery_service| delivery_service_list_item(delivery_service) } \ 31 | .unshift([I18n.t("delivery_options.select_delivery_service"), nil]) 32 | end 33 | 34 | def single_delivery_service? 35 | delivery_services.one? 36 | end 37 | 38 | def single_delivery_service 39 | delivery_services.last 40 | end 41 | 42 | def order_frequencies 43 | [ 44 | [I18n.t("delivery_options.order_frequencies.select"), nil], 45 | [I18n.t("delivery_options.order_frequencies.weekly"), :weekly], 46 | [I18n.t("delivery_options.order_frequencies.fortnightly"), :fortnightly], 47 | [I18n.t("delivery_options.order_frequencies.monthly"), :monthly], 48 | [I18n.t("delivery_options.order_frequencies.single"), :single], 49 | ] 50 | end 51 | 52 | def extra_frequencies 53 | [ 54 | [I18n.t("delivery_options.extra_frequencies.always"), false], 55 | [I18n.t("delivery_options.extra_frequencies.once"), true], 56 | ] 57 | end 58 | 59 | def to_h 60 | { 61 | delivery_service_id: delivery_service, 62 | start_date: start_date, 63 | frequency: frequency, 64 | days: days, 65 | extra_frequency: extra_frequency, 66 | } 67 | end 68 | 69 | private 70 | 71 | def customer 72 | cart.customer 73 | end 74 | 75 | def delivery_service_list_item(delivery_service) 76 | [delivery_service.name_days_and_fee, delivery_service.id] 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /app/models/checkout/home.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../customer" 4 | 5 | class Home 6 | def initialize(args = {}) 7 | @webstore = args.fetch(:webstore) 8 | @existing_customer = args.fetch(:existing_customer) 9 | end 10 | 11 | def products 12 | @products ||= API.boxes 13 | end 14 | 15 | def customer(customer_class = Customer) 16 | @customer ||= customer_class.new( 17 | existing_customer_id: existing_customer && existing_customer.id, 18 | ) 19 | end 20 | 21 | private 22 | 23 | attr_reader :webstore 24 | attr_reader :existing_customer 25 | end 26 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/customer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | 5 | class Customer 6 | include Draper::Decoratable 7 | 8 | attr_reader :cart 9 | attr_reader :existing_customer_id 10 | 11 | GUEST_HALTED = false 12 | GUEST_DISCOUNTED = false 13 | GUEST_ACTIVE = false 14 | 15 | delegate :balance_threshold, to: :existing_customer 16 | 17 | def self.exists?(args) 18 | API.customers(args).present? 19 | end 20 | 21 | def self.find(id) 22 | API.customer(id, embed: "address") 23 | end 24 | 25 | def initialize(args = {}) 26 | @cart = args.fetch(:cart, nil) 27 | @existing_customer_id = args.fetch(:existing_customer_id, nil) 28 | end 29 | 30 | def fetch(key, default_value = nil) 31 | send(key) || default_value 32 | end 33 | 34 | def guest? 35 | !existing_customer 36 | end 37 | 38 | def existing_customer 39 | self.class.find(existing_customer_id) if existing_customer_id 40 | end 41 | 42 | def associate_real_customer(customer_id) 43 | @existing_customer_id = customer_id 44 | end 45 | 46 | def halted? 47 | guest? ? GUEST_HALTED : existing_customer.halted? 48 | end 49 | 50 | def discount? 51 | guest? ? GUEST_DISCOUNTED : existing_customer.discount? 52 | end 53 | 54 | def active? 55 | guest? ? GUEST_ACTIVE : existing_customer.active? 56 | end 57 | 58 | def name 59 | existing_customer.name unless guest? 60 | end 61 | 62 | def email 63 | existing_customer.email unless guest? 64 | end 65 | 66 | def delivery_service_id 67 | existing_customer.delivery_service_id if existing_customer.present? && existing_customer.delivery_service_id.present? 68 | end 69 | 70 | def address 71 | existing_customer ? Address.new(existing_customer.address.to_h) : NullObject.new 72 | end 73 | 74 | def account_balance 75 | existing_customer ? existing_customer.account_balance : CrazyMoney.zero 76 | end 77 | 78 | def number 79 | raise "No number for guest customer" if guest? 80 | 81 | existing_customer.number 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /app/models/customer_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CustomerFactory 4 | CustomerWrapper = Struct.new( 5 | :id, :address, :via_webstore, :email, :delivery_service_id, 6 | :first_name, :last_name 7 | ) 8 | 9 | AddressWrapper = Struct.new( 10 | :address_1, :address_2, :suburb, :city, :postcode, :delivery_note, 11 | :home_phone, :mobile_phone, :work_phone 12 | ) 13 | 14 | def self.assemble(args) 15 | customer_factory = new(args) 16 | customer_factory.assemble 17 | end 18 | 19 | def initialize(args) 20 | @cart = args.fetch(:cart) 21 | derive_data 22 | end 23 | 24 | def assemble 25 | prepare_address 26 | prepare_customer 27 | API.create_or_update_customer(customer.to_json) 28 | end 29 | 30 | private 31 | 32 | attr_reader :cart 33 | attr_reader :customer 34 | attr_reader :order 35 | attr_reader :information 36 | attr_reader :address 37 | 38 | def assign_attributes_to_object(object, attributes) 39 | attributes.each do |attribute| 40 | value = send(attribute) 41 | object.public_send("#{attribute}=", value) if value 42 | end 43 | 44 | object 45 | end 46 | 47 | def prepare_address 48 | address.public_send("#{phone_type}_phone=", phone_number) if phone_number && phone_type.present? 49 | assign_attributes_to_object(address, %i[address_1 address_2 suburb city postcode delivery_note]) 50 | end 51 | 52 | def prepare_customer 53 | customer.id = existing_customer_id if existing_customer_id 54 | customer.address = address 55 | customer.via_webstore = true 56 | assign_attributes_to_object(customer, %i[email delivery_service_id first_name last_name]) 57 | end 58 | 59 | def phone_number 60 | information[:phone_number] 61 | end 62 | 63 | def phone_type 64 | information[:phone_type] 65 | end 66 | 67 | def address_1 68 | information[:address_1] 69 | end 70 | 71 | def address_2 72 | information[:address_2] 73 | end 74 | 75 | def suburb 76 | information[:suburb] 77 | end 78 | 79 | def city 80 | information[:city] 81 | end 82 | 83 | def postcode 84 | information[:postcode] 85 | end 86 | 87 | def delivery_note 88 | information[:delivery_note] 89 | end 90 | 91 | def email 92 | information[:email] 93 | end 94 | 95 | def delivery_service_id 96 | information[:delivery_service_id] 97 | end 98 | 99 | def name 100 | information[:name].split(" ", 2) 101 | end 102 | 103 | def first_name 104 | name.first 105 | end 106 | 107 | def last_name 108 | name.last 109 | end 110 | 111 | def derive_data 112 | @customer = CustomerWrapper.new 113 | @address = AddressWrapper.new 114 | @order = cart.order 115 | @information = order.information 116 | end 117 | 118 | def existing_customer_id 119 | cart.customer.existing_customer_id 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /app/models/factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "order_factory" 4 | require_relative "customer_factory" 5 | 6 | class Factory 7 | attr_reader :cart 8 | attr_reader :customer 9 | attr_reader :order 10 | 11 | def self.assemble(args) 12 | webstore_factory = new(args) 13 | webstore_factory.assemble 14 | end 15 | 16 | def initialize(args) 17 | @cart = args.fetch(:cart) 18 | end 19 | 20 | def assemble 21 | assemble_customer 22 | assemble_order 23 | 24 | @cart.save 25 | 26 | self 27 | end 28 | 29 | private 30 | 31 | def assemble_customer 32 | @customer = CustomerFactory.assemble(cart: @cart) 33 | end 34 | 35 | def assemble_order 36 | @order = OrderFactory.assemble(cart: @cart, customer: @customer) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/models/form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "virtus" 4 | require "active_model/naming" 5 | require "active_model/conversion" 6 | require "active_model/callbacks" 7 | require "active_model/validator" 8 | require "active_model/validations" 9 | require "active_model/translation" 10 | 11 | class Form 12 | extend ActiveModel::Naming 13 | 14 | include Virtus.model 15 | include ActiveModel::Conversion 16 | include ActiveModel::Validations 17 | 18 | attribute :cart 19 | 20 | delegate :id, to: :cart, prefix: true 21 | 22 | def initialize(attributes = {}) 23 | attributes = sanitize_attributes(attributes) 24 | before_standard_initialize(attributes) 25 | super 26 | after_standard_initialize(attributes) 27 | end 28 | 29 | def save 30 | return false unless valid? 31 | 32 | cart.add_order_information(self) 33 | cart.save 34 | end 35 | 36 | # So SimpleForm infers the right paths and POST method 37 | def persisted? 38 | false 39 | end 40 | 41 | # Overwrite i18n_scope from activemodel/lib/active_model/translation.rb 42 | def self.i18n_scope 43 | :virtus 44 | end 45 | 46 | protected 47 | 48 | def sanitize_attributes(attributes) 49 | attributes 50 | end 51 | 52 | def before_standard_initialize(attributes) 53 | # just a NO OP hook 54 | end 55 | 56 | def after_standard_initialize(attributes) 57 | # just a NO OP hook 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/models/order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | require_relative "order_price" 5 | 6 | class Order 7 | include Draper::Decoratable 8 | 9 | attr_reader :cart 10 | attr_reader :product_id 11 | attr_reader :information 12 | 13 | delegate :name, to: :product, prefix: true 14 | delegate :description, to: :product, prefix: true 15 | delegate :name, to: :delivery_service, prefix: true 16 | 17 | def initialize(args = {}) 18 | @cart = args.fetch(:cart) 19 | @information = args.fetch(:information, {}) 20 | @product_id = args.fetch(:product_id, nil) 21 | end 22 | 23 | def add_product(product_id) 24 | @product_id = product_id 25 | end 26 | 27 | def add_information(new_information) 28 | information.merge!(new_information.to_h) 29 | end 30 | 31 | def has_exclusions? 32 | exclusions.present? 33 | end 34 | 35 | def has_substitutions? 36 | substitutions.present? 37 | end 38 | 39 | def has_extras? 40 | extras.present? 41 | end 42 | 43 | def is_scheduled? 44 | frequency 45 | end 46 | 47 | def has_total? 48 | !information.empty? 49 | end 50 | 51 | def has_discount? 52 | customer.discount? 53 | end 54 | 55 | def for_halted_customer? 56 | customer.halted? 57 | end 58 | 59 | def exclusion_line_items 60 | cart.stock_list.select { |line_item| line_item.id.in?(exclusions) } 61 | end 62 | 63 | def substitution_line_items 64 | cart.stock_list.select { |line_item| line_item.id.in?(substitutions) } 65 | end 66 | 67 | def extras_as_objects 68 | extra_ids = extras ? extras.keys : [] 69 | product.extras.select { |extra| extra_ids.include?(extra.id) } 70 | end 71 | 72 | def total(with_discount: true) 73 | product_price(with_discount: with_discount) + 74 | extras_price(with_discount: with_discount) + 75 | delivery_service_fee 76 | end 77 | 78 | def discount 79 | total(with_discount: false) - total(with_discount: true) 80 | end 81 | 82 | def product_price(order_price_class = ::OrderPrice, with_discount: true) 83 | customer = with_discount ? existing_customer : nil 84 | order_price_class.discounted(product.price, customer) 85 | end 86 | 87 | def extras_price(order_price_class = ::OrderPrice, with_discount: true) 88 | customer = with_discount ? existing_customer : nil 89 | order_price_class.extras_price(extras_as_hashes, customer) 90 | end 91 | 92 | def delivery_service_fee 93 | delivery_service ? delivery_service.fee : 0 94 | end 95 | 96 | def product 97 | API.box(product_id) 98 | end 99 | 100 | def extras_list 101 | product.extras 102 | end 103 | 104 | def product_image 105 | product.images.webstore 106 | end 107 | 108 | def extra_quantity(extra) 109 | extras[extra.id] 110 | end 111 | 112 | def schedule(schedule_builder_class = ScheduleRule) 113 | schedule_builder_class.new( 114 | start_date: start_date, 115 | frequency: frequency, 116 | days: days, 117 | ) 118 | end 119 | 120 | def exclusions 121 | information[:dislikes] 122 | end 123 | 124 | def substitutions 125 | information[:likes] 126 | end 127 | 128 | def extras 129 | information[:extras] 130 | end 131 | 132 | def extra_frequency 133 | information[:extra_frequency] 134 | end 135 | 136 | def payment_method 137 | information[:payment_method] 138 | end 139 | 140 | def customisable? 141 | product.customizable 142 | end 143 | 144 | def invalid? 145 | product # fetch the actual product and make sure it exists 146 | false 147 | rescue BuckyBox::API::NotFoundError 148 | true 149 | end 150 | 151 | def delivery_service 152 | API.delivery_service(delivery_service_id) if delivery_service_id 153 | end 154 | 155 | def pickup_point? 156 | delivery_service.pickup_point 157 | end 158 | 159 | def recurring? 160 | frequency != "single" 161 | end 162 | 163 | def customer 164 | cart ? cart.customer : Customer.new 165 | end 166 | 167 | def frequency 168 | information[:frequency] 169 | end 170 | 171 | private 172 | 173 | attr_writer :product 174 | attr_writer :information 175 | 176 | def existing_customer 177 | cart ? cart.existing_customer : nil 178 | end 179 | 180 | def extras_as_hashes 181 | extras_as_objects.each_with_object([]) do |extra, array| 182 | count = extra_quantity(extra) 183 | array << extra.to_hash.with_indifferent_access.merge(count: count) 184 | end 185 | end 186 | 187 | def start_date 188 | information[:start_date] 189 | end 190 | 191 | def days 192 | information[:days] 193 | end 194 | 195 | def delivery_service_id 196 | information[:delivery_service_id] 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /app/models/order_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OrderFactory 4 | OrderWrapper = Struct.new( 5 | :customer_id, :box_id, :extras, :extras_one_off, :exclusions, :completed, 6 | :substitutions, :payment_method, :frequency, :start_date, :week_days, :week 7 | ) 8 | 9 | def self.assemble(args) 10 | order_factory = new(args) 11 | order_factory.assemble 12 | end 13 | 14 | def initialize(args) 15 | @cart = args.fetch(:cart) 16 | @customer = args[:customer] 17 | @order = OrderWrapper.new 18 | derive_data 19 | end 20 | 21 | def assemble 22 | prepare_order 23 | API.create_order(order.to_json) 24 | order 25 | end 26 | 27 | private 28 | 29 | attr_reader :cart 30 | attr_reader :customer 31 | attr_reader :order 32 | attr_reader :webstore_order 33 | 34 | def prepare_order 35 | order.customer_id = customer_id 36 | order.box_id = product_id 37 | order.extras = extras 38 | order.extras_one_off = extras_one_off 39 | order.exclusions = exclusions 40 | order.substitutions = substitutions 41 | order.payment_method = payment_method 42 | schedule_rule.to_h.each { |k, v| order.public_send("#{k}=", v) } 43 | order.completed = true 44 | order 45 | end 46 | 47 | def product_id 48 | webstore_order.product_id 49 | end 50 | 51 | def customer_id 52 | customer.id 53 | end 54 | 55 | def schedule_rule 56 | webstore_order.schedule 57 | end 58 | 59 | def extras 60 | extra_ids_and_counts.map do |id, count| 61 | { 62 | id: id, 63 | quantity: count, 64 | } 65 | end 66 | end 67 | 68 | def extra_ids_and_counts 69 | webstore_order.extras || {} 70 | end 71 | 72 | def extras_one_off 73 | webstore_order.extra_frequency 74 | end 75 | 76 | def exclusions 77 | webstore_order.exclusions 78 | end 79 | 80 | def substitutions 81 | webstore_order.substitutions 82 | end 83 | 84 | def payment_method 85 | webstore_order.payment_method 86 | end 87 | 88 | def derive_data 89 | @webstore_order = cart.order 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /app/models/order_price.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OrderPrice 4 | def self.discounted(price, customer) 5 | return price if customer.nil? 6 | 7 | customer_discount = BigDecimal(customer.discount.to_s) 8 | price * (1 - customer_discount) 9 | end 10 | 11 | def self.extras_price(order_extras, customer) 12 | total_price = order_extras.sum do |order_extra| 13 | CrazyMoney.new(order_extra.fetch(:price)) * order_extra.fetch(:count) 14 | end 15 | 16 | discounted(total_price, customer) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/models/product.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | 5 | class Product 6 | include Draper::Decoratable 7 | end 8 | -------------------------------------------------------------------------------- /app/services/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class API 4 | class << self 5 | WEBSTORE_ID_FORMAT = /\A[a-z0-9\-_]+\Z/ 6 | 7 | def webstore(id) 8 | raise BuckyBox::API::NotFoundError if id !~ WEBSTORE_ID_FORMAT 9 | 10 | webstore_id(id) 11 | api.webstore 12 | end 13 | 14 | def method_missing(method, *args) 15 | api.respond_to?(method) ? api.public_send(method, *args) : super 16 | end 17 | 18 | def respond_to_missing?(*args) 19 | api.respond_to?(*args) 20 | end 21 | 22 | private 23 | 24 | def webstore_id(id = nil) 25 | Thread.current[:webstore_id] = id unless id.nil? 26 | Thread.current[:webstore_id] 27 | end 28 | 29 | def api 30 | @api ||= {} 31 | @api[webstore_id] ||= BuckyBox::API.new( 32 | credentials.merge("Webstore-ID" => webstore_id), 33 | ) 34 | end 35 | 36 | def credentials 37 | @credentials ||= begin 38 | key = Figaro.env.buckybox_api_key 39 | secret = Figaro.env.buckybox_api_secret 40 | 41 | key ||= "" if Rails.env.test? 42 | secret ||= "" if Rails.env.test? 43 | 44 | if key.nil? || secret.nil? 45 | raise "You must set BUCKYBOX_API_KEY and BUCKYBOX_API_SECRET variables" 46 | end 47 | 48 | { "API-Key" => key, "API-Secret" => secret } 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/views/application/_order.html.haml: -------------------------------------------------------------------------------- 1 | - if order.for_halted_customer? 2 | .alert.alert-error 3 | = t 'halted_customer' 4 | 5 | .row-fluid 6 | #current-order.span12 7 | .row-fluid 8 | .span12.completed-header.important 9 | = t 'order.summary' 10 | 11 | .row-fluid{style: "margin-top: 1em"} 12 | .span12 13 | .section-info-background 14 | 15 | = image_tag(order.product_image) 16 | 17 | %table.section-info 18 | %tbody 19 | %tr#description 20 | %td.description 21 | %h1= order.product_name 22 | = simple_format(order.product_description) 23 | %td.price= order.product_price 24 | 25 | - if order.has_exclusions? 26 | %tr#customisation 27 | %td.description 28 | %h2= t('order.exclusions_and_substitutes') 29 | %table.sub-section-info 30 | %tr 31 | %td.icon 32 | %i.icon-minus-sign 33 | %td{ title: order.exclusions }= order.exclusions 34 | 35 | - if order.has_substitutions? 36 | %tr 37 | %td.icon 38 | %i.icon-plus-sign 39 | %td{ title: order.substitutions }= order.substitutions 40 | %td.price 41 | 42 | - if order.has_extras? 43 | %tr#extras 44 | %td.description 45 | %h2= t 'order.extras' 46 | %table.sub-section-info 47 | - order.extras.each do |extra| 48 | %tr 49 | %td.icon 50 | %i.icon-plus 51 | %td= extra.with_unit 52 | %td.quantity 53 | %span.label= order.extra_quantity(extra) 54 | %td.price= order.extras_price 55 | 56 | - if order.is_scheduled? 57 | %tr#delivery_service 58 | %td.description 59 | %h2= t('delivery_service') 60 | = order.schedule 61 | %br 62 | - if order.pickup_point? 63 | = order.delivery_service_name 64 | - else 65 | = order.customer.address.to_s('
') 66 | %td.price= order.delivery_service_fee 67 | 68 | - if order.has_discount? 69 | %tr#discount 70 | %td.description 71 | %h2= t 'order.discount' 72 | %td.price= order.discount 73 | 74 | - if order.total.present? 75 | %tr#total 76 | %td.description 77 | %h1= t 'order.total' 78 | %td.price 79 | %span= order.total 80 | -------------------------------------------------------------------------------- /app/views/application/_payment_instructions.html.haml: -------------------------------------------------------------------------------- 1 | #webstore-payment-instructions 2 | .row-fluid 3 | .span12.completed-header.important= t 'payment_instructions.account_details' 4 | .row-fluid 5 | .span10.legend 6 | .row-fluid= t 'payment_instructions.existing_balance' 7 | .row-fluid= t 'payment_instructions.this_order' 8 | .row-fluid= t 'payment_instructions.closing_balance' 9 | .span2.prices 10 | .text-right= form_object.current_balance 11 | .text-right= form_object.order_price 12 | .text-right 13 | %span.total= form_object.closing_balance 14 | 15 | -------------------------------------------------------------------------------- /app/views/application/_sidebar.html.haml: -------------------------------------------------------------------------------- 1 | - if current_webstore.company_team_image 2 | .row-fluid 3 | = image_tag(current_webstore.company_team_image, class: 'img-polaroid span12') 4 | 5 | .row-fluid 6 | .span12 7 | %h1#webstore_name= current_webstore.name 8 | .webstore-information-section 9 | %h4= current_webstore.city 10 | 11 | - unless current_webstore.sidebar_description.blank? 12 | .webstore-information-section 13 | = simple_format(current_webstore.sidebar_description) 14 | 15 | .webstore-information-section 16 | - unless current_webstore.facebook_url.blank? 17 | .row-fluid.facebook 18 | = link_to "https://#{current_webstore.facebook_url}", id: 'facebook-link', target: "_blank" do 19 | .span2 20 | %i.facebook-icon 21 | .span10 22 | %div= I18n.t("sidebar.find_us_on_facebook") 23 | - unless current_webstore.phone.blank? 24 | .row-fluid.phone 25 | = link_to "tel:#{current_webstore.phone.gsub(/[^\d\+]/ , '')}" do 26 | .span2 27 | %i.phone-icon 28 | .span10 29 | %div= current_webstore.phone 30 | -------------------------------------------------------------------------------- /app/views/authentication/authentication.html.haml: -------------------------------------------------------------------------------- 1 | = render partial: 'order', object: order 2 | 3 | = simple_form_for(authentication, url: authentication_path) do |f| 4 | = f.input :cart_id, as: :hidden 5 | 6 | #webstore-login.row-fluid.webstore-section 7 | .span12 8 | .row-fluid 9 | .ten.columns= f.input :email, label: t('authentication.enter_email'), input_html: { class: 'span12', required: true } 10 | #registered.row-fluid 11 | .ten.columns= f.input :registered, label: false, collection: authentication.options, as: :radio_buttons 12 | #password-field.row-fluid 13 | .ten.columns= f.input :password, label: t('authentication.enter_password'), as: :password, input_html: { class: 'span12' } 14 | .row-fluid 15 | .ten.columns 16 | %i.icon-lock 17 | = link_to t('password_forgotten?'), "https://my.buckybox.com/customers/password/new?distributor=#{authentication.webstore_id}", target: '_blank' 18 | 19 | .row-fluid.webstore-section 20 | .span12= f.button :submit, t('next'), class: 'pull-right btn btn-process' 21 | -------------------------------------------------------------------------------- /app/views/completed/completed.html.haml: -------------------------------------------------------------------------------- 1 | = render partial: 'payment_instructions', locals: { form_object: cart } 2 | 3 | - if cart.negative_closing_balance? && cart.has_payment_options? 4 | .row-fluid.spacer 5 | #webstore-payment.span12 6 | .row-fluid 7 | #payment.span12 8 | .row-fluid 9 | .span12.completed-header.important 10 | %span= t('completed.amount_due') 11 | %span.pull-right= cart.amount_due 12 | .row-fluid 13 | .span12.text-center.payment-title 14 | = t('completed.pay_by') 15 | = completed.payment_title 16 | .row-fluid 17 | .span9.center-row= render partial: "payments/#{cart.payment_method}", locals: { form_object: completed } 18 | 19 | .row-fluid 20 | .span12.text-center 21 | = link_to t('completed.back_to_webstore'), webstore_path(completed.webstore.id), class: 'btn btn-process', id: 'webstore' 22 | = link_to t('completed.my_account'), customer_dashboard_path, class: 'btn btn-process', id: 'webstore-my-account' 23 | 24 | .spacer 25 | 26 | -------------------------------------------------------------------------------- /app/views/customise_order/_extra.html.haml: -------------------------------------------------------------------------------- 1 | %tr.extras-row 2 | %td.information= extra.with_price_per_unit 3 | %td.cost.text-right= extra.formatted_price 4 | %td.quantity.text-right= g.input "#{extra.id}", input_html: { min: 0, value: 0, class: 'input-text span12' }, as: :integer, label: false 5 | -------------------------------------------------------------------------------- /app/views/customise_order/customise_order.html.haml: -------------------------------------------------------------------------------- 1 | = render partial: 'order', object: order 2 | 3 | = simple_form_for(customise_order, url: customise_order_path, html: { class: 'form-inline' }) do |f| 4 | = f.input :cart_id, as: :hidden 5 | 6 | - if customise_order.exclusions? 7 | #webstore-customise.row-fluid.webstore-section 8 | .span12 9 | .row-fluid 10 | .span12= f.input :has_customisations, as: :boolean, label: t('customise_order.customise_product'), wrapper: :inline_checkbox, wrapper_html: { class: 'checkbox-header' } 11 | #webstore-customisations.row-fluid 12 | .span12 13 | .dislikes_input.clearfix 14 | = f.input :dislikes, label: "".html_safe, collection: customise_order.stock_list, input_html: { multiple: true, data: { placeholder: t('customise_order.exclude_items'), limits: customise_order.exclusions_limit} }, wrapper: :inline 15 | - if customise_order.substitutions? 16 | .likes_input.clearfix 17 | = f.input :likes, label: "".html_safe, collection: customise_order.stock_list, input_html: { multiple: true, data: { placeholder: t('customise_order.substitute_with_items'), limits: customise_order.substitutions_limit} }, wrapper: :inline 18 | 19 | - if customise_order.extras_allowed? 20 | #webstore-extras.row-fluid.webstore-section 21 | .span12 22 | .row-fluid 23 | .span12 24 | %h4 25 | %i.icon-plus 26 | - if customise_order.extras_unlimited? 27 | = t 'customise_order.add_unlimited_extras' 28 | - else 29 | = t 'customise_order.add_n_extras', limit: customise_order.extras_limit 30 | 31 | .row-fluid 32 | .span12 33 | %table.table.table-bordered 34 | = f.simple_fields_for(:extras) do |g| 35 | %tbody 36 | = render partial: 'extra', collection: extras_list, locals: { g: g } 37 | 38 | = f.input :add_extra, label: false, collection: extras_list, label_method: :with_price_per_unit, input_html: { class: 'span12' } 39 | 40 | .row-fluid.webstore-section 41 | .span12= f.button :submit, t('next'), class: 'pull-right btn btn-process' 42 | -------------------------------------------------------------------------------- /app/views/delivery_options/_delivery_service.html.haml: -------------------------------------------------------------------------------- 1 | .row-fluid.delivery_service-schedule-inputs{ id: delivery_service.schedule_input_id } 2 | .span12 3 | -# = f.input :start_date, label: false, collection: form_object.start_dates(delivery_service), input_html: { class: 'schedule-start-date span12' }, include_blank: false, disabled: true 4 | -# NOTE: cannot use SimpleForm here, see https://github.com/plataformatec/simple_form/issues/1104 5 | .control-group.select.required.disabled.webstore_delivery_options_start_date 6 | .controls 7 | = select_tag "delivery_options[start_date]", options_for_select(delivery_service.start_dates), class: "select required disabled schedule-start-date span12" 8 | 9 | %h4= t('delivery_options.repeat_delivery') 10 | = f.input :frequency, label: false, collection: form_object.order_frequencies, input_html: { class: 'delivery_service-schedule-frequency span12' }, include_blank: false, disabled: true 11 | 12 | = f.simple_fields_for(:days) do |g| 13 | %table.order-days.table.table-bordered 14 | %tbody 15 | - delivery_service.dates_grid.each_with_index do |week, index| 16 | %tr 17 | %td= index.succ.ordinalize 18 | - week.each do |day, number| 19 | - day_en = I18n.t('date.abbr_day_names', locale: :en)[number % 7] 20 | %td= g.input "#{number}", label: day, input_html: { data: ({enabled: true} if delivery_service.public_send(day_en.downcase)) }, wrapper_html: { id: "day-#{number}" }, as: :boolean, disabled: true 21 | 22 | -------------------------------------------------------------------------------- /app/views/delivery_options/delivery_options.html.haml: -------------------------------------------------------------------------------- 1 | = render partial: 'order', object: order 2 | 3 | = simple_form_for(delivery_options, url: delivery_options_path) do |f| 4 | = f.input :cart_id, as: :hidden 5 | 6 | #webstore-delivery_service.row-fluid.webstore-section{ style: "display: #{delivery_options.can_change_delivery_service? ? 'block' : 'none'}" } 7 | .span12 8 | .row-fluid 9 | .span12 10 | - if delivery_options.single_delivery_service? 11 | %h4= t("delivery_options.#{delivery_options.single_delivery_service.pickup_point ? 'pickup' : 'delivery'}_details") 12 | - else 13 | %h4= t 'delivery_options.pick_delivery_service' 14 | .row-fluid 15 | .span12 16 | - if delivery_options.single_delivery_service? 17 | = f.input :delivery_service, as: :hidden, input_html: { id: 'delivery_service_select', value: delivery_options.single_delivery_service.id} 18 | %strong= delivery_options.single_delivery_service.name 19 | - else 20 | = f.input :delivery_service, collection: delivery_options.delivery_service_list, selected: delivery_options.existing_delivery_service_id, label: t('delivery_options.select_best'), include_blank: false, input_html: { id: 'delivery_service_select', class: 'span12' } 21 | .row-fluid 22 | .span12 23 | - delivery_services.each do |delivery_service| 24 | .delivery_service-info.hide{ id: "delivery_service-info-#{delivery_service.id}" }= delivery_service.instructions 25 | 26 | #webstore-schedule.hide.row-fluid{ class: "#{delivery_options.can_change_delivery_service? ? '' : 'webstore-section'}" } 27 | .span12 28 | .row-fluid 29 | .span12 30 | .row-fluid 31 | .span12 32 | %h4= t 'delivery_options.when' 33 | = render partial: 'delivery_service', collection: delivery_services, locals: { f: f, form_object: delivery_options } 34 | 35 | - if delivery_options.cart_has_extras? 36 | #webstore-extras-frequency.row-fluid 37 | .span12 38 | .row-fluid 39 | .span12 40 | .row-fluid 41 | .span12 42 | %h4= t 'delivery_options.extra_frequency' 43 | .row-fluid 44 | .span12= f.input :extra_frequency, label: false, collection: delivery_options.extra_frequencies, include_blank: false, input_html: { class: 'span12' } 45 | 46 | .row-fluid.webstore-section 47 | .span12= f.button :submit, t('next'), class: 'pull-right btn btn-process' 48 | 49 | -------------------------------------------------------------------------------- /app/views/layouts/_banner.html.haml: -------------------------------------------------------------------------------- 1 | #bucky-box-controls.containter-fluid 2 | .container 3 | .row 4 | .span6 5 | = link_to root_path, title: "Bucky Box, easy-to-use software for your CSA, Vege Box Scheme, or Food Hub" do 6 | = image_tag "bucky-box-powered.png" 7 | 8 | #auth-controls.span6.text-right 9 | - if current_customer 10 | .btn-group 11 | = link_to "#{t('my_account')} (#{current_customer.name})", customer_dashboard_path, class: 'btn btn-inverse' 12 | = link_to '#', { class: 'btn btn-inverse dropdown-toggle', "data-toggle" => "dropdown" } do 13 | %span.caret 14 | .account-dropdown.dropdown-menu.pull-right 15 | .current-account.text-left 16 | %p 17 | %strong= current_customer.webstore_id 18 | %p 19 | = current_customer.name 20 | %br 21 | = current_customer.email 22 | %p.logout.text-right 23 | = link_to t('log_out'), customer_sign_out_path, method: :delete 24 | 25 | - other_accounts = current_customers.reject { |customer| current_customer && customer.id == current_customer.id } 26 | - if other_accounts.any? 27 | .switch-accounts.text-left 28 | %p 29 | %strong Switch Accounts 30 | - other_accounts.each do |customer| 31 | %li 32 | %i.icon-user.icon-white 33 | = link_to customer_can_switch_account? ? webstore_url(customer.webstore_id) : 'javascript:alert("You cannot switch account now because you have an ongoing order.")' do 34 | = customer.webstore_name 35 | 36 | - else 37 | = link_to t('log_in'), customer_sign_in_url(webstore_id: current_webstore.id) 38 | 39 | #public-banner.containter-fluid 40 | .container 41 | .row 42 | = link_to webstore_path(current_webstore.id) do 43 | #company-logo.span12.text-center 44 | - if current_webstore.company_logo 45 | = image_tag(current_webstore.company_logo) 46 | - else 47 | %h1= current_webstore.name 48 | 49 | -------------------------------------------------------------------------------- /app/views/layouts/_bugsnag.html.haml: -------------------------------------------------------------------------------- 1 | - if (Rails.env.production? || Rails.env.staging?) && Figaro.env.bugsnag_api_key.present? 2 | / Bugsnag 3 | = javascript_include_tag "https://d2wy8f7a9ursnm.cloudfront.net/bugsnag-3.min.js", "data-apikey" => Figaro.env.bugsnag_api_key 4 | :javascript 5 | if (typeof Bugsnag !== 'undefined') { 6 | Bugsnag.beforeNotify = function(payload) { 7 | return (navigator.userAgent.match(/MSIE|Trident|Mobile|Googlebot/i) === null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/views/layouts/_common_head.html.haml: -------------------------------------------------------------------------------- 1 | %meta{ charset: "utf-8" } 2 | %meta{ name: "viewport", content: "width=device-width, initial-scale=1" } 3 | 4 | = favicon_link_tag "/public/favicon.ico" 5 | = favicon_link_tag "/public/apple-touch-icon.png", rel: "apple-touch-icon", type: "image/png" 6 | 7 | = render partial: "layouts/bugsnag" 8 | = render partial: 'layouts/pingdom' 9 | -------------------------------------------------------------------------------- /app/views/layouts/_google_analytics.html.haml: -------------------------------------------------------------------------------- 1 | - if Rails.env.production? 2 | - buckybox_tracker = defined?(ga_tracking_id) ? ga_tracking_id : Figaro.env.ga_tracking_id 3 | - webstore_tracker = defined?(current_webstore) && current_webstore.ga_tracking_id 4 | 5 | - if buckybox_tracker.present? || webstore_tracker.present? 6 | / Google Analytics 7 | :javascript 8 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 9 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 10 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 11 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 12 | 13 | - if buckybox_tracker.present? 14 | :javascript 15 | ga('create', '#{buckybox_tracker}', 'auto'); 16 | ga('send', 'pageview'); 17 | 18 | - if webstore_tracker.present? 19 | :javascript 20 | ga('create', '#{webstore_tracker}', 'auto', {'name': 'webstoreTracker'}); 21 | ga('webstoreTracker.send', 'pageview'); 22 | -------------------------------------------------------------------------------- /app/views/layouts/_i18n.html.haml: -------------------------------------------------------------------------------- 1 | :javascript 2 | // custom JS I18n helper - works like Rails' one 3 | I18n = {} 4 | 5 | I18n.translations = #{I18n.t('javascript').to_json.html_safe}; 6 | 7 | I18n.t = function(key) { 8 | var current_scope = I18n.translations; 9 | $.each(key.split("."), function(i, key_part) { 10 | current_scope = current_scope[key_part]; 11 | if (current_scope == undefined) { 12 | return false; 13 | } 14 | }); 15 | return current_scope || "missing translation: #{I18n.locale}.javascript." + key; 16 | }; 17 | -------------------------------------------------------------------------------- /app/views/layouts/_pingdom.html.haml: -------------------------------------------------------------------------------- 1 | - if (Rails.env.production? || Rails.env.staging?) && Figaro.env.pingdom_rum_id.present? 2 | / Pingdom 3 | :javascript 4 | try { 5 | var _prum = [['id', '#{Figaro.env.pingdom_rum_id}'], 6 | ['mark', 'firstbyte', (new Date()).getTime()]]; 7 | (function() { 8 | var s = document.getElementsByTagName('script')[0] 9 | , p = document.createElement('script'); 10 | p.async = 'async'; 11 | p.src = '//rum-static.pingdom.net/prum.min.js'; 12 | s.parentNode.insertBefore(p, s); 13 | })(); 14 | } catch(e) { 15 | // swallow errors, it isn't vital that this code runs 16 | } 17 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | 3 | %html 4 | %head 5 | = render partial: "layouts/common_head" 6 | 7 | %title Bucky Box - #{t('web_store', webstore: current_webstore.name)} 8 | 9 | = csrf_meta_tag 10 | = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true 11 | = render partial: 'layouts/google_analytics' 12 | = render partial: 'layouts/i18n' 13 | = javascript_include_tag 'application', 'data-turbolinks-track' => true, async: true 14 | 15 | %body 16 | = render partial: 'layouts/banner' 17 | 18 | .container 19 | #customer-messages= render_flash_messages(flash) 20 | 21 | .row 22 | #content-information.span3 23 | #information 24 | = render partial: 'sidebar' 25 | #content-body.span9 26 | .row-fluid 27 | .span12= yield 28 | 29 | -------------------------------------------------------------------------------- /app/views/map/index.html.haml: -------------------------------------------------------------------------------- 1 | !!! 5 2 | 3 | %html 4 | %head 5 | = render partial: "layouts/common_head" 6 | 7 | %title Find locally sourced food, CSA Box Schemes powered by Bucky Box 8 | 9 | = stylesheet_link_tag "map" 10 | 11 | = render partial: "layouts/google_analytics", locals: { ga_tracking_id: "UA-21417656-11" } 12 | 13 | %body 14 | %div.lightbox{ tabindex: -1, role: "dialog", "aria-hidden": true } 15 | .wrapper 16 | .content 17 | %h1 18 | = image_tag "map-marker-icon.png", alt: "" 19 | Find locally sourced food 20 | 21 | .content 22 | %ul 23 | %li.fade-in box schemes 24 | %li.fade-in community supported agriculture 25 | %li.fade-in food co-ops 26 | %li.fade-in recipe bags 27 | %li.fade-in raw milk 28 | 29 | %footer.fade-in.invisible 30 | This is a map of web stores powered by 31 | = link_to("Bucky Box", "http://www.buckybox.com", target: "_blank") + "," 32 | %strong an ordering system for local food organisations 33 | %span> . 34 | 35 | %div.close 36 | = image_tag "close-icon.png", alt: "close" 37 | 38 | %div#map{ style: "min-height: 100%" } 39 | = javascript_include_tag "map" 40 | 41 | -------------------------------------------------------------------------------- /app/views/payment_options/payment_options.html.haml: -------------------------------------------------------------------------------- 1 | = render partial: 'order', object: order 2 | 3 | = simple_form_for(payment_options, url: payment_options_path) do |f| 4 | = f.input :cart_id, as: :hidden 5 | 6 | #webstore-address.row-fluid.webstore-section 7 | .span12 8 | - show_address_form = !payment_options.address_complete? || payment_options.pickup_point? 9 | 10 | #existing-address.row-fluid{ :class => ("hide" if show_address_form) } 11 | .span12 12 | .row-fluid 13 | .span12 14 | %h4= t 'payment_options.deliver_to' 15 | .row-fluid 16 | .span12 17 | = payment_options.address.to_s('
') 18 | %br 19 | = link_to t('payment_options.change_delivery_address'), 'javascript:void(0)', id: 'edit-address' 20 | #update-address.row-fluid{ :class => ("hide" unless show_address_form) } 21 | .span12 22 | .row-fluid 23 | .span12 24 | %h4= t 'payment_options.name' 25 | .row-fluid 26 | .span12= f.input :name, label: false, placeholder: t('payment_options.enter_name'), required: true, input_html: { class: 'span12' } 27 | 28 | - if payment_options.collect_phone 29 | .row-fluid 30 | .span12 31 | %h4= t 'payment_options.phone' 32 | .row-fluid 33 | .span9= f.input :phone_number, label: false, placeholder: t('payment_options.enter_phone'), required: payment_options.require_phone, input_html: { class: 'span12', value: payment_options.default_phone_number } 34 | .span3= f.input :phone_type, collection: payment_options.phone_types, label: false, required: payment_options.require_phone, include_blank: false, prompt: "", selected: payment_options.default_phone_type, input_html: { class: 'span12' } 35 | 36 | - unless payment_options.pickup_point? 37 | .row-fluid 38 | .span12 39 | %h4= t 'payment_options.deliver_to' 40 | .row-fluid 41 | .span12 42 | = f.input :address_1, label: false, placeholder: t('payment_options.address.address_1'), input_html: { class: 'span12', required: payment_options.require_address_1 } 43 | = f.input :address_2, label: false, placeholder: t('payment_options.address.address_2'), input_html: { class: 'span12', required: payment_options.require_address_2 } 44 | = f.input :suburb, label: false, placeholder: t('payment_options.address.suburb'), input_html: { class: 'span12', required: payment_options.require_suburb } 45 | .row-fluid 46 | .span9= f.input :city, label: false, placeholder: t('payment_options.address.city'), input_html: { class: 'span12' }, wrapper_html: { id: 'city', required: payment_options.require_city } 47 | .span3= f.input :postcode, label: false, placeholder: t('payment_options.address.postcode'), input_html: { class: 'span12', required: payment_options.require_postcode }, wrapper_html: { id: 'postal_code' } 48 | 49 | - if payment_options.collect_delivery_note 50 | .row-fluid 51 | .span12 52 | %h4= t('payment_options.address.delivery_note') 53 | .row-fluid 54 | .span12 55 | = f.input :delivery_note, as: :text, label: false, placeholder: t('payment_options.address.placeholders.delivery_note'), input_html: { class: 'span12', rows: 3, required: payment_options.require_delivery_note } 56 | 57 | - if cart.has_payment_options? 58 | - if payment_options.existing_customer? 59 | .row-fluid.webstore-section 60 | = render partial: 'payment_instructions', locals: { form_object: cart } 61 | 62 | .row-fluid.webstore-section 63 | #webstore-payment.span12 64 | .row-fluid 65 | #amount-due.span12.completed-header 66 | .row-fluid.important 67 | - if cart.negative_closing_balance? 68 | .span10= t 'payment_options.amount_due' 69 | .span2.text-right.total_price 70 | = cart.amount_due 71 | - else 72 | .span10= t 'payment_options.in_credit' 73 | 74 | .row-fluid 75 | #payment-options.span12 76 | - if cart.negative_closing_balance? 77 | .row-fluid 78 | .form-horizontal.span6.center-row 79 | = f.input :payment_method, as: :radio_buttons, collection: cart.payment_list, checked: payment_options.payment_method, label: t('payment_options.pay_by') << t('colon'), label_html: { class: 'emphasised-label' } 80 | - else 81 | .span12.text-center= t 'payment_options.no_payment' 82 | = f.input :payment_method, as: :hidden, input_html: { value: PaymentOptions::PAID } 83 | 84 | .row-fluid 85 | .span12.text-center 86 | = f.input :complete, as: :hidden, input_html: { value: true } 87 | = f.button :submit, t('payment_options.complete_order'), class: 'btn btn-process' 88 | 89 | -------------------------------------------------------------------------------- /app/views/payments/_bank_deposit.html.haml: -------------------------------------------------------------------------------- 1 | .row-fluid 2 | .span12.payment-message 3 | .row-fluid 4 | .span12 5 | %strong 6 | =t '.deposit_funds' 7 | %span.bank_deposit_name= form_object.bank_name 8 | 9 | .row-fluid 10 | .span12 11 | =t('.account_name') << t('colon') 12 | %span.bank_deposit_account_name= form_object.bank_account_name 13 | %br 14 | %span.bank_deposit_account_number= simple_format(form_object.bank_account_number, {}, wrapper_tag: "span") 15 | %br 16 | =t('.reference') << t('colon') << form_object.customer_number 17 | 18 | .row-fluid 19 | .span12.note 20 | = simple_format(form_object.note, { class: "bank_deposit_customer_message" }, sanitize: false) 21 | 22 | -------------------------------------------------------------------------------- /app/views/payments/_cash_on_delivery.html.haml: -------------------------------------------------------------------------------- 1 | .row-fluid 2 | .span12.payment-message 3 | .row-fluid 4 | .span12 5 | = simple_format(form_object.payment_message, {class: "cash_on_delivery_cod_payment_message"}, sanitize: false) 6 | 7 | -------------------------------------------------------------------------------- /app/views/payments/_paypal.html.haml: -------------------------------------------------------------------------------- 1 | .row-fluid 2 | .span12 3 | .row-fluid 4 | .span12.text-center 5 | -# https://developer.paypal.com/docs/classic/paypal-payments-standard/integration-guide/formbasics/ 6 | -# https://developer.paypal.com/docs/classic/paypal-payments-standard/integration-guide/subscribe_buttons/#id08ADFJ0401I 7 | %form{action: "https://www.paypal.com/cgi-bin/webscr", method: "post"} 8 | %input{type: "hidden", name: "charset", value: "utf-8"} 9 | %input{type: "hidden", name: "currency_code", value: form_object.currency} 10 | %input{type: "hidden", name: "email", value: form_object.customer_email} 11 | %input{type: "hidden", name: "business", value: form_object.paypal_email} 12 | %input{type: "hidden", name: "item_name", value: form_object.product_name} 13 | %input{type: "hidden", name: "quantity", value: 1} 14 | %input{type: "hidden", name: "return", value: customer_dashboard_url} 15 | 16 | -# NOTE: to help reconciliation 17 | %input{type: "hidden", name: "custom", value: form_object.customer_number} 18 | 19 | - if form_object.payment_recurring? 20 | -# subscribe button 21 | %input{type: "hidden", name: "cmd", value: "_xclick-subscriptions"} 22 | 23 | -# a3 - amount to billed each recurrence 24 | %input{type: "hidden", name: "a3", value: form_object.amount_due_without_symbol} 25 | 26 | - recurring_payment_params = PaypalForm.recurring_payment_params(form_object.order_frequency) 27 | %input{type: "hidden", name: "p3", value: recurring_payment_params.p3} 28 | %input{type: "hidden", name: "t3", value: recurring_payment_params.t3} 29 | 30 | -# Set recurring payments until canceled 31 | %input{type: "hidden", name: "src", value: "1"} 32 | 33 | - else 34 | %input{type: "hidden", name: "cmd", value: "_xclick"} 35 | %input{type: "hidden", name: "amount", value: form_object.amount_due_without_symbol} 36 | 37 | %input{type: "submit", name: "submit", class: "btn btn-default paypal-pay-now", value: t('.pay_now')} 38 | 39 | -------------------------------------------------------------------------------- /app/views/session/new.html.haml: -------------------------------------------------------------------------------- 1 | = simple_form_for(:session, url: customer_sign_in_path) do |f| 2 | 3 | .row-fluid 4 | .span12= f.input :email, label: t('authentication.enter_email'), input_html: { required: true } 5 | .row-fluid 6 | .span12= f.input :password, label: t('authentication.enter_password'), as: :password, input_html: { required: true } 7 | 8 | .row-fluid 9 | .span12 10 | = f.button :submit, t('log_in'), class: 'btn btn-process' 11 | %i.icon-lock 12 | = link_to t('password_forgotten?'), "https://my.buckybox.com/customers/password/new?distributor=#{current_webstore.id}", target: '_blank' 13 | 14 | -------------------------------------------------------------------------------- /app/views/store/_product.html.haml: -------------------------------------------------------------------------------- 1 | .webstore-item 2 | .box 3 | = image_tag(product.images.webstore) 4 | 5 | %section 6 | %h4.name= product.name 7 | .description= simple_format product.description 8 | 9 | %aside.clearfix 10 | .price= product.price 11 | = link_to t('product.order'), product.order_link, class: 'btn btn-process' 12 | -------------------------------------------------------------------------------- /app/views/store/home.html.haml: -------------------------------------------------------------------------------- 1 | - raise "#{params[:webstore_id]} != #{current_webstore.id}" if params[:webstore_id] != current_webstore.id 2 | 3 | - cache([request.fullpath, products.map(&:id).hash, products.map(&:updated_at).max]) do 4 | %noscript 5 | .alert.alert-error 6 | = t('no_js') 7 | 8 | .row-fluid 9 | .span12 10 | #webstore-items= render partial: 'product', collection: products 11 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 5 | load Gem.bin_path("bundler", "bundle") 6 | -------------------------------------------------------------------------------- /bin/check_i18n: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | cd "$(dirname $0)/.." 4 | 5 | locales=$(find config/locales/ -type d -printf "%f\n" | tail -n+2) 6 | locales="${locales[@]/en}" # don't check en to en pair 7 | 8 | files=$(find config/locales/en/ -type f -printf "%f\n") 9 | 10 | for locale in $locales; do 11 | for file in $files; do 12 | echo -n "Checking completeness from en/${file} to ${locale}..." 13 | output=$(./vendor/bin/yamlkeysdiff "config/locales/en/$file#en" "config/locales/$locale/$file#$locale" || true) 14 | [ -n "$output" ] && { echo -e "\n${output}"; exit 1; } || echo " OK" 15 | done 16 | done 17 | 18 | -------------------------------------------------------------------------------- /bin/ci: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | export CI=1 4 | export RAILS_ENV=test 5 | export COVERAGE=1 6 | export SPEC_OPTS="--format p" 7 | 8 | output="$(git submodule update --remote)" 9 | echo -n "$output" 10 | test -z "$output" 11 | 12 | bundle install 13 | 14 | bundle exec rubocop -DES 15 | bundle exec brakeman -qz --no-progress --no-pager -w3 16 | bundle exec bundle-audit check --update --ignore CVE-2016-10735 CVE-2019-8331 CVE-2019-16676 CVE-2020-5267 17 | 18 | $(dirname $0)/check_i18n 19 | 20 | bundle exec rspec 21 | bundle exec cucumber 22 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | APP_PATH = File.expand_path("../config/application", __dir__) 5 | require_relative "../config/boot" 6 | require "rails/commands" 7 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative "../config/boot" 5 | require "rake" 6 | Rake.application.run 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "pathname" 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path("..", __dir__) 8 | 9 | Dir.chdir APP_ROOT do 10 | # This script is a starting point to setup your application. 11 | # Add necessary setup steps to this file: 12 | 13 | puts "== Installing dependencies ==" 14 | system "gem install bundler --conservative" 15 | system "bundle check || bundle install" 16 | 17 | # puts "\n== Copying sample files ==" 18 | # unless File.exist?("config/database.yml") 19 | # system "cp config/database.yml.sample config/database.yml" 20 | # end 21 | 22 | puts "\n== Preparing database ==" 23 | system "bin/rake db:setup" 24 | 25 | puts "\n== Removing old logs and tempfiles ==" 26 | system "rm -f log/*" 27 | system "rm -rf tmp/cache" 28 | 29 | puts "\n== Restarting application server ==" 30 | system "touch tmp/restart.txt" 31 | end 32 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require ::File.expand_path("../config/environment", __FILE__) 6 | run Rails.application 7 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("boot", __dir__) 4 | 5 | # Pick the frameworks you want: 6 | require "active_model/railtie" 7 | # require "active_record/railtie" 8 | require "action_controller/railtie" 9 | # require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "sprockets/railtie" 12 | # require "rails/test_unit/railtie" 13 | 14 | # Require the gems listed in Gemfile, including any gems 15 | # you've limited to :test, :development, or :production. 16 | Bundler.require(*Rails.groups) 17 | 18 | module BuckyBox 19 | module Webstore 20 | class Application < Rails::Application 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration should go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded. 24 | 25 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 26 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 27 | # config.time_zone = 'Central Time (US & Canada)' 28 | 29 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 30 | config.i18n.load_path += Dir[Rails.root.join("config", "locales", "**", "*.{rb,yml}").to_s] 31 | 32 | # https://github.com/svenfuchs/rails-i18n 33 | config.i18n.available_locales = Dir[Rails.root.join("config", "locales", "*")] 34 | .select { |path| File.directory?(path) && !File.symlink?(path) } 35 | .map { |directory| File.basename directory } 36 | 37 | config.i18n.default_locale = :en 38 | 39 | # fall back to config.i18n.default_locale translation if key is missing 40 | config.i18n.fallbacks = true 41 | 42 | # http://stackoverflow.com/questions/20361428/rails-i18n-validation-deprecation-warning 43 | config.i18n.enforce_available_locales = true 44 | 45 | # For not swallow errors in after_commit/after_rollback callbacks. 46 | # config.active_record.raise_in_transactional_callbacks = true 47 | 48 | # Custom directories with classes and modules you want to be autoloadable. 49 | config.autoload_paths += Dir["#{config.root}/app/models/checkout"] 50 | config.autoload_paths += Dir["#{config.root}/lib"] 51 | 52 | # Use memory store since we only need to cache a few megabytes 53 | config.cache_store = :memory_store, { compress: false } 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /config/application.yml.example: -------------------------------------------------------------------------------- 1 | # Bucky Box API 2 | BUCKYBOX_API_KEY: "" 3 | BUCKYBOX_API_SECRET: "" 4 | 5 | # BugSnag 6 | BUGSNAG_API_KEY: "" 7 | 8 | # Google Analytics 9 | GA_TRACKING_ID: "" 10 | 11 | # Pingdom 12 | PINGDOM_RUM_ID: "" 13 | 14 | # URLs 15 | MARKETING_SITE_URL: http://www.buckybox.com/ 16 | 17 | # Environment-dependant configuration 18 | production: 19 | # Secret for config/secrets.yml 20 | SECRET_KEY_BASE: "" 21 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 4 | 5 | require "bundler/setup" # Set up gems listed in the Gemfile. 6 | -------------------------------------------------------------------------------- /config/cucumber.yml: -------------------------------------------------------------------------------- 1 | <% 2 | rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" 3 | rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" 4 | std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags 'not @wip'" 5 | %> 6 | default: <%= std_opts %> features 7 | wip: --tags @wip:3 --wip features 8 | rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags 'not @wip' 9 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require File.expand_path("application", __dir__) 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded on 7 | # every request. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports and disable caching. 15 | config.consider_all_requests_local = true 16 | config.action_controller.perform_caching = true 17 | 18 | # Don't care if the mailer can't send. 19 | # config.action_mailer.raise_delivery_errors = false 20 | 21 | # Print deprecation notices to the Rails logger. 22 | config.active_support.deprecation = :log 23 | 24 | # Raise an error on page load if there are pending migrations. 25 | # config.active_record.migration_error = :page_load 26 | 27 | # Debug mode disables concatenation and preprocessing of assets. 28 | # This option may cause significant delays in view rendering with a large 29 | # number of complex assets. 30 | config.assets.debug = true 31 | 32 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 33 | # yet still be able to expire them through the digest params. 34 | config.assets.digest = true 35 | 36 | # Adds additional error checking when serving assets at runtime. 37 | # Checks for improperly declared sprockets dependencies. 38 | # Raises helpful error messages. 39 | config.assets.raise_runtime_errors = true 40 | 41 | # Don't use precompiled assets locally 42 | # http://guides.rubyonrails.org/asset_pipeline.html#local-precompilation 43 | config.assets.prefix = "/dev-assets" 44 | 45 | # Raises error for missing translations 46 | config.action_view.raise_on_missing_translations = true 47 | end 48 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 20 | # Add `rack-cache` to your Gemfile before enabling this. 21 | # For large-scale production use, consider using a caching reverse proxy like NGINX, varnish or squid. 22 | # config.action_dispatch.rack_cache = true 23 | 24 | # Disable Rails's static asset server (Apache or NGINX will already do this). 25 | config.serve_static_files = false 26 | 27 | # Compress JavaScripts and CSS. 28 | # Don't mangle variable names to be idempotent and avoid noise in commits 29 | # config.assets.js_compressor = :uglifier 30 | config.assets.js_compressor = Uglifier.new(mangle: false) 31 | config.assets.css_compressor = :sass 32 | 33 | # Do not fallback to assets pipeline if a precompiled asset is missed. 34 | config.assets.compile = false 35 | 36 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 37 | # yet still be able to expire them through the digest params. 38 | config.assets.digest = true 39 | 40 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 41 | 42 | # Specifies the header that your server uses for sending files. 43 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 44 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 45 | 46 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 47 | # config.force_ssl = true 48 | 49 | # Set to :info to decrease the log volume. 50 | config.log_level = :debug 51 | 52 | # Prepend all log lines with the following tags. 53 | # config.log_tags = [ :subdomain, :uuid ] 54 | 55 | # Use a different logger for distributed setups. 56 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 57 | 58 | # Use a different cache store in production. 59 | # config.cache_store = :mem_cache_store 60 | 61 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 62 | # config.action_controller.asset_host = "http://assets.example.com" 63 | 64 | # Ignore bad email addresses and do not raise email delivery errors. 65 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 66 | # config.action_mailer.raise_delivery_errors = false 67 | 68 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 69 | # the I18n.default_locale when a translation cannot be found). 70 | config.i18n.fallbacks = true 71 | 72 | # Send deprecation notices to registered listeners. 73 | config.active_support.deprecation = :notify 74 | 75 | # Use default logging formatter so that PID and timestamp are not suppressed. 76 | config.log_formatter = ::Logger::Formatter.new 77 | 78 | # Do not dump schema after migrations. 79 | # config.active_record.dump_schema_after_migration = false 80 | end 81 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # The test environment is used exclusively to run your application's 7 | # test suite. You never need to work with it otherwise. Remember that 8 | # your test database is "scratch space" for the test suite and is wiped 9 | # and recreated between test runs. Don't rely on the data there! 10 | config.cache_classes = true 11 | 12 | # Do not eager load code on boot. This avoids loading your whole application 13 | # just for the purpose of running a single test. If you are using a tool that 14 | # preloads Rails for running tests, you may have to set it to true. 15 | config.eager_load = false 16 | 17 | # Configure static asset server for tests with Cache-Control for performance. 18 | config.serve_static_files = true 19 | config.static_cache_control = "public, max-age=3600" 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Tell Action Mailer not to deliver emails to the real world. 32 | # The :test delivery method accumulates sent emails in the 33 | # ActionMailer::Base.deliveries array. 34 | # config.action_mailer.delivery_method = :test 35 | 36 | # Print deprecation notices to the stderr. 37 | config.active_support.deprecation = :stderr 38 | 39 | # Raises error for missing translations 40 | config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Version of your assets, change this if you want to expire all your assets. 6 | Rails.application.config.assets.version = "1.0" 7 | 8 | # Add additional assets to the asset load path 9 | # Rails.application.config.assets.paths << Emoji.images_path 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 13 | Rails.application.config.assets.precompile += %w[map.js map.css] 14 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 9 | # Rails.backtrace_cleaner.remove_silencers! 10 | -------------------------------------------------------------------------------- /config/initializers/bugsnag.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined?(Bugsnag) && Figaro.env.bugsnag_api_key.present? 4 | # :nocov: 5 | Bugsnag.configure do |config| 6 | config.api_key = Figaro.env.bugsnag_api_key 7 | config.notify_release_stages = %w[production staging] 8 | end 9 | # :nocov: 10 | end 11 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure sensitive parameters which will be filtered from the log file. 6 | Rails.application.config.filter_parameters += [:password] 7 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new inflection rules using the following format. Inflections 6 | # are locale specific, and you may define rules for as many different 7 | # locales as you wish. All of these examples are active by default: 8 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 9 | # inflect.plural /^(ox)$/i, '\1en' 10 | # inflect.singular /^(ox)en/i, '\1' 11 | # inflect.irregular 'person', 'people' 12 | # inflect.uncountable %w( fish sheep ) 13 | # end 14 | 15 | # These inflection rules are supported but not enabled by default: 16 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 17 | # inflect.acronym 'RESTful' 18 | # end 19 | 20 | ActiveSupport::Inflector.inflections(:en) do |inflect| 21 | inflect.acronym "API" # so autoload doesn't get confused with 'Api' 22 | end 23 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new mime types for use in respond_to blocks: 6 | # Mime::Type.register "text/richtext", :rtf 7 | -------------------------------------------------------------------------------- /config/initializers/naught.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # https://github.com/avdi/naught 4 | 5 | if defined? Naught 6 | NullObject = Naught.build 7 | BlackHole = Naught.build(&:black_hole) 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/redis.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $redis = Redis.new( 4 | driver: :hiredis, 5 | url: ENV.fetch("REDIS_URL", "redis://localhost:6379/"), 6 | ) 7 | -------------------------------------------------------------------------------- /config/initializers/secure_headers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "secure_headers" 4 | 5 | SecureHeaders::Configuration.default do |config| 6 | config.x_frame_options = "DENY" 7 | 8 | # rubocop:disable Lint/PercentStringArray 9 | config.csp = { 10 | default_src: %w['none'], 11 | img_src: %w['self' data: my.buckybox.com *.google-analytics.com *.pingdom.net notify.bugsnag.com *.tile.openstreetmap.org], 12 | script_src: %w['self' 'unsafe-inline' *.google-analytics.com *.pingdom.net https://d2wy8f7a9ursnm.cloudfront.net/bugsnag-3.min.js], 13 | style_src: %w['self' 'unsafe-inline'], 14 | form_action: %w['self' www.paypal.com], 15 | connect_src: %w['self' api.buckybox.com *.google-analytics.com], 16 | report_uri: %w[https://api.buckybox.com/v1/csp-report], 17 | } 18 | 19 | config.csp[:img_src] << "http://my.buckybox.local:3000" if Rails.env.development? 20 | 21 | # config.hpkp = { 22 | # TODO: set up HPKP 23 | # } 24 | # rubocop:enable Lint/PercentStringArray 25 | end 26 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Rails.application.config.session_store :cookie_store, 6 | key: "_webstore_session", 7 | secure: !Rails.env.development? && !Rails.env.test?, 8 | httponly: true 9 | -------------------------------------------------------------------------------- /config/initializers/simple_form_bootstrap.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # TODO: see if we can get rid of this when upgrading SimpleForm 4 | 5 | # Use this setup block to configure all options available in SimpleForm. 6 | SimpleForm.setup do |config| 7 | config.wrappers :bootstrap, tag: "div", class: "control-group", error_class: "error" do |b| 8 | b.use :html5 9 | b.use :placeholder 10 | b.use :label 11 | b.use :maxlength 12 | b.wrapper tag: "div", class: "controls" do |ba| 13 | ba.use :input 14 | ba.use :error, wrap_with: { tag: "span", class: "help-inline" } 15 | ba.use :hint, wrap_with: { tag: "p", class: "help-block" } 16 | end 17 | end 18 | 19 | config.wrappers :prepend, tag: "div", class: "control-group", error_class: "error" do |b| 20 | b.use :html5 21 | b.use :placeholder 22 | b.use :label 23 | b.use :maxlength 24 | b.wrapper tag: "div", class: "controls" do |input| 25 | input.wrapper tag: "div", class: "input-prepend" do |prepend| 26 | prepend.use :input 27 | end 28 | input.use :hint, wrap_with: { tag: "span", class: "help-block" } 29 | input.use :error, wrap_with: { tag: "span", class: "help-inline" } 30 | end 31 | end 32 | 33 | config.wrappers :append, tag: "div", class: "control-group", error_class: "error" do |b| 34 | b.use :html5 35 | b.use :placeholder 36 | b.use :label 37 | b.use :maxlength 38 | b.wrapper tag: "div", class: "controls" do |input| 39 | input.wrapper tag: "div", class: "input-append" do |append| 40 | append.use :input 41 | end 42 | input.use :hint, wrap_with: { tag: "span", class: "help-block" } 43 | input.use :error, wrap_with: { tag: "span", class: "help-inline" } 44 | end 45 | end 46 | 47 | config.wrappers :inline, tag: "div", error_class: "error" do |b| 48 | b.use :html5 49 | b.use :placeholder 50 | b.use :maxlength 51 | b.wrapper tag: "div", class: "controls" do |ba| 52 | ba.use :label 53 | ba.use :input 54 | ba.use :error, wrap_with: { tag: "span", class: "help-inline" } 55 | ba.use :hint, wrap_with: { tag: "p", class: "help-block" } 56 | end 57 | end 58 | 59 | config.wrappers :inline_checkbox, tag: "div", class: "control-group", error_class: "error" do |b| 60 | b.use :html5 61 | b.use :placeholder 62 | b.wrapper tag: "div", class: "controls" do |ba| 63 | ba.use :input 64 | ba.use :label 65 | ba.use :error, wrap_with: { tag: "span", class: "help-inline" } 66 | ba.use :hint, wrap_with: { tag: "p", class: "help-block" } 67 | end 68 | end 69 | 70 | # Wrappers for forms and inputs using the Twitter Bootstrap toolkit. 71 | # Check the Bootstrap docs (http://twitter.github.com/bootstrap) 72 | # to learn about the different styles for forms and inputs, 73 | # buttons and other elements. 74 | config.default_wrapper = :bootstrap 75 | end 76 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 11 | end 12 | 13 | # To enable root element in JSON for ActiveRecord objects. 14 | # ActiveSupport.on_load(:active_record) do 15 | # self.include_root_in_json = true 16 | # end 17 | -------------------------------------------------------------------------------- /config/locales/de/common.yml: -------------------------------------------------------------------------------- 1 | de: 2 | current_locale_en: German 3 | current_locale: Deutsch 4 | colon: ":" 5 | hi: Hallo 6 | thanks: Danke 7 | back: zurück 8 | cancel: abbrechen 9 | log_in: einloggen 10 | log_out: ausloggen 11 | update: aktualisieren 12 | next: weiter 13 | or: oder 14 | password_forgotten?: Passwort vergessen? 15 | delivery_service: Lieferservice 16 | my_account: Mein Konto 17 | your_account: Ihr Konto 18 | deliver_on: Liefern am 19 | oops: Oh, es ist ein Fehler aufgetreten 20 | no_js: Oh, Du musst JavaScript in deinem Browser einrichten 21 | cash_on_delivery: Barzahlung bei Lieferung 22 | bank_deposit: Bankdepot 23 | paypal_cc: PayPal / Kreditkarte 24 | -------------------------------------------------------------------------------- /config/locales/de/javascript.yml: -------------------------------------------------------------------------------- 1 | de: 2 | javascript: 3 | customise_order: 4 | add_extra: Füge einen weiteren Artikel hinzu 5 | -------------------------------------------------------------------------------- /config/locales/de/simple_form.yml: -------------------------------------------------------------------------------- 1 | de: 2 | simple_form: 3 | 'yes': 'Ja' 4 | 'no': 'Nein' 5 | required: 6 | text: 'erforderlich' 7 | mark: '*' 8 | error_notification: 9 | default_message: "Bitte schau Dir das Problem unten stehend an:" 10 | -------------------------------------------------------------------------------- /config/locales/de/webstore.yml: -------------------------------------------------------------------------------- 1 | de: 2 | web_store: "%{webstore} - Web Shop" 3 | no_ongoing_order: Es gibt noch keine laufende Bestellung, bitte beginne jetzt.. 4 | cancelled_order: 'Deine Bestellung wurde storniert, du kannst nun eine Neue beginnen. ' 5 | order_placed: Deine Bestellung wurde entgegengenommen. 6 | halted_customer: Dein Kontostand ist unter dem Minimum und erlaubt keine neuen Bestellungen, es sei denn, dass der Kontostand bis zum Liefertermin ausgeglichen wurde. 7 | product: 8 | order: Bestellung 9 | order: 10 | summary: Zusammenfassung der Bestellung 11 | exclusions_and_substitutes: Ausschlüsse und Ersatzprodukte 12 | extras: Zusätzliche Waren 13 | discount: Dein Rabatt 14 | total: Gesamt 15 | schedule_rule: 16 | deliver_on: Lieferung am 17 | deliver_weekly_on: Wöchentliche Lieferung am 18 | deliver_fornightly_on: 14-tägige Lieferung am 19 | deliver_monthly_on: Monatliche Lieferung am 20 | no_future_deliveries: 'Keine zukünftigen Lieferungen ' 21 | phone_collection: 22 | mobile_phone: Mobiltelefon 23 | work_phone: Telefon Arbeit 24 | home_phone: Telefon zu Hause 25 | sidebar: 26 | find_us_on_facebook: Find uns auf Facebook 27 | customise_order: 28 | customise_product: Mein Produkt anpassen 29 | exclude_items: Diese Waren weglassen... 30 | substitute_with_items: Dafür mit diesen Waren ersetzen... 31 | add_unlimited_extras: Füge beliebig viele Extrawaren hinzu 32 | add_n_extras: Füge bis %{limit} extra Waren hinzu 33 | authentication: 34 | enter_email: Deine Emailadresse 35 | enter_password: Dein Passwort 36 | bad_email_password: Dein/e Email/Passwort war falsch. 37 | new_customer: Ich bin ein neuer Nutzer 38 | existing_customer: Ich bin bereits Nutzer 39 | delivery_options: 40 | pick_delivery_service: Wähle eine Lieferart 41 | delivery_details: Lieferungs Details 42 | pickup_details: Abhol Details 43 | select_best: Wähle einen Anbieter dem du vertraust. 44 | repeat_delivery: Lieferung/en wiederholen? 45 | when: Wann möchtest du das? 46 | extra_frequency: Was ist mit den extra Waren? 47 | select_delivery_service: "- Wähle einen Lieferservice -" 48 | order_frequencies: 49 | select: "- Wähle die Häufigkeit der Lieferung -" 50 | single: Einmalige Lieferung 51 | weekly: Liefere wöchentlich am... 52 | fortnightly: Lieferung alle 2 Wochen am... 53 | monthly: Lieferung monatlich 54 | extra_frequencies: 55 | always: Füge die extra Waren JEDER Lieferung hinzu 56 | once: Füge die extra Waren nur der NÄCHSTEN Lieferung hinzu 57 | payment_options: 58 | deliver_to: Lieferung an 59 | delivery_note: Lieferungs Hinweise 60 | change_delivery_address: Ändere meine Lieferadresse 61 | name: Dein Name 62 | enter_name: Namen eintragen 63 | phone: Deine Telefonnummer 64 | enter_phone: Telefonnummer eintragen 65 | amount_due: Gesamtbetrag fällig zum 66 | in_credit: Dein Guthaben reicht aus 67 | pay_by: Bezahlen mit 68 | no_payment: Keine Zahlung erforderlich 69 | complete_order: Bestellung abschließen 70 | address: 71 | address_1: Adresse 1 72 | address_2: Adresse 2 73 | suburb: Bezirk / Ortsteil 74 | city: Ort 75 | postcode: Postleitzahl 76 | delivery_note: Lieferungs Hinweis 77 | placeholders: 78 | delivery_note: Lieferungshinweise bzw. Anweisungen wo/wem genau das Paket abgestelt oder ausgehändigt werden soll. 79 | completed: 80 | amount_due: Gesamtbetrag fällig zum 81 | pay_by: Bezahlen mit 82 | back_to_webstore: Zurück zum Web Shop 83 | my_account: Mein Konto 84 | payment_instructions: 85 | account_details: Konto Details 86 | existing_balance: Dein aktuller Kontostand bei uns 87 | this_order: Diese Bestellung 88 | closing_balance: Abschließender Kontostand 89 | payments: 90 | bank_deposit: 91 | deposit_funds: Bitte Kontoguthaben einzahlen an 92 | account_name: Konto Name 93 | reference: Referenz 94 | paypal: 95 | pay_now: Jetzt bezahlen 96 | top_up: Konto aufladen 97 | amount: Betrag 98 | amount_to_top_up: Aufladebetrag 99 | -------------------------------------------------------------------------------- /config/locales/en/common.yml: -------------------------------------------------------------------------------- 1 | en: 2 | current_locale_en: English 3 | current_locale: English 4 | colon: ": " 5 | hi: Hi 6 | thanks: Thanks 7 | back: back 8 | cancel: cancel 9 | log_in: Log in 10 | log_out: Log out 11 | update: Update 12 | next: Next 13 | or: or 14 | password_forgotten?: Lost your password? 15 | delivery_service: Delivery service 16 | my_account: My account 17 | your_account: Your account 18 | deliver_on: Deliver on 19 | oops: Oops there was an issue 20 | no_js: Oops! You must enable JavaScript in your browser to be able to order from this webstore. 21 | cash_on_delivery: Cash on Delivery 22 | bank_deposit: Bank Deposit 23 | paypal_cc: PayPal / Credit Card 24 | -------------------------------------------------------------------------------- /config/locales/en/javascript.yml: -------------------------------------------------------------------------------- 1 | en: 2 | javascript: 3 | customise_order: 4 | add_extra: Add an extra item 5 | -------------------------------------------------------------------------------- /config/locales/en/simple_form.yml: -------------------------------------------------------------------------------- 1 | en: 2 | simple_form: 3 | "yes": 'Yes' 4 | "no": 'No' 5 | required: 6 | text: 'required' 7 | mark: '*' 8 | # You can uncomment the line below if you need to overwrite the whole required html. 9 | # When using html, text and mark won't be used. 10 | # html: '*' 11 | error_notification: 12 | default_message: "Please review the problems below:" 13 | # Labels and hints examples 14 | # labels: 15 | # defaults: 16 | # password: 'Password' 17 | # user: 18 | # new: 19 | # email: 'E-mail to sign in.' 20 | # edit: 21 | # email: 'E-mail.' 22 | # hints: 23 | # defaults: 24 | # username: 'User name to sign in.' 25 | # password: 'No special characters, please.' 26 | 27 | -------------------------------------------------------------------------------- /config/locales/en/webstore.yml: -------------------------------------------------------------------------------- 1 | en: 2 | web_store: "%{webstore} - Web Store" 3 | no_ongoing_order: There is no ongoing order, please start one. 4 | cancelled_order: Your previous order has been cancelled, you can start a new one. 5 | order_placed: Your order has been placed. 6 | halted_customer: Your account is under the minimum balance and will not recieve new orders unless payment is cleared before the packing date. 7 | product: 8 | order: Order 9 | order: 10 | summary: Order summary 11 | exclusions_and_substitutes: Exclusions & substitutes 12 | extras: Extra items 13 | discount: Your discount 14 | total: Total 15 | schedule_rule: 16 | deliver_on: Deliver on 17 | deliver_weekly_on: Deliver weekly on 18 | deliver_fornightly_on: Deliver fortnightly on 19 | deliver_monthly_on: Deliver monthly on the 20 | no_future_deliveries: No future deliveries scheduled 21 | phone_collection: 22 | mobile_phone: Mobile phone 23 | work_phone: Work phone 24 | home_phone: Home phone 25 | sidebar: 26 | find_us_on_facebook: Find us on Facebook 27 | customise_order: 28 | customise_product: Customise my product 29 | exclude_items: Exclude these items... 30 | substitute_with_items: Substitute for these items... 31 | add_unlimited_extras: Add any amount of extra items 32 | add_n_extras: Add up to %{limit} extra items 33 | authentication: 34 | enter_email: Enter your email address 35 | enter_password: Enter your password 36 | bad_email_password: Your email and/or password is incorrect. 37 | new_customer: I'm a new customer 38 | existing_customer: I'm a returning customer 39 | delivery_options: 40 | pick_delivery_service: Pick A Delivery Service 41 | delivery_details: Delivery Details 42 | pickup_details: Pickup Details 43 | select_best: Select the one that best services your location 44 | repeat_delivery: Repeat delivery? 45 | when: When would you like it? 46 | extra_frequency: What about those extra items? 47 | select_delivery_service: "- Select delivery service -" 48 | order_frequencies: 49 | select: "- Select delivery frequency -" 50 | single: Deliver once 51 | weekly: Deliver weekly on... 52 | fortnightly: Deliver every 2 weeks on... 53 | monthly: Deliver monthly 54 | extra_frequencies: 55 | always: Include Extra Items with EVERY delivery 56 | once: Include Extra Items with NEXT delivery only 57 | payment_options: 58 | deliver_to: Deliver to 59 | delivery_note: Delivery note 60 | change_delivery_address: Change my delivery address 61 | name: Your name 62 | enter_name: Enter your name 63 | phone: Your phone number 64 | enter_phone: Enter your phone number 65 | amount_due: Total amount due 66 | in_credit: Your account is in credit 67 | pay_by: Pay by 68 | no_payment: No payment necessary 69 | complete_order: Complete Order 70 | address: 71 | address_1: Address 1 72 | address_2: Address 2 73 | suburb: Suburb 74 | city: City 75 | postcode: Postcode 76 | delivery_note: Delivery note 77 | placeholders: 78 | delivery_note: Delivery instructions such as where to place the package 79 | completed: 80 | amount_due: Total amount due 81 | pay_by: Pay by 82 | back_to_webstore: Back to web store 83 | my_account: My account 84 | payment_instructions: 85 | account_details: Account details 86 | existing_balance: Your existing account balance with us 87 | this_order: This order 88 | closing_balance: Closing balance 89 | payments: 90 | bank_deposit: 91 | deposit_funds: Please deposit funds into 92 | account_name: Account Name 93 | reference: Reference 94 | paypal: 95 | pay_now: Pay now 96 | top_up: Add credit to my account 97 | amount: Amount 98 | amount_to_top_up: Amount to top up 99 | 100 | -------------------------------------------------------------------------------- /config/locales/fr/common.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | current_locale_en: French 3 | current_locale: Français 4 | colon: " : " 5 | hi: Bonjour 6 | thanks: Merci 7 | back: retour 8 | cancel: annuler 9 | log_in: Connexion 10 | log_out: Déconnexion 11 | update: Modifier 12 | next: Suivant 13 | or: ou 14 | password_forgotten?: Mot de passe oublié ? 15 | delivery_service: Service de livraison 16 | my_account: Mon compte 17 | your_account: Votre compte 18 | deliver_on: Livraison le 19 | oops: Oups, quelque chose ne va pas 20 | no_js: Oups ! Veuillez activer JavaScript dans votre navigateur. 21 | cash_on_delivery: paiement à la livraison 22 | bank_deposit: virement bancaire 23 | paypal_cc: PayPal / carte de crédit 24 | -------------------------------------------------------------------------------- /config/locales/fr/javascript.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | javascript: 3 | customise_order: 4 | add_extra: Ajouter un nouvel extra 5 | -------------------------------------------------------------------------------- /config/locales/fr/simple_form.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | simple_form: 3 | 'yes': 'Oui' 4 | 'no': 'Non' 5 | required: 6 | text: 'requis' 7 | mark: '*' 8 | error_notification: 9 | default_message: "Veuillez corriger ces erreurs :" 10 | -------------------------------------------------------------------------------- /config/locales/fr/webstore.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | web_store: "%{webstore} - Boutique en ligne" 3 | no_ongoing_order: Il n'y a pas de commande en cours, veuillez en passer une nouvelle. 4 | cancelled_order: Votre commande précédente a été annulée, vous pouvez en passer une nouvelle. 5 | order_placed: Votre commande a été passée. 6 | halted_customer: Your account is under the minimum balance and will not recieve new orders unless payment is cleared before the packing date. 7 | product: 8 | order: Commander 9 | order: 10 | summary: Récapitulatif de la commande 11 | exclusions_and_substitutes: Exclusions et substitutions 12 | extras: Extras 13 | discount: Votre remise 14 | total: Total 15 | schedule_rule: 16 | deliver_on: Livraison le 17 | deliver_weekly_on: Livraison hebdomadaire le 18 | deliver_fornightly_on: Livraison bi-mensuelle le 19 | deliver_monthly_on: Livraison mensuelle le 20 | no_future_deliveries: Aucune future livraison planifiée 21 | phone_collection: 22 | mobile_phone: Numéro mobile 23 | work_phone: Numéro bureau 24 | home_phone: Numéro domicile 25 | sidebar: 26 | find_us_on_facebook: Retrouvez nous sur Facebook 27 | customise_order: 28 | customise_product: Personaliser mon produit 29 | exclude_items: Exclure ces éléments... 30 | substitute_with_items: Et les remplacer par... 31 | add_unlimited_extras: Ajouter des extras 32 | add_n_extras: Ajouter jusqu'à %{limit} extras 33 | authentication: 34 | enter_email: Entrez votre adresse email 35 | enter_password: Entrez votre mot de passe 36 | bad_email_password: Votre email et/ou mot de passe est incorrect. 37 | new_customer: Je n'ai pas de compte 38 | existing_customer: J'ai déjà un compte 39 | delivery_options: 40 | pick_delivery_service: Veuillez choisir un service de livraison 41 | delivery_details: Détails de livraison 42 | pickup_details: Détails du point relai 43 | select_best: Choisissez le service qui vous convient le mieux 44 | repeat_delivery: Renouvellement automatique de la commande ? 45 | when: Quand désirez vous recevoir cette commande ? 46 | extra_frequency: À propos de vos extras 47 | select_delivery_service: "- Veuillez sélectionner un service de livraison -" 48 | order_frequencies: 49 | select: "- Veuillez sélectionner une fréquence de livraison -" 50 | single: Livrer cette fois seulement 51 | weekly: Livrer chaque semaine le... 52 | fortnightly: Livrer toutes les deux semaines le... 53 | monthly: Livrer mensuellement 54 | extra_frequencies: 55 | always: Inclure les extras avec chaque commande 56 | once: Inclure les extras avec cette commande seulement 57 | payment_options: 58 | deliver_to: Livrer à 59 | delivery_note: Note pour la livraison 60 | change_delivery_address: Modifier mon adresse de livraison 61 | name: Votre nom 62 | enter_name: Entrez votre nom 63 | phone: Votre numéro de téléphone 64 | enter_phone: Entrez votre numéro 65 | amount_due: Montant total 66 | in_credit: Your account is in credit 67 | pay_by: Payer par 68 | no_payment: Pas de paiement nécessaire 69 | complete_order: Completer la commande 70 | address: 71 | address_1: Adresse 1 72 | address_2: Adresse 2 73 | suburb: Région 74 | city: Ville 75 | postcode: Code postal 76 | delivery_note: Note pour la livraison 77 | placeholders: 78 | delivery_note: Instructions telles que où laisser la commande, par example « déposer dans la véranda » 79 | completed: 80 | amount_due: Montant total 81 | pay_by: Payer par 82 | back_to_webstore: Retour à la boutique en ligne 83 | my_account: Mon compte 84 | payment_instructions: 85 | account_details: Détails de votre compte 86 | existing_balance: Votre solde actuel 87 | this_order: Cette commande 88 | closing_balance: Solde final 89 | payments: 90 | bank_deposit: 91 | deposit_funds: Veuillez faire votre virement vers 92 | account_name: Intitulé du compte 93 | reference: Référence 94 | paypal: 95 | pay_now: Payer maintenant 96 | top_up: Rechargement du compte 97 | amount: Montant 98 | amount_to_top_up: Montant à créditer 99 | -------------------------------------------------------------------------------- /config/locales/it/common.yml: -------------------------------------------------------------------------------- 1 | it: 2 | current_locale_en: Italian 3 | current_locale: Italiano 4 | colon: ": " 5 | hi: Ciao 6 | thanks: Grazie 7 | back: indietro 8 | cancel: chiudi 9 | log_in: Log in 10 | log_out: Log out 11 | update: Modifica 12 | next: Seguente 13 | or: o 14 | password_forgotten?: Hai dimenticato la tua password? 15 | delivery_service: Servizio di consegna 16 | my_account: Il mio account 17 | your_account: Il tuo account 18 | deliver_on: Consegna il 19 | oops: Oops qualcosa è andato storto 20 | no_js: Oops! Devi abilitare JavaScript nel tuo browser. 21 | cash_on_delivery: Pagamento alla consegna 22 | bank_deposit: Bonifico bancario 23 | paypal_cc: PayPal / Carta di credito 24 | -------------------------------------------------------------------------------- /config/locales/it/javascript.yml: -------------------------------------------------------------------------------- 1 | it: 2 | javascript: 3 | customise_order: 4 | add_extra: Aggiungi un prodotto 5 | -------------------------------------------------------------------------------- /config/locales/it/simple_form.yml: -------------------------------------------------------------------------------- 1 | it: 2 | simple_form: 3 | 'yes': 'Sì' 4 | 'no': 'No' 5 | required: 6 | text: 'obbligatorio' 7 | mark: '*' 8 | error_notification: 9 | default_message: "Si prega di esaminare i problemi sotto:" 10 | -------------------------------------------------------------------------------- /config/locales/it/webstore.yml: -------------------------------------------------------------------------------- 1 | it: 2 | web_store: "%{webstore} - Online" 3 | no_ongoing_order: Inserisci un nuovo ordine. 4 | cancelled_order: Il tuo ordine precedente è stato cancellato, puoi inserirne uno nuovo. 5 | order_placed: Il tuo ordine è stato inserito. 6 | halted_customer: Il tuo saldo si trova sotto la soglia minima, non effettueremo alcuna consegna fino a nuovo pagamento. 7 | product: 8 | order: Ordine 9 | order: 10 | summary: I tuoi ordini 11 | exclusions_and_substitutes: Escludi & sostituisci 12 | extras: Prodotti 13 | discount: Il tuo sconto 14 | total: Totale 15 | schedule_rule: 16 | deliver_on: Consegna il 17 | deliver_weekly_on: Consegna settimanale il 18 | deliver_fornightly_on: Consegna ogni due settimane il 19 | deliver_monthly_on: Consegna una volta al mese il 20 | no_future_deliveries: Non ci sono consegne programmate 21 | phone_collection: 22 | mobile_phone: Cellulare 23 | work_phone: Lavoro 24 | home_phone: Casa 25 | sidebar: 26 | find_us_on_facebook: Seguici su Facebook 27 | customise_order: 28 | customise_product: Personalizza la tua cassetta 29 | exclude_items: Escludi questi prodotti... 30 | substitute_with_items: E sostituiscili con... 31 | add_unlimited_extras: Aggiungi tutti i prodotti che desideri 32 | add_n_extras: Aggiungi fino a %{limit} prodotti 33 | authentication: 34 | enter_email: Indirizzo email 35 | enter_password: Password 36 | bad_email_password: La tua email e/o password risulta errata. 37 | new_customer: Nuovo cliente 38 | existing_customer: Sono già registrato 39 | delivery_options: 40 | pick_delivery_service: Seleziona il giro di consegna 41 | delivery_details: Dettagli consegna 42 | pickup_details: Dettagli punto di ritiro 43 | select_best: Scegli il giro di consegna che più ti è comodo 44 | repeat_delivery: Ripeti automaticamente la consegna? 45 | when: Quando desideri la consegna? 46 | extra_frequency: Per quanto riguarda i prodotti aggiunti? 47 | select_delivery_service: "- Seleziona il giro di consegna -" 48 | order_frequencies: 49 | select: "- Seleziona la frequenza delle consegne -" 50 | single: Consegna singola 51 | weekly: Consegna una volta a settimana ogni... 52 | fortnightly: Consegna una volta ogni 2 settimane il... 53 | monthly: Consegna mensile 54 | extra_frequencies: 55 | always: Aggiungi i prodotti A TUTTE LE CONSEGNE 56 | once: Aggiungi i prodotti SOLO ALLA PROSSIMA CONSEGNA 57 | payment_options: 58 | deliver_to: Consegna a 59 | delivery_note: Note di consegna 60 | change_delivery_address: Modifica il mio indirizzo di consegna 61 | name: Il tuo nome 62 | enter_name: Inserisci il tuo nome 63 | phone: Il tuo numero di telefono 64 | enter_phone: Inserisci il tuo numero di telefono 65 | amount_due: Totale importo dovuto 66 | in_credit: Il tuo conto ha un saldo positivo 67 | pay_by: Pagamento con 68 | no_payment: Non è necessario alcun pagamento 69 | complete_order: Ordine completo 70 | address: 71 | address_1: Indirizzo 1 72 | address_2: Indirizzo 2 73 | suburb: Località 74 | city: Comune 75 | postcode: CAP 76 | delivery_note: Note di consegna 77 | placeholders: 78 | delivery_note: 'Istruzioni per la consegna, per esempio: "lasciare sul pianerottolo"' 79 | completed: 80 | amount_due: Totale importo dovuto 81 | pay_by: Pagamento con 82 | back_to_webstore: Torna alla vetrina 83 | my_account: Il mio profilo 84 | payment_instructions: 85 | account_details: Dettagli profilo 86 | existing_balance: Il tuo saldo attuale 87 | this_order: Questo ordine 88 | closing_balance: Saldo finale 89 | payments: 90 | bank_deposit: 91 | deposit_funds: Perfavore effettuare bonifico bancario al seguente conto corrente 92 | account_name: Intestato a 93 | reference: Causale 94 | paypal: 95 | pay_now: Paga ora 96 | top_up: Carica account 97 | amount: Importo 98 | amount_to_top_up: Importo da caricare 99 | -------------------------------------------------------------------------------- /config/locales/nl/common.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | current_locale_en: Dutch 3 | current_locale: Nederlands 4 | colon: ": " 5 | hi: Hallo 6 | thanks: Dank u 7 | back: terug 8 | cancel: annuleer 9 | log_in: inloggen 10 | log_out: uitloggen 11 | update: Update 12 | next: Volgende 13 | or: of 14 | password_forgotten?: Paswoord vergeten? 15 | delivery_service: Thuis bezorging 16 | my_account: Mijn account 17 | your_account: Uw account 18 | deliver_on: Lever op 19 | oops: Oeps, er was een probleempje... 20 | no_js: Oops! U moet JavaScript aanzetten in uw browser. 21 | cash_on_delivery: Betaal bij levering 22 | bank_deposit: Bank overschrijving 23 | paypal_cc: Paypall / Credit Card 24 | -------------------------------------------------------------------------------- /config/locales/nl/javascript.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | javascript: 3 | customise_order: 4 | add_extra: Voeg een extra product toe 5 | -------------------------------------------------------------------------------- /config/locales/nl/simple_form.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | simple_form: 3 | 'yes': 'Ja' 4 | 'no': 'Nee' 5 | required: 6 | text: 'vereist' 7 | mark: '*' 8 | error_notification: 9 | default_message: "Kijk aub onderstaande problemen na:" 10 | -------------------------------------------------------------------------------- /config/locales/nl/webstore.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | web_store: "%{webstore} - Webshop" 3 | no_ongoing_order: Er is geen lopende bestelling, kies aub een product. 4 | cancelled_order: Uw vorige bestelling is geannuleerd. U kan een nieuwe bestelling maken. 5 | order_placed: Uw bestelling is geplaatst. 6 | halted_customer: Uw account saldo is onder het minimum. U kan slechts nieuwe bestellingen plaatsen als u de betaling uitvoert voor de bestellings-deadline. 7 | product: 8 | order: Bestelling 9 | order: 10 | summary: Overzicht bestelling 11 | exclusions_and_substitutes: Weggelaten en vervanging 12 | extras: Extra items 13 | discount: Uw korting 14 | total: Totaal 15 | schedule_rule: 16 | deliver_on: Lever op 17 | deliver_weekly_on: Lever wekelijks op 18 | deliver_fornightly_on: Lever tweewekelijks op 19 | deliver_monthly_on: Lever maandelijks op de 20 | no_future_deliveries: Geen toekomstige leveringen gepland 21 | phone_collection: 22 | mobile_phone: GSM 23 | work_phone: Werk telefoon 24 | home_phone: Huis telefoon 25 | sidebar: 26 | find_us_on_facebook: Vind ons op Facebook 27 | customise_order: 28 | customise_product: Personaliseer mijn product 29 | exclude_items: Laat volgende items weg... 30 | substitute_with_items: Vervang door volgende items... 31 | add_unlimited_extras: Voeg naar wens extra producten toe 32 | add_n_extras: Voeg tot %{limit} extra producten toe 33 | authentication: 34 | enter_email: Voer uw email adres in 35 | enter_password: Voer uw wachtwoord in 36 | bad_email_password: Uw email en/of wachtwoord is foutief 37 | new_customer: Ik ben een nieuwe klant 38 | existing_customer: Ik ben een bestaande klant 39 | delivery_options: 40 | pick_delivery_service: Kies een leverings-service 41 | delivery_details: Details Levering 42 | pickup_details: Details Ophalen 43 | select_best: Kies wat best bij uw locatie past 44 | repeat_delivery: Herhaal levering? 45 | when: Wanneer wil je het? 46 | extra_frequency: Wat met de extra producten? 47 | select_delivery_service: "- Kies leveringsmethode -" 48 | order_frequencies: 49 | select: "- Kies leveringsfrequentie -" 50 | single: Lever éénmalig 51 | weekly: Lever wekelijks op... 52 | fortnightly: Lever tweewekelijks op... 53 | monthly: Lever maandelijks 54 | extra_frequencies: 55 | always: Voeg Extra Producten toe bij ELKE levering 56 | once: Voeg Extra Producten toe bij enkel de VOLGENDE levering 57 | payment_options: 58 | deliver_to: Lever op 59 | delivery_note: Leveringsnota 60 | change_delivery_address: Wijzig mijn leveringsadres 61 | name: Uw naam 62 | enter_name: Voer uw naam in 63 | phone: Uw telefoonnummer 64 | enter_phone: Voer uw telefoonnummer in 65 | amount_due: Totaal te betalen bedrag 66 | in_credit: Uw account heeft krediet 67 | pay_by: Betaal tegen 68 | no_payment: Geen betaling nodig 69 | complete_order: Rond bestelling af 70 | address: 71 | address_1: Adres 1 72 | address_2: Adres 2 73 | suburb: Wijk 74 | city: Stad 75 | postcode: Postcode 76 | delivery_note: Leveringsnota 77 | placeholders: 78 | delivery_note: Leveringsinstructies zoals waar het pakket te plaatsen 79 | completed: 80 | amount_due: Totaal te betalen bedrag 81 | pay_by: Betaal tegen 82 | back_to_webstore: Terug naar webshop 83 | my_account: Mijn account 84 | payment_instructions: 85 | account_details: Account details 86 | existing_balance: Uw huidige account saldo bij ons 87 | this_order: Deze bestelling 88 | closing_balance: Nieuw saldo 89 | payments: 90 | bank_deposit: 91 | deposit_funds: Schrijf het bedrag aub over naar 92 | account_name: Naam rekeninghouder 93 | reference: Referentie 94 | paypal: 95 | pay_now: Betaal nu 96 | top_up: Voeg crediet toe aan mijn account 97 | amount: Bedrag 98 | amount_to_top_up: Op te laden Bedrag 99 | -------------------------------------------------------------------------------- /config/locales/pt-BR/common.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | current_locale_en: Portuguese 3 | current_locale: Português 4 | colon: ": " 5 | hi: Olá 6 | thanks: Muito obrigado! 7 | back: voltar 8 | cancel: cancelar 9 | log_in: Acessar 10 | log_out: Sair 11 | update: Atualizar 12 | next: Próximo 13 | or: ou 14 | password_forgotten?: Esqueceu sua senha? 15 | delivery_service: Serviço de entregas 16 | my_account: Minha conta 17 | your_account: 'Sua conta ' 18 | deliver_on: Entregar 19 | oops: Oops! Houve um problema 20 | no_js: Oops! Você precisa habilitar JavaScript em seu navegador. 21 | cash_on_delivery: 'Dinheiro, contra entrega ' 22 | bank_deposit: Depósito bancário 23 | paypal_cc: PayPal / Cartão de crédito 24 | -------------------------------------------------------------------------------- /config/locales/pt-BR/javascript.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | javascript: 3 | customise_order: 4 | add_extra: Adicionar um item extra 5 | -------------------------------------------------------------------------------- /config/locales/pt-BR/simple_form.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | simple_form: 3 | 'yes': 'Sim' 4 | 'no': 'Não' 5 | required: 6 | text: 'requerido' 7 | mark: '*' 8 | error_notification: 9 | default_message: "Por favor, verifique os problemas abaixo:" 10 | -------------------------------------------------------------------------------- /config/locales/pt-BR/webstore.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | web_store: "%{webstore} - Loja virtual" 3 | no_ongoing_order: Você não tem pedidos em andamento, por favor inicie um novo pedido. 4 | cancelled_order: Seu pedido anterior foi cancelado, mas você pode fazer um novo pedido. 5 | order_placed: Seu pedido foi aceito. 6 | halted_customer: O saldo de sua conta ultrapassou o limite permitido! Você não poderá receber novas entregas se seus pagamentos não forem regularizados antes da data de embalagem das próximas entregas programadas para todos os Clientes. 7 | product: 8 | order: Encomendar 9 | order: 10 | summary: Sumário do pedido 11 | exclusions_and_substitutes: Exclusões e substituições 12 | extras: Itens extras 13 | discount: Seu desconto 14 | total: Total 15 | schedule_rule: 16 | deliver_on: Entregar 17 | deliver_weekly_on: Entregar semanalmente 18 | deliver_fornightly_on: Entregar quinzenalmente às 19 | deliver_monthly_on: Entregar mensalmente no dia 20 | no_future_deliveries: Não há entregas futuras programadas 21 | phone_collection: 22 | mobile_phone: Telefone celular 23 | work_phone: Telefone no trabalho 24 | home_phone: Telefone residencial 25 | sidebar: 26 | find_us_on_facebook: Encontre-nos no Facebook 27 | customise_order: 28 | customise_product: Personalizar minha encomenda 29 | exclude_items: Excluir estes itens 30 | substitute_with_items: Substituir por estes itens 31 | add_unlimited_extras: Adicionar os itens extras que você quer receber 32 | add_n_extras: Adicionar até %{limit} itens extras 33 | authentication: 34 | enter_email: Digite seu endereço de email 35 | enter_password: Digite sua senha 36 | bad_email_password: Endereço de email ou senha incorretos. 37 | new_customer: Sou um novo cliente 38 | existing_customer: Sou um cliente que está de volta 39 | delivery_options: 40 | pick_delivery_service: Escolher um serviço de entregas 41 | delivery_details: Detalhes para entrega 42 | pickup_details: Detalhes para apanha pelo Cliente 43 | select_best: Selecione aquele que melhor atende a sua área 44 | repeat_delivery: Repetir a entrega? 45 | when: Quando você gostaria de receber sua(s) encomenda(s)? 46 | extra_frequency: O que você achou dos itens extras que você solicitou? 47 | select_delivery_service: "- Selecionar um serviço de entregas -" 48 | order_frequencies: 49 | select: "- Selecionar a frequencia de entrega -" 50 | single: Entregar apenas uma vez 51 | weekly: Entregar semanalmente 52 | fortnightly: Entregar a cada duas semanas 53 | monthly: Entregar mensalmente, no dia 54 | extra_frequencies: 55 | always: Incluir os itens extras em TODAS as entregas 56 | once: Incluir os itens extras somente na PRÓXIMA entrega 57 | payment_options: 58 | deliver_to: Entregar para 59 | delivery_note: Nota de entrega 60 | change_delivery_address: Mudar meu endereço de entrega 61 | name: Seu nome 62 | enter_name: Digite seu nome 63 | phone: Seu telefone 64 | enter_phone: Digite seu telefone 65 | amount_due: 'Total devido ' 66 | in_credit: Existe crédito em sua conta 67 | pay_by: Pagar 68 | no_payment: Não é necessário pagar 69 | complete_order: Completar este pedido 70 | address: 71 | address_1: Endereço 1 72 | address_2: Endereço 2 73 | suburb: Bairro 74 | city: Cidade 75 | postcode: CEP 76 | delivery_note: Nota de entrega 77 | placeholders: 78 | delivery_note: Instruções de entrega, tais como onde deixar o pacote 79 | completed: 80 | amount_due: Total devido 81 | pay_by: Pagar 82 | back_to_webstore: Voltar para a loja virtual 83 | my_account: Minha conta 84 | payment_instructions: 85 | account_details: Detalhes da conta 86 | existing_balance: Saldo de sua conta conosco 87 | this_order: Este pedido 88 | closing_balance: Saldo final 89 | payments: 90 | bank_deposit: 91 | deposit_funds: Favor fazer seu depósito no 92 | account_name: Favorecido 93 | reference: 'Referência: ' 94 | paypal: 95 | pay_now: Pague agora 96 | top_up: Adicionar crédito à minha conta 97 | amount: Montante 98 | amount_to_top_up: Montante limite 99 | -------------------------------------------------------------------------------- /config/locales/pt_BR: -------------------------------------------------------------------------------- 1 | pt-BR -------------------------------------------------------------------------------- /config/locales/zh/common.yml: -------------------------------------------------------------------------------- 1 | zh: 2 | current_locale_en: Chinese 3 | current_locale: 中文 4 | colon: ": " 5 | hi: 你好 6 | thanks: 谢谢 7 | back: 后退 8 | cancel: 取消 9 | log_in: 登录 10 | log_out: 注销 11 | update: 更新 12 | next: 下一个 13 | or: 或 14 | password_forgotten?: 忘记密码? 15 | delivery_service: 配送服务 16 | my_account: 我的账户 17 | your_account: 你的账户 18 | deliver_on: 配送日期 19 | oops: 抱歉,出现问题 20 | no_js: Oops! 请在你的浏览器设置中启用JavaScript才能使用这个在线商店。 21 | cash_on_delivery: 货到付现金 22 | bank_deposit: 银行转账 23 | paypal_cc: PayPal / 信用卡 24 | -------------------------------------------------------------------------------- /config/locales/zh/javascript.yml: -------------------------------------------------------------------------------- 1 | zh: 2 | javascript: 3 | customise_order: 4 | add_extra: 添加额外项目 5 | -------------------------------------------------------------------------------- /config/locales/zh/simple_form.yml: -------------------------------------------------------------------------------- 1 | zh: 2 | simple_form: 3 | 'yes': '是' 4 | 'no': '否' 5 | required: 6 | text: '必选' 7 | mark: '*' 8 | error_notification: 9 | default_message: "请检查如下错误:" 10 | -------------------------------------------------------------------------------- /config/locales/zh/webstore.yml: -------------------------------------------------------------------------------- 1 | zh: 2 | web_store: "%{webstore} - 在线商店" 3 | no_ongoing_order: 没有任何订单,请开始新的订购。 4 | cancelled_order: 你先前订单已取消,你可以开始新的预订。 5 | order_placed: 你订购商品已下单。 6 | halted_customer: 你的账户余额过低,在付款前无法接受新的订单。 7 | product: 8 | order: 订单 9 | order: 10 | summary: 订单详情 11 | exclusions_and_substitutes: 未找到商品或替代品 12 | extras: 额外商品 13 | discount: 你的折扣 14 | total: 总计 15 | schedule_rule: 16 | deliver_on: 送货时间 17 | deliver_weekly_on: 每周固定送货日期 18 | deliver_fornightly_on: 每两周固定送货日期 19 | deliver_monthly_on: 每月固定送货日期 20 | no_future_deliveries: 没有更多预定好的配送 21 | phone_collection: 22 | mobile_phone: 手机 23 | work_phone: 工作电话 24 | home_phone: 家用电话 25 | sidebar: 26 | find_us_on_facebook: 在Facebook上联系我们 27 | customise_order: 28 | customise_product: 定制我的产品 29 | exclude_items: 排除这些... 30 | substitute_with_items: 替代这些商品... 31 | add_unlimited_extras: 添加任意数量的额外商品 32 | add_n_extras: 添加最多 %{limit} 件额外商品 33 | authentication: 34 | enter_email: 输入你的电子邮件地址 35 | enter_password: 输入你的密码 36 | bad_email_password: 你的电子邮件/密码错误 37 | new_customer: 我是一名新客户 38 | existing_customer: 我是一名老客户 39 | delivery_options: 40 | pick_delivery_service: 选择一种配送方式 41 | delivery_details: 配送详情 42 | pickup_details: 取货详情 43 | select_best: 选择一个最适合你所居住位置的服务 44 | repeat_delivery: 重复配送? 45 | when: 你什么时候需要? 46 | extra_frequency: 那么额外商品呢? 47 | select_delivery_service: "- 选择配送服务 -" 48 | order_frequencies: 49 | select: "- 选择配送频率 -" 50 | single: 只配送一次 51 | weekly: 每周配送,在... 52 | fortnightly: 每两周配送,在... 53 | monthly: 每月配送 54 | extra_frequencies: 55 | always: 在每次配送时包括额外商品 56 | once: 只有在次日送达时包含额外商品 57 | payment_options: 58 | deliver_to: 配送到 59 | delivery_note: 配送备注 60 | change_delivery_address: 更改我的配送地址 61 | name: 你的名字 62 | enter_name: 输入你的名字 63 | phone: 你的电话号码 64 | enter_phone: 输入你的电话号码 65 | amount_due: 总计需付款 66 | in_credit: 你有账户余额 67 | pay_by: 支付人 68 | no_payment: 不需要付款 69 | complete_order: 结束订单 70 | address: 71 | address_1: 地址 1 72 | address_2: 地址 2 73 | suburb: 郊区 74 | city: 城市 75 | postcode: 邮编 76 | delivery_note: 配送备注 77 | placeholders: 78 | delivery_note: 配送指南比如物品放置位置 79 | completed: 80 | amount_due: 总计需付款 81 | pay_by: 付款人 82 | back_to_webstore: 返回到网上商店 83 | my_account: 我的帐户 84 | payment_instructions: 85 | account_details: 帐户细节 86 | existing_balance: 你的现有账户余额 87 | this_order: 这个订单 88 | closing_balance: 期末余额 89 | payments: 90 | bank_deposit: 91 | deposit_funds: 请存款至 92 | account_name: 账号名 93 | reference: 备注 94 | paypal: 95 | pay_now: 现在支付 96 | top_up: 给我的账户充值 97 | amount: 金额 98 | amount_to_top_up: 充值金额 99 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # directory '/var/app/current' 4 | threads 8, 32 5 | workers `grep -c processor /proc/cpuinfo` 6 | preload_app! 7 | # bind 'unix:///var/run/puma/my_app.sock' 8 | # pidfile '/var/run/puma/puma.pid' 9 | # stdout_redirect '/var/log/puma/puma.log', '/var/log/puma/puma.log', true 10 | # daemonize false 11 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | # The priority is based upon order of creation: first created -> highest priority. 5 | 6 | root to: "map#index" 7 | get "/ping", to: "application#ping" 8 | 9 | scope ":webstore_id", constraints: { format: "html" } do 10 | # get "/fruits", action: "fruits", controller: "store" # TODO: categories using box tags 11 | 12 | scope "/account" do 13 | scope module: :account do 14 | # get "/", action: "dashboard", as: "customer_dashboard" # NOTE: future 15 | get "/", to: redirect("https://my.buckybox.com/customer"), as: "customer_dashboard" 16 | end 17 | 18 | scope module: :session do 19 | get "/sign_in", action: "new", as: "customer_sign_in" 20 | post "/sign_in", action: "create", as: nil 21 | delete "/sign_out", action: "destroy", as: "customer_sign_out" 22 | end 23 | end 24 | 25 | get "/", to: "store#home", as: "webstore" 26 | get "/start_checkout/:product_id", to: "store#start_checkout", as: "start_checkout" 27 | # get "/admin", to: redirect("/distributor") # NOTE: future 28 | 29 | scope module: :customise_order do 30 | get "/customise_order", action: "customise_order" 31 | post "/customise_order", action: "save_order_customisation" 32 | end 33 | 34 | scope module: :authentication do 35 | get "/authentication", action: "authentication" 36 | post "/authentication", action: "save_authentication" 37 | end 38 | 39 | scope module: :delivery_options do 40 | get "/delivery_options", action: "delivery_options" 41 | post "/delivery_options", action: "save_delivery_options" 42 | end 43 | 44 | scope module: :payment_options do 45 | get "/payment_options", action: "payment_options" 46 | post "/payment_options", action: "save_payment_options" 47 | end 48 | 49 | scope module: :completed do 50 | get "/completed", action: "completed" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 9f7356b3179a1c86e0454e5f0fba43854a8a14608064299ad0ed9d25f95f820aac0fa6b835d29ae299c45d822856d86e730215d404d46a183bcdffbdcdfc6850 15 | 16 | test: 17 | secret_key_base: 5b98adc9e4a93ca25b1c8329fe9595e0cd3002610a40818a2cf2305b92f56c9a4703731190162947dabae5a889d901fb65238a9d956a2137411015c081311528 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /doc/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/doc/screenshot.jpg -------------------------------------------------------------------------------- /features/step_definitions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/features/step_definitions/.gitkeep -------------------------------------------------------------------------------- /features/step_definitions/webstore_steps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | World(Select2Helper) 4 | 5 | def step_path(step) 6 | path_helper = "#{step}_path" 7 | public_send(path_helper, webstore_id: "fantastic-vege-people") 8 | end 9 | 10 | Given /^I am authenticated$/ do 11 | step "I am on the webstore" 12 | step "I log in" if page.has_link?("Log in") 13 | expect(page).not_to have_button("Log in") 14 | end 15 | 16 | Given /^I am unauthenticated$/ do 17 | step "I am on the webstore" 18 | step "I log out" if page.has_link?("Log out") 19 | expect(page).to have_link("Log in") 20 | end 21 | 22 | When /^I log out$/ do 23 | click "Log out" 24 | end 25 | 26 | Given /^I log in$/ do 27 | step "I am viewing the login page" 28 | step "I fill in valid credentials" 29 | end 30 | 31 | Given /^I am viewing the login page$/ do 32 | visit step_path("customer_sign_in") 33 | end 34 | 35 | When /^I fill in valid credentials$/ do 36 | fill_in "email", with: "joe@buckybox.com" 37 | fill_in "password", with: "whatever" 38 | click_button "Log in" 39 | step "I am authenticated" 40 | end 41 | 42 | Given /^I am on the webstore$/ do 43 | visit step_path("webstore") 44 | end 45 | 46 | When /^I select a customisable box to order$/ do 47 | click_link("Order", match: :first) 48 | end 49 | 50 | Then /^I should be asked to customise the box$/ do 51 | step "I should be viewing the customise_order step" 52 | step "I should not see an error message" 53 | end 54 | 55 | When /^I customise the box$/ do 56 | check "Customise my product" 57 | select2("Cabbage", from: "customise_order_dislikes") 58 | # TODO: test subs 59 | click_button "Next" 60 | end 61 | 62 | Then "I should be asked to log in or sign up" do 63 | step "I should be viewing the authentication step" 64 | step "I should not see an error message" 65 | end 66 | 67 | When /^I fill in my email address$/ do 68 | fill_in :authentication_email, with: "joe@buckybox.com" 69 | click_button "Next" 70 | end 71 | 72 | Then /^I should be asked to select my delivery frequency$/ do 73 | step "I should be viewing the delivery_options step" 74 | step "I should not see an error message" 75 | end 76 | 77 | When /^I select a (.*) delivery frequency$/ do |frequency| 78 | select frequency, from: :delivery_options_frequency 79 | click_button "Next" 80 | end 81 | 82 | Then /^I should be asked for my delivery address$/ do 83 | step "I should be viewing the payment_options step" 84 | step "I should not see an error message" 85 | end 86 | 87 | When /^I (fill in|confirm) my delivery address$/ do |action| 88 | if action == "fill in" 89 | fill_in :payment_options_name, with: "Crazy Rabbit" 90 | fill_in :payment_options_address_1, with: "Rabbit Hole" 91 | end 92 | end 93 | 94 | When /^I select the payment option "(.*)"$/ do |option| 95 | within "#payment-options" do 96 | choose option 97 | end 98 | 99 | click_button "Complete Order" 100 | end 101 | 102 | Then /^My order should be placed$/ do 103 | expect(page).to have_content "Your order has been placed" 104 | end 105 | 106 | Then /^I should see the details of my order$/ do 107 | expect(page).to have_content "Account details" 108 | end 109 | 110 | Then /^I should see "(.*)"$/ do |content| 111 | expect(page).to have_content content 112 | end 113 | 114 | Then /^I should be viewing the (.*) step$/ do |step| 115 | expected_path = step_path(step) 116 | expect(current_path).to eq expected_path 117 | 118 | expect(page.title).to start_with "Bucky Box - " 119 | 120 | step "I should not see an error message" 121 | end 122 | 123 | Then /^I should not see an error message$/ do 124 | expect(page).not_to have_content "Oops" 125 | end 126 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "simplecov" if ENV["COVERAGE"] 4 | 5 | require "cucumber/rails" 6 | ActionController::Base.allow_rescue = true 7 | 8 | require "capybara/poltergeist" 9 | Capybara.javascript_driver = :poltergeist 10 | 11 | require "capybara-screenshot/cucumber" 12 | 13 | require "vcr" 14 | VCR.configure do |c| 15 | c.cassette_library_dir = BuckyBox::API.fixtures_path 16 | c.hook_into :webmock 17 | c.default_cassette_options = { record: :none, allow_playback_repeats: true } 18 | c.ignore_localhost = true 19 | end 20 | 21 | Dir.glob(File.join(BuckyBox::API.fixtures_path, "*.yml")).each do |file| 22 | cassette = File.basename(file, ".yml") 23 | puts "Loading cassette #{cassette}" 24 | VCR.insert_cassette cassette 25 | end 26 | 27 | Dir[Rails.root.join("spec/support/capybara/**/*.rb")].each { |f| require f } 28 | -------------------------------------------------------------------------------- /features/webstore_authenticated.feature: -------------------------------------------------------------------------------- 1 | @javascript 2 | Feature: Authenticated customer places an order 3 | 4 | Background: 5 | Given I am authenticated 6 | 7 | Scenario Outline: Order a box 8 | Given I am on the webstore 9 | When I select a customisable box to order 10 | Then I should be asked to customise the box 11 | When I customise the box 12 | Then I should be asked to select my delivery frequency 13 | When I select a monthly delivery frequency 14 | Then I should be asked for my delivery address 15 | When I confirm my delivery address 16 | And I select the payment option "" 17 | Then My order should be placed 18 | And I should see the details of my order 19 | And I should see "Pay by " 20 | 21 | Examples: 22 | | method | 23 | | Cash on Delivery | 24 | | Bank Deposit | 25 | | PayPal | 26 | -------------------------------------------------------------------------------- /features/webstore_unauthenticated.feature: -------------------------------------------------------------------------------- 1 | @javascript 2 | Feature: Unauthenticated customer places an order 3 | 4 | Background: 5 | Given I am unauthenticated 6 | 7 | Scenario: Order a box 8 | Given I am on the webstore 9 | When I select a customisable box to order 10 | Then I should be asked to customise the box 11 | When I customise the box 12 | Then I should be asked to log in or sign up 13 | When I fill in my email address 14 | Then I should be asked to select my delivery frequency 15 | When I select a monthly delivery frequency 16 | Then I should be asked for my delivery address 17 | When I fill in my delivery address 18 | And I select the payment option "Cash on Delivery" 19 | Then My order should be placed 20 | And I should see the details of my order 21 | -------------------------------------------------------------------------------- /lib/address.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "phone_collection" 4 | 5 | class Address 6 | ADDRESS_ATTRIBUTES = %i[ 7 | address_1 8 | address_2 9 | suburb 10 | city 11 | postcode 12 | delivery_note 13 | ].freeze 14 | 15 | attr_accessor(*ADDRESS_ATTRIBUTES) 16 | attr_accessor(*PhoneCollection.attributes) 17 | 18 | def self.address_attributes 19 | ADDRESS_ATTRIBUTES 20 | end 21 | 22 | def initialize(args = {}) 23 | args.each { |k, v| public_send("#{k}=", v) } 24 | end 25 | 26 | def to_s(join_with = ", ") 27 | ADDRESS_ATTRIBUTES.map do |attribute| 28 | public_send attribute 29 | end.reject(&:blank?).join(join_with).html_safe # rubocop:disable Rails/OutputSafety 30 | end 31 | 32 | def phones 33 | @phones ||= PhoneCollection.new(self) 34 | end 35 | 36 | def default_phone_number 37 | phones.default_number 38 | end 39 | 40 | def default_phone_type 41 | phones.default_type 42 | end 43 | 44 | # Handy helper to update a given number type 45 | def phone=(phone) 46 | type = phone[:type] 47 | number = phone[:number] 48 | return if type.blank? 49 | 50 | send("#{type}_phone=", number) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/lib/assets/.keep -------------------------------------------------------------------------------- /lib/paypal_form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PaypalForm 4 | module_function 5 | 6 | def recurring_payment_params(frequency) 7 | # https://www.paypal.com/en/cgi-bin/webscr?cmd=_pdn_subscr_techview_outside 8 | # p3 - number of time periods between each recurrence 9 | # t3 - time period (D=days, W=weeks, M=months, Y=years) 10 | 11 | p3, t3 = \ 12 | case frequency.to_sym 13 | when :weekly 14 | [1, "W"] 15 | when :fortnightly 16 | [2, "W"] 17 | when :monthly 18 | [1, "M"] 19 | else 20 | # :nocov: 21 | raise ArgumentError, "Invalid frequency for recurring_payment_params: #{frequency.inspect}" 22 | # :nocov: 23 | end 24 | 25 | OpenStruct.new(p3: p3, t3: t3).freeze 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/phone_collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Class to store multiple phone numbers 4 | # Each number is associated with a type (mobile, home, work) 5 | class PhoneCollection 6 | TYPES = %w[mobile home work].reduce({}) do |hash, type| 7 | hash.merge!(type => "#{type}_phone") 8 | end.freeze 9 | 10 | def self.attributes 11 | TYPES.values 12 | end 13 | 14 | def self.types_as_options 15 | TYPES.each_key.map { |type| type_option(type) } 16 | end 17 | 18 | def self.type_option(type) 19 | [I18n.t("phone_collection.#{type}_phone"), type] 20 | end 21 | 22 | def initialize(address) 23 | @address = address 24 | end 25 | 26 | def default_number 27 | @address.send(default[:attribute]) 28 | end 29 | 30 | def default_type 31 | default[:type] 32 | end 33 | 34 | private 35 | 36 | def default 37 | TYPES.each do |type, attribute| 38 | number = @address.public_send(attribute) 39 | return { type: type, attribute: attribute } if number.present? 40 | end 41 | 42 | # fallback to first type if all blank 43 | type, attribute = TYPES.first 44 | { type: type, attribute: attribute } 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/schedule_rule.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ScheduleRule 4 | attr_accessor :frequency, :start_date, :days 5 | 6 | FREQUENCIES = %i[single weekly fortnightly monthly].freeze 7 | 8 | def initialize(frequency:, start_date:, days:) 9 | self.frequency = frequency.to_sym 10 | raise ArgumentError unless FREQUENCIES.include?(self.frequency) 11 | 12 | self.start_date = start_date.is_a?(Date) ? start_date : Date.parse(start_date) 13 | 14 | self.days = days.keys.map(&:to_i) 15 | raise ArgumentError if self.days.any? { |day| day.negative? || day > 27 } 16 | end 17 | 18 | def delivery_days 19 | I18n.t("date.day_names").select.each_with_index do |_day, index| 20 | runs_on index 21 | end.to_sentence 22 | end 23 | 24 | # e.g. [0, 3] 25 | def week_days 26 | days.map { |day| day % 7 } 27 | end 28 | 29 | def week 30 | (days.first / 7).floor 31 | end 32 | 33 | def runs_on(week_day_index) 34 | week_days.include?(week_day_index) 35 | end 36 | 37 | def to_s 38 | case frequency 39 | when :single 40 | "#{I18n.t('schedule_rule.deliver_on')} #{I18n.l(start_date, format: '%d %b')}" 41 | when :weekly 42 | "#{I18n.t('schedule_rule.deliver_weekly_on')} #{delivery_days}" 43 | when :fortnightly 44 | "#{I18n.t('schedule_rule.deliver_fornightly_on')} #{delivery_days}" 45 | when :monthly 46 | "#{I18n.t('schedule_rule.deliver_monthly_on')} #{week.succ.ordinalize_in_full} #{delivery_days}" 47 | end 48 | end 49 | 50 | def to_h 51 | attributes = %i[frequency start_date week_days] 52 | attributes << :week if frequency != :single 53 | 54 | attributes.each_with_object({}) do |attr, hash| 55 | hash[attr] = public_send(attr) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/assets.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :assets do 4 | desc "Remove compiled assets and recompile them" 5 | task update: %i[clobber precompile] do 6 | puts "Updated all assets" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/log/.keep -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 500.html -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/assets/application-731c774331642949afafd49563b1ad64e3ee06bdc5e886d3188c43247c00709a.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/application-731c774331642949afafd49563b1ad64e3ee06bdc5e886d3188c43247c00709a.js.gz -------------------------------------------------------------------------------- /public/assets/application-f38798986a4c88227b916d75d74dbedc35a7b60b67f4a1a40fc92ca6cdceea02.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/application-f38798986a4c88227b916d75d74dbedc35a7b60b67f4a1a40fc92ca6cdceea02.css.gz -------------------------------------------------------------------------------- /public/assets/background-5f61dbdbbda4f607223a5b6b855ea75ccfada574f3a4a5f3c3f2df744e7469d1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/background-5f61dbdbbda4f607223a5b6b855ea75ccfada574f3a4a5f3c3f2df744e7469d1.png -------------------------------------------------------------------------------- /public/assets/bucky-box-powered-7d986f2ee6ad772d8880f6fbddc0a2ae2e6366c872a4934d472db593a11625bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/bucky-box-powered-7d986f2ee6ad772d8880f6fbddc0a2ae2e6366c872a4934d472db593a11625bb.png -------------------------------------------------------------------------------- /public/assets/close-icon-397a02e804aad172d9186b3f92da72c5d323618f4965917901230bb9bf12ca8a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/close-icon-397a02e804aad172d9186b3f92da72c5d323618f4965917901230bb9bf12ca8a.png -------------------------------------------------------------------------------- /public/assets/facebook-icon-1f1ef4d63dffd8ad069e72c67ed5b635a62c3103b954d823aa54d475e3437ef3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/facebook-icon-1f1ef4d63dffd8ad069e72c67ed5b635a62c3103b954d823aa54d475e3437ef3.png -------------------------------------------------------------------------------- /public/assets/fallbacks/box/box_image/webstore_default-f6ac6c6001ef9b277031a76f571ead446e230037099e0a92b76b6c6dd38b20cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/fallbacks/box/box_image/webstore_default-f6ac6c6001ef9b277031a76f571ead446e230037099e0a92b76b6c6dd38b20cd.png -------------------------------------------------------------------------------- /public/assets/glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png -------------------------------------------------------------------------------- /public/assets/glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png -------------------------------------------------------------------------------- /public/assets/layers-1dbbe9d028e292f36fcba8f8b3a28d5e8932754fc2215b9ac69e4cdecf5107c6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/layers-1dbbe9d028e292f36fcba8f8b3a28d5e8932754fc2215b9ac69e4cdecf5107c6.png -------------------------------------------------------------------------------- /public/assets/layers-2x-066daca850d8ffbef007af00b06eac0015728dee279c51f3cb6c716df7c42edf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/layers-2x-066daca850d8ffbef007af00b06eac0015728dee279c51f3cb6c716df7c42edf.png -------------------------------------------------------------------------------- /public/assets/map-4aa8a25cbf7a3b1c49089b140a359ac8bae65503825dd6b38dd7be725b323a0e.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/map-4aa8a25cbf7a3b1c49089b140a359ac8bae65503825dd6b38dd7be725b323a0e.js.gz -------------------------------------------------------------------------------- /public/assets/map-9a88d1a7714d73d7ecc1c1ea35031b3537b7b7b1167634702b923257f5ed6fdf.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/map-9a88d1a7714d73d7ecc1c1ea35031b3537b7b7b1167634702b923257f5ed6fdf.css.gz -------------------------------------------------------------------------------- /public/assets/map-marker-icon-cee88d7d4f8b842112804ca8f8a718f893d5b05f9f925e84553c61bc81244191.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/map-marker-icon-cee88d7d4f8b842112804ca8f8a718f893d5b05f9f925e84553c61bc81244191.png -------------------------------------------------------------------------------- /public/assets/marker-icon-2x-2d77a2e4c2f08bbac41808324ef946b9a2fe61b6150480d011b72b379c3b238d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/marker-icon-2x-2d77a2e4c2f08bbac41808324ef946b9a2fe61b6150480d011b72b379c3b238d.png -------------------------------------------------------------------------------- /public/assets/marker-icon-574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/marker-icon-574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437.png -------------------------------------------------------------------------------- /public/assets/marker-shadow-264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/marker-shadow-264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da.png -------------------------------------------------------------------------------- /public/assets/phone-icon-2456db717faf8f3bee70c50bf2ece7e7a4d0ea4f613fac6dd8cf3a897bfd8a7c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/phone-icon-2456db717faf8f3bee70c50bf2ece7e7a4d0ea4f613fac6dd8cf3a897bfd8a7c.png -------------------------------------------------------------------------------- /public/assets/select2-d6b5d8d83dbc18fb8d77c8761d331cd9e5123c9684950bab0406e98a24ac5ae8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/select2-d6b5d8d83dbc18fb8d77c8761d331cd9e5123c9684950bab0406e98a24ac5ae8.png -------------------------------------------------------------------------------- /public/assets/select2-spinner-f6ecff617ec2ba7f559e6f535cad9b70a3f91120737535dab4d4548a6c83576c.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/select2-spinner-f6ecff617ec2ba7f559e6f535cad9b70a3f91120737535dab4d4548a6c83576c.gif -------------------------------------------------------------------------------- /public/assets/select2x2-6fe28d687dc0ed4d96016238c608ba1e7198c9c9accfa0b360b78018b9fb9bc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/select2x2-6fe28d687dc0ed4d96016238c608ba1e7198c9c9accfa0b360b78018b9fb9bc2.png -------------------------------------------------------------------------------- /public/assets/webstore-background-25dc57c5899bd5f11d50dfe04ed11577654bdddacaa138725ccfd158e472728d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/assets/webstore-background-25dc57c5899bd5f11d50dfe04ed11577654bdddacaa138725ccfd158e472728d.png -------------------------------------------------------------------------------- /public/favicon-gray.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/favicon-gray.ico -------------------------------------------------------------------------------- /public/favicon-red.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/favicon-red.ico -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # Hello Robot :) 2 | -------------------------------------------------------------------------------- /spec/lib/schedule_rule_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../lib/schedule_rule" 4 | 5 | describe ScheduleRule do 6 | let(:schedule) { described_class.new(frequency: :monthly, start_date: "2014-09-10", days: { 10 => 1, 13 => 1 }) } 7 | 8 | before { I18n.locale = :en } 9 | 10 | describe "#delivery_days" do 11 | specify { expect(schedule.delivery_days).to eq "Wednesday and Saturday" } 12 | end 13 | 14 | describe "#to_s" do 15 | specify { expect(schedule.to_s).to eq "Deliver monthly on the second Wednesday and Saturday" } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/models/cart_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../app/models/cart" 4 | 5 | describe Cart do 6 | let(:persistence_class) { double("persistence_class") } # rubocop:disable RSpec/VerifiedDoubles 7 | let(:persistence) { double("persistence", cart: cart) } # rubocop:disable RSpec/VerifiedDoubles 8 | let(:args) { { webstore_id: 1, persistence_class: persistence_class } } 9 | let(:cart) { described_class.new(args) } 10 | 11 | describe ".find" do 12 | context "when a cart is found" do 13 | it "returns restored cart" do 14 | allow(cart).to receive(:id) { 1 } 15 | allow(persistence_class).to receive(:find) { persistence } 16 | expect(described_class.find(1, persistence_class).new?).to be false 17 | end 18 | end 19 | 20 | context "when a cart is not found" do 21 | it "returns nil" do 22 | allow(persistence_class).to receive(:find) { nil } 23 | expect(described_class.find(1, persistence_class)).to be_nil 24 | end 25 | end 26 | end 27 | 28 | describe "#new?" do 29 | it "is considered new if the cart does not have an true" do 30 | cart_without_id = described_class.new(args) 31 | expect(cart_without_id.new?).to be true 32 | end 33 | 34 | it "is not considered new if the cart has an true" do 35 | cart_without_id = described_class.new(args.merge(id: 1)) 36 | expect(cart_without_id.new?).not_to be true 37 | end 38 | end 39 | 40 | describe "#==" do 41 | context "with a new cart" do 42 | let(:cart1) { described_class.new(args) } 43 | 44 | it "returns true if the carts are the same objects" do 45 | expect(cart1).to eq(cart1) 46 | end 47 | 48 | it "returns false if the carts are diffent objects" do 49 | cart2 = described_class.new(args) 50 | expect(cart1).not_to eq(cart2) 51 | end 52 | end 53 | 54 | context "with a saved cart" do 55 | let(:cart1) { described_class.new(args.merge(id: 1)) } 56 | 57 | it "returns true if the carts have the same id" do 58 | cart2 = described_class.new(args.merge(id: 1)) 59 | expect(cart1).to eq(cart2) 60 | end 61 | 62 | it "returns false if the carts have a different id" do 63 | cart2 = described_class.new(args.merge(id: 2)) 64 | expect(cart1).not_to eq(cart2) 65 | end 66 | end 67 | end 68 | 69 | describe "#save" do 70 | before do 71 | allow(persistence).to receive(:id) { 1 } 72 | allow(cart).to receive(:find_or_create_persistence) { persistence } 73 | end 74 | 75 | context "when save works" do 76 | it "saves a cart and returns an true" do 77 | allow(persistence).to receive(:save) { true } 78 | expect(cart.save).to be_truthy 79 | end 80 | end 81 | 82 | context "when save fails" do 83 | it "returns 0" do 84 | allow(persistence).to receive(:save) { false } 85 | expect(cart.save).to be_falsy 86 | end 87 | end 88 | end 89 | 90 | describe "#add_product" do 91 | it "returns true if the product is successfully added to the order" do 92 | allow(cart.order).to receive(:add_product) { true } 93 | expect(cart.add_product(product_id: 1)).to be true 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /spec/models/checkout/authentication_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../app/models/checkout/authentication" 4 | 5 | describe Authentication do 6 | let(:cart) { instance_double(Cart) } 7 | let(:args) { { cart: cart } } 8 | let(:authentication) { described_class.new(args) } 9 | 10 | describe "#options" do 11 | it "returns an option for authorisation" do 12 | expected_options = [["I'm a new customer", "new"], ["I'm a returning customer", "returning"]] 13 | expect(authentication.options).to eq(expected_options) 14 | end 15 | end 16 | 17 | describe "#webstore_id" do 18 | it "returns the name of the web store associated with this cart" do 19 | allow(cart).to receive(:webstore_id) { "webstore-name" } 20 | expect(authentication.webstore_id).to eq("webstore-name") 21 | end 22 | end 23 | 24 | describe "#to_h" do 25 | it "returns a hash of the important form data" do 26 | authentication.email = "test@example.com" 27 | authentication.password = "password" 28 | expect(authentication.to_h).to eq(email: "test@example.com") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/models/checkout/checkout_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../app/models/checkout/checkout" 4 | 5 | describe Checkout do 6 | let(:webstore) { double("webstore") } # rubocop:disable RSpec/VerifiedDoubles 7 | let(:logged_in_customer) { instance_double(Customer) } 8 | let(:cart) { instance_double(Cart).as_null_object } 9 | let(:cart_class) { class_double(Cart, new: cart) } 10 | let(:checkout) { described_class.new(args) } 11 | let(:args) do 12 | { 13 | webstore: webstore, 14 | logged_in_customer: logged_in_customer, 15 | cart_class: cart_class, 16 | existing_customer: nil, 17 | } 18 | end 19 | 20 | describe "#customer" do 21 | it "returns a customer" do 22 | allow(cart).to receive(:customer) { logged_in_customer } 23 | expect(checkout.customer).to eq(logged_in_customer) 24 | end 25 | end 26 | 27 | describe "#add_product!" do 28 | it "returns true if the product is added to the cart" do 29 | allow(cart).to receive(:add_product) 30 | expect(checkout.add_product!(3)).to be_truthy 31 | end 32 | end 33 | 34 | describe "#cart_id" do 35 | it "returns the id of the cart" do 36 | allow(cart).to receive(:id) { 3 } 37 | expect(checkout.cart_id).to eq(3) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/models/checkout/customise_order_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../app/models/checkout/customise_order" 4 | 5 | describe CustomiseOrder do 6 | let(:product) { double("product") } # rubocop:disable RSpec/VerifiedDoubles 7 | let(:cart) { instance_double(Cart, product: product) } 8 | let(:args) { { cart: cart } } 9 | let(:customise_order) { described_class.new(args) } 10 | 11 | describe "#stock_list" do 12 | it "returns a list of stock items" do 13 | stock_list = [double("stock_item")] # rubocop:disable RSpec/VerifiedDoubles 14 | allow(cart).to receive(:stock_list) { stock_list } 15 | expect(customise_order.stock_list).to eq(stock_list) 16 | end 17 | end 18 | 19 | describe "#extras_list" do 20 | it "returns a list of extras items for a cart" do 21 | extras_list = [double("extra")] # rubocop:disable RSpec/VerifiedDoubles 22 | allow(cart).to receive(:extras_list) { extras_list } 23 | expect(customise_order.extras_list).to eq(extras_list) 24 | end 25 | end 26 | 27 | describe "#exclusions?" do 28 | specify do 29 | allow(product).to receive(:dislikes) { true } 30 | expect(customise_order.exclusions?).to be true 31 | end 32 | 33 | specify do 34 | allow(product).to receive(:dislikes) { false } 35 | expect(customise_order.exclusions?).to be false 36 | end 37 | end 38 | 39 | describe "#substitutions?" do 40 | specify do 41 | allow(product).to receive(:likes) { true } 42 | expect(customise_order.substitutions?).to be true 43 | end 44 | 45 | specify do 46 | allow(product).to receive(:likes) { false } 47 | expect(customise_order.substitutions?).to be false 48 | end 49 | end 50 | 51 | describe "#extras_allowed?" do 52 | specify do 53 | allow(product).to receive(:extras_allowed) { true } 54 | expect(customise_order.extras_allowed?).to be true 55 | end 56 | 57 | specify do 58 | allow(product).to receive(:extras_allowed) { false } 59 | expect(customise_order.extras_allowed?).to be false 60 | end 61 | end 62 | 63 | describe "#extras_unlimited?" do 64 | specify do 65 | allow(product).to receive(:extras_unlimited) { true } 66 | expect(customise_order.extras_unlimited?).to be true 67 | end 68 | 69 | specify do 70 | allow(product).to receive(:extras_unlimited) { false } 71 | expect(customise_order.extras_unlimited?).to be false 72 | end 73 | end 74 | 75 | describe "#exclusions_limit" do 76 | specify do 77 | allow(product).to receive(:exclusions_limit) { 5 } 78 | expect(customise_order.exclusions_limit).to eq(5) 79 | end 80 | end 81 | 82 | describe "#substitutions_limit" do 83 | specify do 84 | allow(product).to receive(:substitutions_limit) { 5 } 85 | expect(customise_order.substitutions_limit).to eq(5) 86 | end 87 | end 88 | 89 | describe "#extras_limit" do 90 | specify do 91 | allow(product).to receive(:extras_limit) { 5 } 92 | expect(customise_order.extras_limit).to eq(5) 93 | end 94 | end 95 | 96 | describe "#to_h" do 97 | it "returns a hash of the important form data" do 98 | customise_order.dislikes = [1] 99 | customise_order.likes = [1] 100 | customise_order.extras = { 1 => 1 } 101 | expect(customise_order.to_h).to eq(dislikes: [1], likes: [1], extras: { 1 => 1 }) 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/models/checkout/delivery_options_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../app/models/checkout/delivery_options" 4 | 5 | describe DeliveryOptions do 6 | let(:webstore) { double("webstore") } # rubocop:disable RSpec/VerifiedDoubles 7 | let(:customer) { instance_double(Customer) } 8 | let(:cart) { instance_double(Cart, webstore: webstore, customer: customer) } 9 | let(:args) { { cart: cart } } 10 | let(:delivery_options) { described_class.new(args) } 11 | 12 | describe "#existing_delivery_service_id" do 13 | it "returns a delivery_service id" do 14 | allow(customer).to receive(:delivery_service_id) { 3 } 15 | expect(delivery_options.existing_delivery_service_id).to eq(3) 16 | end 17 | end 18 | 19 | describe "#can_change_delivery_service?" do 20 | it "returns true if the there is not an existing delivery service" do 21 | allow(customer).to receive(:delivery_service_id) { nil } 22 | expect(delivery_options.can_change_delivery_service?).to be true 23 | end 24 | 25 | it "returns false if the there is an existing delivery service" do 26 | allow(customer).to receive(:delivery_service_id) { 3 } 27 | expect(delivery_options.can_change_delivery_service?).to be false 28 | end 29 | end 30 | 31 | describe "#order_frequencies" do 32 | it "returns a list of order frequency options" do # rubocop:disable RSpec/ExampleLength 33 | expected_options = [ 34 | ["- Select delivery frequency -", nil], 35 | ["Deliver weekly on...", :weekly], 36 | ["Deliver every 2 weeks on...", :fortnightly], 37 | ["Deliver monthly", :monthly], 38 | ["Deliver once", :single], 39 | ] 40 | 41 | expect(delivery_options.order_frequencies).to eq(expected_options) 42 | end 43 | end 44 | 45 | describe "#extra_frequencies" do 46 | it "returns a list of extra frequency options" do 47 | expected_options = [["Include Extra Items with EVERY delivery", false], ["Include Extra Items with NEXT delivery only", true]] 48 | expect(delivery_options.extra_frequencies).to eq(expected_options) 49 | end 50 | end 51 | 52 | describe "#cart_has_extras" do 53 | it "returns true if this cart allows extras" do 54 | allow(cart).to receive(:has_extras?) { true } 55 | expect(delivery_options.cart_has_extras?).to be true 56 | end 57 | end 58 | 59 | describe "#to_h" do 60 | it "returns a hash of the important form data" do # rubocop:disable RSpec/ExampleLength 61 | start_date = Date.parse("2013-02-03") 62 | delivery_options.delivery_service = 3 63 | delivery_options.start_date = start_date 64 | delivery_options.frequency = "single" 65 | delivery_options.days = { 1 => 2 } 66 | delivery_options.extra_frequency = true 67 | expect(delivery_options.to_h).to eq(delivery_service_id: 3, start_date: start_date, frequency: "single", days: { 1 => 2 }, extra_frequency: true) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/models/checkout/home_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../../app/models/checkout/home" 4 | 5 | describe Home do 6 | let(:webstore) { double("webstore") } # rubocop:disable RSpec/VerifiedDoubles 7 | let(:logged_in_customer) { instance_double(Customer) } 8 | let(:home) { described_class.new(args) } 9 | let(:args) do 10 | { 11 | webstore: webstore, 12 | logged_in_customer: logged_in_customer, 13 | existing_customer: nil, 14 | } 15 | end 16 | 17 | describe "#customer" do 18 | it "returns a webstore customer" do 19 | customer = instance_double(Customer) 20 | customer_class = class_double(Customer, new: customer) 21 | expect(home.customer(customer_class)).to eq(customer) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/models/form_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../app/models/form" 4 | 5 | describe Form do 6 | let(:form) { described_class.new } 7 | 8 | describe "#save" do 9 | let(:cart) { instance_double(Cart, add_order_information: true) } 10 | 11 | before { allow(form).to receive(:cart) { cart } } 12 | 13 | context "when successful" do 14 | it "returns true" do 15 | allow(cart).to receive(:save) { true } 16 | expect(form.save).to be true 17 | end 18 | end 19 | 20 | context "when unsuccessful" do 21 | it "returns false" do 22 | allow(cart).to receive(:save) { false } 23 | expect(form.save).to be false 24 | end 25 | end 26 | end 27 | 28 | describe "#persisted?" do 29 | it "is false" do 30 | expect(form.persisted?).to be false 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/models/order_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../app/models/order" 4 | 5 | describe Order do 6 | let(:delivery_service_class) { instance_double("delivery_service_class") } 7 | let(:product) { instance_double(Product) } 8 | let(:product_class) { instance_double("product_class", find: product) } 9 | let(:cart) { instance_double(Cart).as_null_object } 10 | let(:args) { { cart: cart, delivery_service_class: delivery_service_class, product_class: product_class } } 11 | let(:order) { described_class.new(args) } 12 | 13 | describe "#extra_quantity" do 14 | it "returns the quantity for an extra in this order" do 15 | allow(order).to receive(:extras) { { 1 => 1 } } 16 | extra = double("extra", id: 1) # rubocop:disable RSpec/VerifiedDoubles 17 | expect(order.extra_quantity(extra)).to eq(1) 18 | end 19 | end 20 | 21 | describe "#extras_price" do 22 | it "returns the total price of the extras" do 23 | expected_array = [double("tuple1"), double("tuple2")] # rubocop:disable RSpec/VerifiedDoubles 24 | allow(order).to receive(:extras_as_hashes) 25 | order_price_class = class_double(OrderPrice, extras_price: expected_array) 26 | expect(order.extras_price(order_price_class)).to eq(expected_array) 27 | end 28 | end 29 | 30 | describe "#scheduled?" do 31 | it "return true if the order has a schedule" do 32 | allow(order).to receive(:frequency) { "single" } 33 | expect(order).to be_is_scheduled 34 | end 35 | end 36 | 37 | describe "#delivery_service_fee" do 38 | it "return the delivery fee" do 39 | allow(order).to receive(:delivery_service) { object_double("delivery_service", fee: 5) } 40 | expect(order.delivery_service_fee).to eq(5) 41 | end 42 | end 43 | 44 | describe "#has_total?" do 45 | it "returns true if can calculate a total" do 46 | allow(order).to receive(:information) { { complete: false } } 47 | expect(order.has_total?).to eq(true) 48 | end 49 | end 50 | 51 | describe "#total" do 52 | before do 53 | allow(order).to receive(:product_price) { 10 } 54 | allow(order).to receive(:extras_price) { 10 } 55 | allow(order).to receive(:delivery_service_fee) { 5 } 56 | end 57 | 58 | it "returns the total cost of the order" do 59 | expect(order.total).to eq(10 + 10 + 5) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/requests/application_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ApplicationController do 4 | describe "GET /ping" do 5 | it "is successful" do 6 | get ping_path 7 | 8 | expect(response).to have_http_status :ok 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/requests/map_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe MapController do 4 | describe "GET /" do 5 | it "is successful" do 6 | get root_path 7 | 8 | expect(response).to be_success 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["RAILS_ENV"] ||= "test" 4 | 5 | require "simplecov" if ENV["COVERAGE"] 6 | 7 | require File.expand_path("../config/environment", __dir__) 8 | 9 | require "rspec/rails" 10 | RSpec.configure do |config| 11 | config.raise_errors_for_deprecations! 12 | config.infer_spec_type_from_file_location! 13 | # config.disable_monkey_patching! # TODO 14 | config.warnings = true 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/capybara/select2_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Select2Helper 4 | # @example 5 | # select2 "Item", from: "select_id" 6 | # select2 /^Item/, from: "select_id" 7 | # 8 | # @note Works with Select2 version 3.4.1. 9 | def select2(text, options) 10 | find("#s2id_#{options[:from]}").click 11 | all(".select2-result-label").find do |result| 12 | result.text =~ Regexp.new(text) 13 | end.click 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/javascripts/jquery.imagesloaded.min.js: -------------------------------------------------------------------------------- 1 | (function(c,n){var l="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";c.fn.imagesLoaded=function(f){function m(){var b=c(i),a=c(h);d&&(h.length?d.reject(e,b,a):d.resolve(e));c.isFunction(f)&&f.call(g,e,b,a)}function j(b,a){b.src===l||-1!==c.inArray(b,k)||(k.push(b),a?h.push(b):i.push(b),c.data(b,"imagesLoaded",{isBroken:a,src:b.src}),o&&d.notifyWith(c(b),[a,e,c(i),c(h)]),e.length===k.length&&(setTimeout(m),e.unbind(".imagesLoaded")))}var g=this,d=c.isFunction(c.Deferred)?c.Deferred(): 2 | 0,o=c.isFunction(d.notify),e=g.find("img").add(g.filter("img")),k=[],i=[],h=[];c.isPlainObject(f)&&c.each(f,function(b,a){if("callback"===b)f=a;else if(d)d[b](a)});e.length?e.bind("load.imagesLoaded error.imagesLoaded",function(b){j(b.target,"error"===b.type)}).each(function(b,a){var d=a.src,e=c.data(a,"imagesLoaded");if(e&&e.src===d)j(a,e.isBroken);else if(a.complete&&a.naturalWidth!==n)j(a,0===a.naturalWidth||0===a.naturalHeight);else if(a.readyState||a.complete)a.src=l,a.src=d}):m();return d?d.promise(g): 3 | g}})(jQuery); 4 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/vendor/assets/stylesheets/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: #8B0; 8 | 9 | position: fixed; 10 | z-index: 1031; 11 | top: 0; 12 | left: 0; 13 | 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | /* Fancy blur effect */ 19 | #nprogress .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0px; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px #8B0, 0 0 5px #8B0; 26 | opacity: 1.0; 27 | 28 | -webkit-transform: rotate(3deg) translate(0px, -4px); 29 | -ms-transform: rotate(3deg) translate(0px, -4px); 30 | transform: rotate(3deg) translate(0px, -4px); 31 | } 32 | 33 | /* Remove these to get rid of the spinner */ 34 | #nprogress .spinner { 35 | display: block; 36 | position: fixed; 37 | z-index: 1031; 38 | top: 15px; 39 | right: 15px; 40 | } 41 | 42 | #nprogress .spinner-icon { 43 | width: 18px; 44 | height: 18px; 45 | box-sizing: border-box; 46 | 47 | border: solid 2px transparent; 48 | border-top-color: #8B0; 49 | border-left-color: #8B0; 50 | border-radius: 50%; 51 | 52 | -webkit-animation: nprogress-spinner 400ms linear infinite; 53 | animation: nprogress-spinner 400ms linear infinite; 54 | } 55 | 56 | .nprogress-custom-parent { 57 | overflow: hidden; 58 | position: relative; 59 | } 60 | 61 | .nprogress-custom-parent #nprogress .spinner, 62 | .nprogress-custom-parent #nprogress .bar { 63 | position: absolute; 64 | } 65 | 66 | @-webkit-keyframes nprogress-spinner { 67 | 0% { -webkit-transform: rotate(0deg); } 68 | 100% { -webkit-transform: rotate(360deg); } 69 | } 70 | @keyframes nprogress-spinner { 71 | 0% { transform: rotate(0deg); } 72 | 100% { transform: rotate(360deg); } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /vendor/bin/install_phantomjs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | # Install PhantomJS on a Debian-like distro. 4 | 5 | VERSION=2.1.1 6 | PHANTOM_JS=phantomjs-$VERSION-linux-x86_64 7 | 8 | cd /tmp 9 | wget --no-verbose https://github.com/Medium/phantomjs/releases/download/v$VERSION/$PHANTOM_JS.tar.bz2 10 | tar xf $PHANTOM_JS.tar.bz2 11 | mv $PHANTOM_JS /usr/local/share 12 | ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/local/bin 13 | -------------------------------------------------------------------------------- /vendor/bin/yamlkeysdiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/2ae8d9bb85c015cd907f9bb22e15204d256ca470/vendor/bin/yamlkeysdiff --------------------------------------------------------------------------------