├── .gitmodules ├── log └── .keep ├── app ├── mailers │ └── .keep ├── models │ ├── .keep │ ├── concerns │ │ └── .keep │ ├── product.rb │ ├── order_price.rb │ ├── checkout │ │ ├── home.rb │ │ ├── authentication.rb │ │ ├── checkout.rb │ │ ├── completed.rb │ │ ├── delivery_options.rb │ │ ├── customise_order.rb │ │ └── payment_options.rb │ ├── factory.rb │ ├── cart_persistence.rb │ ├── form.rb │ ├── customer.rb │ ├── order_factory.rb │ ├── customer_factory.rb │ ├── cart.rb │ └── order.rb ├── assets │ ├── images │ │ ├── .keep │ │ ├── background.png │ │ ├── close-icon.png │ │ ├── phone-icon.png │ │ ├── facebook-icon.png │ │ ├── map-marker-icon.png │ │ ├── bucky-box-powered.png │ │ ├── webstore-background.png │ │ └── fallbacks │ │ │ └── box │ │ │ └── box_image │ │ │ └── webstore_default.png │ ├── stylesheets │ │ ├── application.sass │ │ └── map.sass │ └── javascripts │ │ ├── nprogress-config.js │ │ ├── application.js │ │ └── map.js ├── controllers │ ├── concerns │ │ └── .keep │ ├── map_controller.rb │ ├── application_controller.rb │ ├── session_controller.rb │ ├── completed_controller.rb │ ├── customise_order_controller.rb │ ├── payment_options_controller.rb │ ├── delivery_options_controller.rb │ ├── authentication_controller.rb │ ├── store_controller.rb │ └── checkout_controller.rb ├── views │ ├── payments │ │ ├── _cash_on_delivery.html.haml │ │ ├── _bank_deposit.html.haml │ │ └── _paypal.html.haml │ ├── customise_order │ │ ├── _extra.html.haml │ │ └── customise_order.html.haml │ ├── store │ │ ├── _product.html.haml │ │ └── home.html.haml │ ├── layouts │ │ ├── _common_head.html.haml │ │ ├── _bugsnag.html.haml │ │ ├── _i18n.html.haml │ │ ├── _pingdom.html.haml │ │ ├── application.html.haml │ │ ├── _google_analytics.html.haml │ │ └── _banner.html.haml │ ├── application │ │ ├── _payment_instructions.html.haml │ │ ├── _sidebar.html.haml │ │ └── _order.html.haml │ ├── session │ │ └── new.html.haml │ ├── completed │ │ └── completed.html.haml │ ├── authentication │ │ └── authentication.html.haml │ ├── map │ │ └── index.html.haml │ ├── delivery_options │ │ ├── _delivery_service.html.haml │ │ └── delivery_options.html.haml │ └── payment_options │ │ └── payment_options.html.haml ├── decorators │ ├── customer_decorator.rb │ ├── product_decorator.rb │ ├── cart_decorator.rb │ ├── delivery_service_decorator.rb │ └── order_decorator.rb ├── helpers │ └── application_helper.rb └── services │ └── api.rb ├── lib ├── assets │ └── .keep ├── tasks │ ├── .keep │ └── assets.rake ├── paypal_form.rb ├── phone_collection.rb ├── address.rb └── schedule_rule.rb ├── .ruby-version ├── public ├── 422.html ├── robots.txt ├── favicon.ico ├── favicon-red.ico ├── favicon-gray.ico ├── apple-touch-icon.png └── assets │ ├── layers-1dbbe9d028e292f36fcba8f8b3a28d5e8932754fc2215b9ac69e4cdecf5107c6.png │ ├── map-4aa8a25cbf7a3b1c49089b140a359ac8bae65503825dd6b38dd7be725b323a0e.js.gz │ ├── map-9a88d1a7714d73d7ecc1c1ea35031b3537b7b7b1167634702b923257f5ed6fdf.css.gz │ ├── select2-d6b5d8d83dbc18fb8d77c8761d331cd9e5123c9684950bab0406e98a24ac5ae8.png │ ├── layers-2x-066daca850d8ffbef007af00b06eac0015728dee279c51f3cb6c716df7c42edf.png │ ├── select2x2-6fe28d687dc0ed4d96016238c608ba1e7198c9c9accfa0b360b78018b9fb9bc2.png │ ├── background-5f61dbdbbda4f607223a5b6b855ea75ccfada574f3a4a5f3c3f2df744e7469d1.png │ ├── close-icon-397a02e804aad172d9186b3f92da72c5d323618f4965917901230bb9bf12ca8a.png │ ├── marker-icon-574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437.png │ ├── phone-icon-2456db717faf8f3bee70c50bf2ece7e7a4d0ea4f613fac6dd8cf3a897bfd8a7c.png │ ├── application-731c774331642949afafd49563b1ad64e3ee06bdc5e886d3188c43247c00709a.js.gz │ ├── application-f38798986a4c88227b916d75d74dbedc35a7b60b67f4a1a40fc92ca6cdceea02.css.gz │ ├── facebook-icon-1f1ef4d63dffd8ad069e72c67ed5b635a62c3103b954d823aa54d475e3437ef3.png │ ├── marker-icon-2x-2d77a2e4c2f08bbac41808324ef946b9a2fe61b6150480d011b72b379c3b238d.png │ ├── marker-shadow-264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da.png │ ├── bucky-box-powered-7d986f2ee6ad772d8880f6fbddc0a2ae2e6366c872a4934d472db593a11625bb.png │ ├── map-marker-icon-cee88d7d4f8b842112804ca8f8a718f893d5b05f9f925e84553c61bc81244191.png │ ├── select2-spinner-f6ecff617ec2ba7f559e6f535cad9b70a3f91120737535dab4d4548a6c83576c.gif │ ├── webstore-background-25dc57c5899bd5f11d50dfe04ed11577654bdddacaa138725ccfd158e472728d.png │ ├── glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png │ ├── glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png │ └── fallbacks │ └── box │ └── box_image │ └── webstore_default-f6ac6c6001ef9b277031a76f571ead446e230037099e0a92b76b6c6dd38b20cd.png ├── config ├── locales │ ├── pt_BR │ ├── zh │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ ├── common.yml │ │ └── webstore.yml │ ├── en │ │ ├── javascript.yml │ │ ├── common.yml │ │ ├── simple_form.yml │ │ └── webstore.yml │ ├── fr │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ ├── common.yml │ │ └── webstore.yml │ ├── it │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ ├── common.yml │ │ └── webstore.yml │ ├── nl │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ ├── common.yml │ │ └── webstore.yml │ ├── de │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ ├── common.yml │ │ └── webstore.yml │ └── pt-BR │ │ ├── javascript.yml │ │ ├── simple_form.yml │ │ ├── common.yml │ │ └── webstore.yml ├── initializers │ ├── redis.rb │ ├── naught.rb │ ├── cookies_serializer.rb │ ├── mime_types.rb │ ├── filter_parameter_logging.rb │ ├── bugsnag.rb │ ├── session_store.rb │ ├── backtrace_silencers.rb │ ├── assets.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ ├── secure_headers.rb │ └── simple_form_bootstrap.rb ├── boot.rb ├── environment.rb ├── puma.rb ├── application.yml.example ├── cucumber.yml ├── secrets.yml ├── environments │ ├── test.rb │ ├── development.rb │ └── production.rb ├── routes.rb └── application.rb ├── vendor ├── assets │ ├── javascripts │ │ ├── .keep │ │ └── jquery.imagesloaded.min.js │ └── stylesheets │ │ ├── .keep │ │ └── nprogress.css └── bin │ ├── yamlkeysdiff │ └── install_phantomjs.sh ├── features ├── step_definitions │ ├── .gitkeep │ └── webstore_steps.rb ├── webstore_unauthenticated.feature ├── support │ └── env.rb └── webstore_authenticated.feature ├── .ebextensions └── 00-packages.config ├── .rspec ├── doc └── screenshot.jpg ├── bin ├── rake ├── bundle ├── rails ├── ci ├── check_i18n └── setup ├── .pullreview.yml ├── config.ru ├── .travis.yml ├── spec ├── requests │ ├── map_controller_spec.rb │ └── application_controller_spec.rb ├── spec_helper.rb ├── support │ └── capybara │ │ └── select2_helper.rb ├── lib │ └── schedule_rule_spec.rb └── models │ ├── checkout │ ├── home_spec.rb │ ├── authentication_spec.rb │ ├── checkout_spec.rb │ ├── delivery_options_spec.rb │ └── customise_order_spec.rb │ ├── form_spec.rb │ ├── order_spec.rb │ └── cart_spec.rb ├── .simplecov ├── .gitlab-ci.yml ├── Rakefile ├── .gitignore ├── LICENSE.txt ├── .tx └── config ├── .rubocop.yml ├── README.md └── Gemfile /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.8 2 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 500.html -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/locales/pt_BR: -------------------------------------------------------------------------------- 1 | pt-BR -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /features/step_definitions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # Hello Robot :) 2 | -------------------------------------------------------------------------------- /.ebextensions/00-packages.config: -------------------------------------------------------------------------------- 1 | packages: 2 | yum: 3 | git: [] 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /doc/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/doc/screenshot.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-red.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/favicon-red.ico -------------------------------------------------------------------------------- /public/favicon-gray.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/favicon-gray.ico -------------------------------------------------------------------------------- /vendor/bin/yamlkeysdiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/vendor/bin/yamlkeysdiff -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /config/locales/zh/javascript.yml: -------------------------------------------------------------------------------- 1 | zh: 2 | javascript: 3 | customise_order: 4 | add_extra: 添加额外项目 5 | -------------------------------------------------------------------------------- /app/assets/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/app/assets/images/background.png -------------------------------------------------------------------------------- /app/assets/images/close-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/app/assets/images/close-icon.png -------------------------------------------------------------------------------- /app/assets/images/phone-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/app/assets/images/phone-icon.png -------------------------------------------------------------------------------- /app/assets/images/facebook-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/app/assets/images/facebook-icon.png -------------------------------------------------------------------------------- /config/locales/en/javascript.yml: -------------------------------------------------------------------------------- 1 | en: 2 | javascript: 3 | customise_order: 4 | add_extra: Add an extra item 5 | -------------------------------------------------------------------------------- /app/assets/images/map-marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/app/assets/images/map-marker-icon.png -------------------------------------------------------------------------------- /config/locales/fr/javascript.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | javascript: 3 | customise_order: 4 | add_extra: Ajouter un nouvel extra 5 | -------------------------------------------------------------------------------- /config/locales/it/javascript.yml: -------------------------------------------------------------------------------- 1 | it: 2 | javascript: 3 | customise_order: 4 | add_extra: Aggiungi un prodotto 5 | -------------------------------------------------------------------------------- /app/assets/images/bucky-box-powered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/app/assets/images/bucky-box-powered.png -------------------------------------------------------------------------------- /config/locales/nl/javascript.yml: -------------------------------------------------------------------------------- 1 | nl: 2 | javascript: 3 | customise_order: 4 | add_extra: Voeg een extra product toe 5 | -------------------------------------------------------------------------------- /app/assets/images/webstore-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/app/assets/images/webstore-background.png -------------------------------------------------------------------------------- /config/locales/de/javascript.yml: -------------------------------------------------------------------------------- 1 | de: 2 | javascript: 3 | customise_order: 4 | add_extra: Füge einen weiteren Artikel hinzu 5 | -------------------------------------------------------------------------------- /config/locales/pt-BR/javascript.yml: -------------------------------------------------------------------------------- 1 | pt-BR: 2 | javascript: 3 | customise_order: 4 | add_extra: Adicionar um item extra 5 | -------------------------------------------------------------------------------- /app/models/product.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "draper" 4 | 5 | class Product 6 | include Draper::Decoratable 7 | end 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.pullreview.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | ignore: 4 | - missing_class_documentation 5 | - missing_method_documentation 6 | - prefer_single_quoted_strings 7 | 8 | -------------------------------------------------------------------------------- /app/assets/images/fallbacks/box/box_image/webstore_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/app/assets/images/fallbacks/box/box_image/webstore_default.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /public/assets/layers-1dbbe9d028e292f36fcba8f8b3a28d5e8932754fc2215b9ac69e4cdecf5107c6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/layers-1dbbe9d028e292f36fcba8f8b3a28d5e8932754fc2215b9ac69e4cdecf5107c6.png -------------------------------------------------------------------------------- /public/assets/map-4aa8a25cbf7a3b1c49089b140a359ac8bae65503825dd6b38dd7be725b323a0e.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/map-4aa8a25cbf7a3b1c49089b140a359ac8bae65503825dd6b38dd7be725b323a0e.js.gz -------------------------------------------------------------------------------- /public/assets/map-9a88d1a7714d73d7ecc1c1ea35031b3537b7b7b1167634702b923257f5ed6fdf.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/map-9a88d1a7714d73d7ecc1c1ea35031b3537b7b7b1167634702b923257f5ed6fdf.css.gz -------------------------------------------------------------------------------- /public/assets/select2-d6b5d8d83dbc18fb8d77c8761d331cd9e5123c9684950bab0406e98a24ac5ae8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/select2-d6b5d8d83dbc18fb8d77c8761d331cd9e5123c9684950bab0406e98a24ac5ae8.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/assets/layers-2x-066daca850d8ffbef007af00b06eac0015728dee279c51f3cb6c716df7c42edf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/layers-2x-066daca850d8ffbef007af00b06eac0015728dee279c51f3cb6c716df7c42edf.png -------------------------------------------------------------------------------- /public/assets/select2x2-6fe28d687dc0ed4d96016238c608ba1e7198c9c9accfa0b360b78018b9fb9bc2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/select2x2-6fe28d687dc0ed4d96016238c608ba1e7198c9c9accfa0b360b78018b9fb9bc2.png -------------------------------------------------------------------------------- /public/assets/background-5f61dbdbbda4f607223a5b6b855ea75ccfada574f3a4a5f3c3f2df744e7469d1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/background-5f61dbdbbda4f607223a5b6b855ea75ccfada574f3a4a5f3c3f2df744e7469d1.png -------------------------------------------------------------------------------- /public/assets/close-icon-397a02e804aad172d9186b3f92da72c5d323618f4965917901230bb9bf12ca8a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/close-icon-397a02e804aad172d9186b3f92da72c5d323618f4965917901230bb9bf12ca8a.png -------------------------------------------------------------------------------- /public/assets/marker-icon-574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/marker-icon-574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437.png -------------------------------------------------------------------------------- /public/assets/phone-icon-2456db717faf8f3bee70c50bf2ece7e7a4d0ea4f613fac6dd8cf3a897bfd8a7c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/phone-icon-2456db717faf8f3bee70c50bf2ece7e7a4d0ea4f613fac6dd8cf3a897bfd8a7c.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/assets/application-731c774331642949afafd49563b1ad64e3ee06bdc5e886d3188c43247c00709a.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/application-731c774331642949afafd49563b1ad64e3ee06bdc5e886d3188c43247c00709a.js.gz -------------------------------------------------------------------------------- /public/assets/application-f38798986a4c88227b916d75d74dbedc35a7b60b67f4a1a40fc92ca6cdceea02.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/application-f38798986a4c88227b916d75d74dbedc35a7b60b67f4a1a40fc92ca6cdceea02.css.gz -------------------------------------------------------------------------------- /public/assets/facebook-icon-1f1ef4d63dffd8ad069e72c67ed5b635a62c3103b954d823aa54d475e3437ef3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/facebook-icon-1f1ef4d63dffd8ad069e72c67ed5b635a62c3103b954d823aa54d475e3437ef3.png -------------------------------------------------------------------------------- /public/assets/marker-icon-2x-2d77a2e4c2f08bbac41808324ef946b9a2fe61b6150480d011b72b379c3b238d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/marker-icon-2x-2d77a2e4c2f08bbac41808324ef946b9a2fe61b6150480d011b72b379c3b238d.png -------------------------------------------------------------------------------- /public/assets/marker-shadow-264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/marker-shadow-264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da.png -------------------------------------------------------------------------------- /public/assets/bucky-box-powered-7d986f2ee6ad772d8880f6fbddc0a2ae2e6366c872a4934d472db593a11625bb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/bucky-box-powered-7d986f2ee6ad772d8880f6fbddc0a2ae2e6366c872a4934d472db593a11625bb.png -------------------------------------------------------------------------------- /public/assets/map-marker-icon-cee88d7d4f8b842112804ca8f8a718f893d5b05f9f925e84553c61bc81244191.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/map-marker-icon-cee88d7d4f8b842112804ca8f8a718f893d5b05f9f925e84553c61bc81244191.png -------------------------------------------------------------------------------- /public/assets/select2-spinner-f6ecff617ec2ba7f559e6f535cad9b70a3f91120737535dab4d4548a6c83576c.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/select2-spinner-f6ecff617ec2ba7f559e6f535cad9b70a3f91120737535dab4d4548a6c83576c.gif -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/assets/webstore-background-25dc57c5899bd5f11d50dfe04ed11577654bdddacaa138725ccfd158e472728d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/webstore-background-25dc57c5899bd5f11d50dfe04ed11577654bdddacaa138725ccfd158e472728d.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/assets/glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/glyphicons-halflings-d99e3fa32c641032f08149914b28c2dc6acf2ec62f70987f2259eabbfa7fc0de.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/assets/glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/glyphicons-halflings-white-f0e0d95a9c8abcdfabf46348e2d4285829bb0491f5f6af0e05af52bffb6324c4.png -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/assets/fallbacks/box/box_image/webstore_default-f6ac6c6001ef9b277031a76f571ead446e230037099e0a92b76b6c6dd38b20cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buckybox/webstore/HEAD/public/assets/fallbacks/box/box_image/webstore_default-f6ac6c6001ef9b277031a76f571ead446e230037099e0a92b76b6c6dd38b20cd.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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
")
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bucky Box Web Store
2 |
3 | [](https://gitlab.com/buckybox/webstore/commits/master)
4 | [](https://travis-ci.org/buckybox/webstore)
5 | [](https://coveralls.io/github/buckybox/webstore?branch=master)
6 | [](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 | 
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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/models/checkout/payment_options.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "../form"
4 |
5 | class PaymentOptions < Form
6 | PAID = "paid"
7 |
8 | attribute :name, String
9 | attribute :phone_number, String
10 | attribute :phone_type, String
11 | attribute :address_1, String
12 | attribute :address_2, String
13 | attribute :suburb, String
14 | attribute :city, String
15 | attribute :postcode, String
16 | attribute :delivery_note, String
17 | attribute :payment_method, String
18 | attribute :complete, Boolean
19 |
20 | validates :name, presence: true
21 | validates :phone_number, presence: true, if: :require_phone
22 | validates :phone_type, presence: true, if: :require_phone
23 | validates :address_1, presence: true, if: :require_address_1
24 | validates :address_2, presence: true, if: :require_address_2
25 | validates :suburb, presence: true, if: :require_suburb
26 | validates :city, presence: true, if: :require_city
27 | validates :postcode, presence: true, if: :require_postcode
28 | validates :delivery_note, presence: true, if: :require_delivery_note
29 | validates :payment_method, presence: true, if: :has_payment_options?
30 |
31 | attr_reader :address
32 |
33 | delegate :collect_phone, :require_phone, :collect_delivery_note, to: :webstore
34 | delegate :has_payment_options?, to: :cart
35 | delegate :address, to: :customer, prefix: true
36 | delegate :delivery_service, to: :order
37 |
38 | def name
39 | super || customer.name
40 | end
41 |
42 | def default_phone_number
43 | phone_number || address.default_phone_number
44 | end
45 |
46 | def default_phone_type
47 | phone_type || address.default_phone_type
48 | end
49 |
50 | def existing_customer?
51 | !customer.guest?
52 | end
53 |
54 | def phone_types(phone_collection_class = ::PhoneCollection)
55 | phone_collection_class.types_as_options
56 | end
57 |
58 | def pickup_point?
59 | delivery_service.pickup_point
60 | end
61 |
62 | def require_address_1
63 | !pickup_point? && webstore.require_address_1
64 | end
65 |
66 | def require_address_2
67 | !pickup_point? && webstore.require_address_2
68 | end
69 |
70 | def require_suburb
71 | !pickup_point? && webstore.require_suburb
72 | end
73 |
74 | def require_city
75 | !pickup_point? && webstore.require_city
76 | end
77 |
78 | def require_postcode
79 | !pickup_point? && webstore.require_postcode
80 | end
81 |
82 | def require_delivery_note
83 | !pickup_point? && webstore.require_delivery_note
84 | end
85 |
86 | # Returns whether the address is valid or not so we can hide the edit form when it is valid
87 | def address_complete?
88 | previous_errors = errors.dup
89 |
90 | begin
91 | valid? # populate `errors`
92 | (errors.keys - %i[phone_type payment_method]).empty?
93 | ensure
94 | # make sure we reset `errors` to its previous value for SimpleForm
95 | # kinda hackish but does the trick until we split up the models
96 | @errors = previous_errors
97 | end
98 | end
99 |
100 | def to_h
101 | {
102 | name: name,
103 | phone_number: phone_number,
104 | phone_type: phone_type,
105 | address_1: address_1,
106 | address_2: address_2,
107 | suburb: suburb,
108 | city: city,
109 | postcode: postcode,
110 | delivery_note: delivery_note,
111 | payment_method: payment_method,
112 | complete: complete,
113 | }
114 | end
115 |
116 | protected
117 |
118 | def before_standard_initialize(attributes)
119 | attributes = defaults.merge(attributes)
120 | @address_class = attributes.delete(:address_class)
121 | end
122 |
123 | def after_standard_initialize(_attributes)
124 | @address = build_address
125 | end
126 |
127 | private
128 |
129 | attr_reader :address_class
130 |
131 | def defaults
132 | { address_class: ::Address }
133 | end
134 |
135 | def build_address
136 | address = if customer.existing_customer
137 | address_class.new(customer.existing_customer.address.to_h)
138 | else
139 | address_class.new
140 | end
141 |
142 | # forward address attributes
143 | address_class.address_attributes.each do |attribute|
144 | address_value = address.public_send(attribute)
145 | public_send("#{attribute}=", address_value) unless public_send(attribute)
146 | address.public_send("#{attribute}=", public_send(attribute))
147 | end
148 |
149 | # set phone number
150 | address.phone = { type: phone_type, number: phone_number }
151 |
152 | address
153 | end
154 |
155 | def cart
156 | super || BlackHole.new
157 | end
158 |
159 | def webstore
160 | cart.webstore
161 | end
162 |
163 | def customer
164 | cart.customer
165 | end
166 |
167 | def order
168 | cart.order
169 | end
170 | end
171 |
--------------------------------------------------------------------------------