├── .rspec ├── app ├── views │ ├── layouts │ │ ├── mailer.text.erb │ │ └── mailer.html.erb │ └── spree │ │ ├── subscription_notifier │ │ ├── notify_reoccurrence.text.erb │ │ ├── notify_for_next_delivery.text.erb │ │ ├── notify_cancellation.text.erb │ │ ├── notify_confirmation.text.erb │ │ ├── notify_for_next_delivery.html.erb │ │ ├── notify_cancellation.html.erb │ │ ├── notify_reoccurrence.html.erb │ │ └── notify_confirmation.html.erb │ │ ├── orders │ │ ├── _cart_subscription_footer.html.erb │ │ ├── _cart_subscription_header.html.erb │ │ ├── _subscription_number.html.erb │ │ └── _subscription_field.html.erb │ │ ├── admin │ │ ├── products │ │ │ ├── _subscribable_listing_header.html.erb │ │ │ ├── _subscribable_listing_content.html.erb │ │ │ ├── _subscribable_filter.html.erb │ │ │ └── _subscribable.html.erb │ │ ├── shared │ │ │ ├── _flash_messages.html.erb │ │ │ └── _subscriptions_sidebar_menu.html.erb │ │ ├── subscription_frequencies │ │ │ ├── new.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── _form.html.erb │ │ │ └── index.html.erb │ │ ├── subscriptions │ │ │ ├── _pause_links.html.erb │ │ │ ├── _payment_info.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── cancellation.html.erb │ │ │ ├── _addresses.html.erb │ │ │ ├── _subscription.html.erb │ │ │ ├── index.html.erb │ │ │ ├── _summary.html.erb │ │ │ ├── _item.html.erb │ │ │ └── _orders.html.erb │ │ └── variants │ │ │ └── _autocomplete_line_items_stock.js.erb │ │ ├── shared │ │ └── _subscription_field.html.erb │ │ ├── subscriptions │ │ ├── _pause_links.html.erb │ │ ├── _payment_info.html.erb │ │ ├── edit.html.erb │ │ ├── _addresses.html.erb │ │ ├── _orders.html.erb │ │ ├── _subscription.html.erb │ │ ├── _item.html.erb │ │ └── _summary.html.erb │ │ ├── products │ │ ├── _cart_checkboxes.html.erb │ │ └── _subscription_fields.html.erb │ │ ├── api │ │ └── variants │ │ │ └── small.v1.rabl │ │ └── users │ │ └── _subscriptions.html.erb ├── assets │ ├── stylesheets │ │ └── spree │ │ │ ├── frontend │ │ │ ├── datepicker.css │ │ │ └── spree_product_subscriptions.scss │ │ │ └── backend │ │ │ ├── spree_product_subscriptions.css │ │ │ └── subscribable.scss │ └── javascripts │ │ └── spree │ │ ├── frontend │ │ ├── datepicker.js │ │ ├── spree_product_subscriptions.js │ │ ├── update_quantity_in_cart.js │ │ ├── cart_radio_button.js │ │ └── ajax_handler.js │ │ └── backend │ │ ├── spree_product_subscriptions.js │ │ ├── subscribable.js │ │ └── line_items_on_order_edit.js ├── controllers │ └── spree │ │ ├── base_controller_decorator.rb │ │ ├── admin │ │ ├── subscription_frequencies_controller.rb │ │ ├── payments_controller_decorator.rb │ │ └── subscriptions_controller.rb │ │ ├── api │ │ └── v1 │ │ │ └── line_items_controller_decorator.rb │ │ ├── users_controller_decorator.rb │ │ ├── orders_controller_decorator.rb │ │ └── subscriptions_controller.rb ├── mailers │ ├── application_mailer.rb │ └── spree │ │ └── subscription_notifier.rb ├── helpers │ └── spree │ │ └── subscriptions_helper.rb ├── overrides │ ├── add_checkboxes_to_product_show_page.rb │ ├── add_subscriptions_admin_tab.rb │ ├── add_subscriptions_to_users_account.rb │ ├── add_subscription_number_to_order_page.rb │ ├── add_subscribable_fields_to_products_show_page.rb │ ├── add_subscribable_to_products.rb │ ├── add_subscribable_checkbox_to_products_filter.rb │ ├── add_subscription_frequency_in_configurations.rb │ ├── add_subscribable_field_to_admin_product_listing.rb │ ├── add_subscribed_field_in_cart_listing.rb │ └── add_subscription_details_to_order_details.rb └── models │ ├── spree │ ├── variant_decorator.rb │ ├── product_subscription_frequency.rb │ ├── order_subscription.rb │ ├── subscription_frequency.rb │ ├── product_decorator.rb │ ├── order_decorator.rb │ ├── line_item_decorator.rb │ └── subscription.rb │ └── subscription_ability.rb ├── gemfiles ├── .bundle │ └── config ├── spree_3_3.gemfile ├── spree_3_4.gemfile ├── spree_3_5.gemfile └── spree_master.gemfile ├── config ├── initializers │ ├── spree_subscription_ability.rb │ └── permitted_attributes_ext.rb ├── routes.rb └── locales │ └── en.yml ├── lib ├── spree_product_subscriptions.rb ├── tasks │ ├── subscription_process.rake │ └── order_prior_notification.rake ├── generators │ └── spree_product_subscriptions │ │ ├── seed_generator.rb │ │ └── install │ │ └── install_generator.rb └── spree_product_subscriptions │ ├── engine.rb │ └── factories.rb ├── spec ├── controllers │ └── spree │ │ ├── admin │ │ ├── subscription_frequencies_controller_spec.rb │ │ ├── payments_controller_decorator_spec.rb │ │ └── subscriptions_controller_spec.rb │ │ ├── api │ │ └── v1 │ │ │ └── line_items_controller_decorator_spec.rb │ │ ├── users_controller_decorator_spec.rb │ │ ├── orders_controller_decorator_spec.rb │ │ └── subscriptions_controller_spec.rb ├── models │ ├── spree │ │ ├── product_subscription_frequency_spec.rb │ │ ├── order_subscription_spec.rb │ │ ├── variant_decorator_spec.rb │ │ ├── subscription_frequency_spec.rb │ │ ├── product_decorator_spec.rb │ │ ├── order_decorator_spec.rb │ │ └── line_item_decorator_spec.rb │ └── subscription_ability_spec.rb ├── mailers │ └── spree │ │ └── subscription_notifier_spec.rb └── spec_helper.rb ├── db ├── migrate │ ├── 20160311061107_remove_end_date_from_subscriptions.rb │ ├── 20160229121236_remove_frequency_from_spree_subscriptions.rb │ ├── 20160301075415_add_enabled_to_spree_subscriptions.rb │ ├── 20160311061332_add_delivery_number_to_spree_subscriptions.rb │ ├── 20160301124600_add_payment_reference_to_spree_subscriptions.rb │ ├── 20160229082140_add_subscribable_to_spree_products.rb │ ├── 20160303145717_add_months_count_column_to_spree_subscription_frequencies.rb │ ├── 20160426131235_add_prior_notification_time_to_subscription.rb │ ├── 20160301080555_add_subscription_frequency_reference_to_spre_subscriptions.rb │ ├── 20160302110531_add_columns_to_spree_subscriptions.rb │ ├── 20180508100649_add_occurence_possible_column_to_spree_subscriptions.rb │ ├── 20160303190542_add_polymorphic_source_reference_to_subscription.rb │ ├── 20160229122244_create_spree_subscription_frequencies.rb │ ├── 20160406115147_rename_pause_in_spree_subscriptions.rb │ ├── 20160405111047_add_pause_and_day_column_to_spree_subscriptions.rb │ ├── 20160229151248_add_default_to_subscribable_in_products.rb │ ├── 20160229122857_create_spree_product_subscription_frequencies.rb │ ├── 20160229110330_create_spree_order_subscriptions.rb │ ├── 20160406100301_add_next_occurrence_at_column_to_spree_subscriptions.rb │ ├── 20160411121318_change_pause_null_in_spree_subscriptions.rb │ ├── 20160229104012_create_spree_subscriptions.rb │ └── 20160301200702_change_names_of_tables_and_columns.rb └── seeds.rb ├── Gemfile ├── bin └── rails ├── Appraisals ├── Rakefile ├── .gitignore ├── LICENSE ├── spree_product_subscriptions.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "1" 3 | -------------------------------------------------------------------------------- /app/views/spree/subscription_notifier/notify_reoccurrence.text.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/spree/subscription_notifier/notify_for_next_delivery.text.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/spree/orders/_cart_subscription_footer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /config/initializers/spree_subscription_ability.rb: -------------------------------------------------------------------------------- 1 | Spree::Ability.register_ability(SubscriptionAbility) -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= yield %> 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/spree_product_subscriptions.rb: -------------------------------------------------------------------------------- 1 | require 'spree_core' 2 | require 'spree_product_subscriptions/engine' 3 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/frontend/datepicker.css: -------------------------------------------------------------------------------- 1 | #ui-datepicker-div { 2 | z-index: 3 !important; 3 | } 4 | -------------------------------------------------------------------------------- /app/views/spree/admin/products/_subscribable_listing_header.html.erb: -------------------------------------------------------------------------------- 1 | <%= Spree.t(:subscribable) %> 2 | -------------------------------------------------------------------------------- /app/controllers/spree/base_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::BaseController.class_eval do 2 | add_flash_types :success, :error 3 | end 4 | -------------------------------------------------------------------------------- /app/views/spree/admin/products/_subscribable_listing_content.html.erb: -------------------------------------------------------------------------------- 1 | <%= product.subscribable? ? "Yes" : "No" %> 2 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /config/initializers/permitted_attributes_ext.rb: -------------------------------------------------------------------------------- 1 | Spree::PermittedAttributes.line_item_attributes.push(:subscription_frequency_id, :delivery_number, :subscribe) 2 | -------------------------------------------------------------------------------- /app/views/spree/subscription_notifier/notify_cancellation.text.erb: -------------------------------------------------------------------------------- 1 | Hi <%= @subscription.parent_order.email %> 2 | 3 | Your <%= @subscription.frequency.title %> is cancelled 4 | -------------------------------------------------------------------------------- /app/views/spree/subscription_notifier/notify_confirmation.text.erb: -------------------------------------------------------------------------------- 1 | SubscriptionMailer#notify_user 2 | 3 | <%= @greeting %>, find me in app/views/subscription_mailer/notify_user.text.erb 4 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/subscription_frequencies_controller.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | module Admin 3 | class SubscriptionFrequenciesController < ResourceController 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/spree/admin/shared/_flash_messages.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /spec/controllers/spree/admin/subscription_frequencies_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::Admin::SubscriptionFrequenciesController, type: :controller do 4 | stub_authorization! 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/spree/api/v1/line_items_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Api::V1::LineItemsController.class_eval do 2 | 3 | self.line_item_options += [:subscribe, :delivery_number, :subscription_frequency_id] 4 | 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/frontend/datepicker.js: -------------------------------------------------------------------------------- 1 | //= require jquery-ui/widgets/datepicker 2 | 3 | $(function() { 4 | $('.datepicker').datepicker({ 5 | dateFormat: "dd-mm-yy", 6 | minDate: new Date() 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /app/helpers/spree/subscriptions_helper.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | module SubscriptionsHelper 3 | def days_left_for_next_occurrence 4 | (@subscription.next_occurrence_at.to_date - Date.current).to_i 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20160311061107_remove_end_date_from_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class RemoveEndDateFromSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | remove_column :spree_subscriptions, :end_date, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160229121236_remove_frequency_from_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class RemoveFrequencyFromSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | remove_column :spree_subscriptions, :frequency, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160301075415_add_enabled_to_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddEnabledToSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :spree_subscriptions, :enabled, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160311061332_add_delivery_number_to_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddDeliveryNumberToSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :spree_subscriptions, :delivery_number, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160301124600_add_payment_reference_to_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddPaymentReferenceToSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | add_reference :spree_subscriptions, :source, index: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/overrides/add_checkboxes_to_product_show_page.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: "spree/products/_cart_form", 3 | name: "add_checkboxes_to_cart_form", 4 | insert_before: ".add-to-cart", 5 | partial: "spree/products/cart_checkboxes" 6 | ) 7 | -------------------------------------------------------------------------------- /app/overrides/add_subscriptions_admin_tab.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: 'spree/layouts/admin', 3 | name: 'subscriptions_admin_tab', 4 | insert_bottom: '#main-sidebar', 5 | partial: 'spree/admin/shared/subscriptions_sidebar_menu' 6 | ) 7 | -------------------------------------------------------------------------------- /app/views/spree/orders/_cart_subscription_header.html.erb: -------------------------------------------------------------------------------- 1 | <%= Spree.t(:recurring_delivery_interval) %> 2 | <%= Spree.t(:total_deliveries) %> 3 | -------------------------------------------------------------------------------- /app/views/spree/orders/_subscription_number.html.erb: -------------------------------------------------------------------------------- 1 | <% if @order.order_subscription %> 2 |

Subscription Number: <%= link_to @order.order_subscription.subscription.number, edit_subscription_path(@order.order_subscription.subscription) %>

3 | <% end %> 4 | -------------------------------------------------------------------------------- /app/overrides/add_subscriptions_to_users_account.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: 'spree/users/show', 3 | name: 'add_subscriptions_to_users_account', 4 | insert_before: '[data-hook="account_my_orders"]', 5 | partial: 'spree/users/subscriptions' 6 | ) 7 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/backend/spree_product_subscriptions.css: -------------------------------------------------------------------------------- 1 | /* 2 | Placeholder manifest file. 3 | the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/backend/all.css' 4 | *= require spree/backend/subscribable 5 | */ 6 | -------------------------------------------------------------------------------- /app/overrides/add_subscription_number_to_order_page.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: 'spree/orders/show', 3 | name: 'add_subscription_number_to_order_page', 4 | insert_after: "fieldset#order_summary h1", 5 | partial: 'spree/orders/subscription_number' 6 | ) 7 | -------------------------------------------------------------------------------- /db/migrate/20160229082140_add_subscribable_to_spree_products.rb: -------------------------------------------------------------------------------- 1 | class AddSubscribableToSpreeProducts < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :spree_products, :subscribable, :boolean 4 | add_index :spree_products, :subscribable 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/overrides/add_subscribable_fields_to_products_show_page.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: "spree/products/_cart_form", 3 | name: "add_subscribable_fields_to_products_show", 4 | insert_after: ".add-to-cart", 5 | partial: "spree/products/subscription_fields" 6 | ) 7 | -------------------------------------------------------------------------------- /app/views/spree/admin/products/_subscribable_filter.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 | -------------------------------------------------------------------------------- /app/views/spree/admin/shared/_subscriptions_sidebar_menu.html.erb: -------------------------------------------------------------------------------- 1 | <% if can? :admin, Spree::Subscription %> 2 | 5 | <% end %> 6 | -------------------------------------------------------------------------------- /db/migrate/20160303145717_add_months_count_column_to_spree_subscription_frequencies.rb: -------------------------------------------------------------------------------- 1 | class AddMonthsCountColumnToSpreeSubscriptionFrequencies < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :spree_subscription_frequencies, :months_count, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160426131235_add_prior_notification_time_to_subscription.rb: -------------------------------------------------------------------------------- 1 | class AddPriorNotificationTimeToSubscription < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :spree_subscriptions, :prior_notification_days_gap, :integer, default: 7, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/overrides/add_subscribable_to_products.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: 'spree/admin/products/_form', 3 | name: 'add_subscribable_to_product_edit', 4 | insert_after: "[data-hook='admin_product_form_description']", 5 | partial: 'spree/admin/products/subscribable', 6 | ) 7 | -------------------------------------------------------------------------------- /db/migrate/20160301080555_add_subscription_frequency_reference_to_spre_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddSubscriptionFrequencyReferenceToSpreSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | add_reference :spree_subscriptions, :subscription_frequency, index: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160302110531_add_columns_to_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddColumnsToSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :spree_subscriptions, :number, :string 4 | add_column :spree_subscriptions, :cancellation_reasons, :text 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180508100649_add_occurence_possible_column_to_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddOccurencePossibleColumnToSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :spree_subscriptions, :next_occurrence_possible, :boolean, default: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/backend/subscribable.scss: -------------------------------------------------------------------------------- 1 | .subscribable-option { 2 | margin-right: 15px !important; 3 | } 4 | 5 | .subscribable_label { 6 | display: inline !important; 7 | } 8 | 9 | .subscribable_checkbox { 10 | outline: none !important; 11 | display: inline !important; 12 | } 13 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | Spree::SubscriptionFrequency.create(title: "monthly", months_count: 1) 2 | Spree::SubscriptionFrequency.create(title: "quarterly", months_count: 3) 3 | Spree::SubscriptionFrequency.create(title: "half yearly", months_count: 6) 4 | Spree::SubscriptionFrequency.create(title: "yearly", months_count: 12) 5 | -------------------------------------------------------------------------------- /db/migrate/20160303190542_add_polymorphic_source_reference_to_subscription.rb: -------------------------------------------------------------------------------- 1 | class AddPolymorphicSourceReferenceToSubscription < ActiveRecord::Migration[4.2] 2 | def change 3 | remove_reference :spree_subscriptions, :source 4 | add_reference :spree_subscriptions, :source, polymorphic: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20160229122244_create_spree_subscription_frequencies.rb: -------------------------------------------------------------------------------- 1 | class CreateSpreeSubscriptionFrequencies < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :spree_subscription_frequencies do |t| 4 | t.string :title 5 | 6 | t.timestamps null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20160406115147_rename_pause_in_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class RenamePauseInSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def up 3 | rename_column :spree_subscriptions, :pause, :paused 4 | end 5 | 6 | def down 7 | rename_column :spree_subscriptions, :paused, :pause 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/tasks/subscription_process.rake: -------------------------------------------------------------------------------- 1 | namespace :subscription do 2 | desc "process all subscriptions whom orders are to be created" 3 | task process: :environment do |t, args| 4 | Spree::Subscription.eligible_for_subscription.find_in_batches do |batches| 5 | batches.map(&:process) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/spree/variant_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Variant.class_eval do 2 | 3 | has_many :subscriptions, class_name: "Spree::Subscription", dependent: :restrict_with_error 4 | delegate :variants_including_master, to: :product, prefix: true 5 | alias_method :product_variants, :product_variants_including_master 6 | 7 | end 8 | -------------------------------------------------------------------------------- /app/overrides/add_subscribable_checkbox_to_products_filter.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: 'spree/admin/products/index', 3 | name: 'add_subscribable_filter_to_products', 4 | insert_bottom: "[data-hook='admin_products_index_search'] .col-md-12", 5 | partial: "spree/admin/products/subscribable_filter" 6 | ) 7 | -------------------------------------------------------------------------------- /db/migrate/20160405111047_add_pause_and_day_column_to_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddPauseAndDayColumnToSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :spree_subscriptions, :pause, :boolean, default: false 4 | add_column :spree_subscriptions, :delivery_day, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/order_prior_notification.rake: -------------------------------------------------------------------------------- 1 | namespace :subscription do 2 | desc "send prior notification for replenishment items" 3 | task prior_notify: :environment do |t, args| 4 | Spree::Subscription.processable.find_in_batches do |batches| 5 | batches.map(&:send_prior_notification) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'byebug', '~> 9.0.6', group: [:development, :test] 4 | 5 | group :test do 6 | gem 'rails-controller-testing', '~> 1.0.1' 7 | end 8 | 9 | gem 'spree', github: 'spree/spree', branch: 'master' 10 | gem 'spree_auth_devise', github: 'spree/spree_auth_devise' 11 | 12 | gemspec 13 | -------------------------------------------------------------------------------- /db/migrate/20160229151248_add_default_to_subscribable_in_products.rb: -------------------------------------------------------------------------------- 1 | class AddDefaultToSubscribableInProducts < ActiveRecord::Migration[4.2] 2 | def up 3 | change_column_default :spree_products, :subscribable, false 4 | end 5 | 6 | def down 7 | change_column_default :spree_products, :subscribable, nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 2 | 3 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 4 | ENGINE_PATH = File.expand_path('../../lib/spree_product_subscriptions/engine', __FILE__) 5 | 6 | require 'rails/all' 7 | require 'rails/engine/commands' 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/backend/spree_product_subscriptions.js: -------------------------------------------------------------------------------- 1 | // Placeholder manifest file. 2 | // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/backend/all.js' 3 | //= require spree/backend/subscribable 4 | //= require spree/frontend/ajax_handler 5 | //= require spree/backend/line_items_on_order_edit 6 | -------------------------------------------------------------------------------- /db/migrate/20160229122857_create_spree_product_subscription_frequencies.rb: -------------------------------------------------------------------------------- 1 | class CreateSpreeProductSubscriptionFrequencies < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :spree_product_subscription_frequencies do |t| 4 | t.references :product, index: true 5 | t.references :subscription_frequency 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20160229110330_create_spree_order_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class CreateSpreeOrderSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :spree_order_subscriptions do |t| 4 | t.references :subscription, index: true 5 | t.references :order, index: true 6 | t.date :failed_at 7 | t.text :failure_reasons 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20160406100301_add_next_occurrence_at_column_to_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddNextOccurrenceAtColumnToSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | remove_column :spree_subscriptions, :last_occurrence_at, :datetime 4 | remove_column :spree_subscriptions, :delivery_day, :integer 5 | add_column :spree_subscriptions, :next_occurrence_at, :datetime 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/frontend/spree_product_subscriptions.js: -------------------------------------------------------------------------------- 1 | // Placeholder manifest file. 2 | //= require spree/frontend/cart_radio_button.js 3 | //= require spree/frontend/datepicker 4 | //= require spree/frontend/ajax_handler.js 5 | //= require spree/frontend/update_quantity_in_cart 6 | // the installer will append this file to the app vendored assets here: vendor/assets/javascripts/spree/frontend/all.js' 7 | -------------------------------------------------------------------------------- /app/controllers/spree/users_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::UsersController.class_eval do 2 | 3 | before_action :load_subscriptions, only: :show 4 | 5 | private 6 | 7 | def load_subscriptions 8 | @orders = @user.orders.complete.order(completed_at: :desc) 9 | @subscriptions = Spree::Subscription.active.order(created_at: :desc).with_parent_orders(@orders) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscription_frequencies/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title do %> 2 | <%= Spree.t(:new_subscription_frequency) %> 3 | <% end %> 4 | 5 | <%= form_for @subscription_frequency, url: collection_url do |f| %> 6 |
7 | <%= render partial: 'form', locals: { f: f } %> 8 | <%= render partial: 'spree/admin/shared/new_resource_links' %> 9 |
10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/overrides/add_subscription_frequency_in_configurations.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: 'spree/admin/shared/sub_menu/_configuration', 3 | name: 'add_subscription_frequency_in_configurations', 4 | insert_bottom: "[data-hook='admin_configurations_sidebar_menu']", 5 | text: %q{ 6 | <%= configurations_sidebar_menu_item(Spree.t(:subscription_frequencies), admin_subscription_frequencies_path) %> 7 | } 8 | ) 9 | -------------------------------------------------------------------------------- /gemfiles/spree_3_3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", "~> 9.0.6", group: [:development, :test] 6 | gem "spree", "~> 3.3.0" 7 | gem "spree_auth_devise", github: "spree/spree_auth_devise" 8 | gem "rails-controller-testing", "~> 1.0.1" 9 | 10 | group :test do 11 | gem "rails-controller-testing", "~> 1.0.1" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/spree_3_4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", "~> 9.0.6", group: [:development, :test] 6 | gem "spree", "~> 3.4.0" 7 | gem "spree_auth_devise", github: "spree/spree_auth_devise" 8 | gem "rails-controller-testing", "~> 1.0.1" 9 | 10 | group :test do 11 | gem "rails-controller-testing", "~> 1.0.1" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /db/migrate/20160411121318_change_pause_null_in_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class ChangePauseNullInSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def up 3 | change_column_null :spree_subscriptions, :paused, false 4 | add_index :spree_subscriptions, :paused 5 | end 6 | 7 | def down 8 | change_column_null :spree_subscriptions, :paused, true 9 | remove_index :spree_subscriptions, :paused 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/spree/product_subscription_frequency.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class ProductSubscriptionFrequency < Spree::Base 3 | 4 | belongs_to :product, class_name: "Spree::Product" 5 | belongs_to :subscription_frequency, class_name: "Spree::SubscriptionFrequency" 6 | 7 | validates :product, :subscription_frequency, presence: true 8 | validates :product, uniqueness: { scope: :subscription_frequency } 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /gemfiles/spree_3_5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", "~> 9.0.6", group: [:development, :test] 6 | gem "spree", github: "spree/spree", branch: "3-5-stable" 7 | gem "spree_auth_devise", github: "spree/spree_auth_devise" 8 | gem "rails-controller-testing", "~> 1.0.1" 9 | 10 | group :test do 11 | gem "rails-controller-testing", "~> 1.0.1" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/spree_master.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "byebug", "~> 9.0.6", group: [:development, :test] 6 | gem "spree", github: "spree/spree", branch: "master" 7 | gem "spree_auth_devise", github: "spree/spree_auth_devise" 8 | gem "rails-controller-testing", "~> 1.0.1" 9 | 10 | group :test do 11 | gem "rails-controller-testing", "~> 1.0.1" 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /app/models/spree/order_subscription.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class OrderSubscription < Spree::Base 3 | 4 | self.table_name = "spree_orders_subscriptions" 5 | 6 | belongs_to :order, class_name: "Spree::Order" 7 | belongs_to :subscription, class_name: "Spree::Subscription" 8 | 9 | validates :order, :subscription, presence: true 10 | validates :order_id, uniqueness: { scope: :subscription_id, allow_blank: true } 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscription_frequencies/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title do %> 2 | <%= Spree.t(:editing_resource, resource: Spree::SubscriptionFrequency.model_name.human) %> 3 | <% end %> 4 | 5 | <%= form_for @subscription_frequency, url: object_url, method: :put do |f| %> 6 |
7 | <%= render partial: 'form', locals: { f: f } %> 8 | <%= render partial: 'spree/admin/shared/edit_resource_links' %> 9 |
10 | <% end %> 11 | -------------------------------------------------------------------------------- /lib/generators/spree_product_subscriptions/seed_generator.rb: -------------------------------------------------------------------------------- 1 | module SpreeProductSubscriptions 2 | module Generators 3 | class SeedGenerator < Rails::Generators::Base 4 | source_root File.expand_path("../../templates", __FILE__) 5 | 6 | desc "Create demo gift card" 7 | 8 | def run_db_seeds 9 | seed_file = File.join(File.expand_path("../../../../db", __FILE__), "seeds.rb") 10 | load(seed_file) if File.exist?(seed_file) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/views/spree/shared/_subscription_field.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <% subscription = @order.subscriptions.find_by(variant: item.variant) %> 3 | <% if subscription %> 4 | <%= subscription.frequency.title.capitalize %> 5 | <% else %> 6 | Not Subscribed 7 | <% end %> 8 | 9 | 10 | <% if subscription %> 11 | <%= subscription.delivery_number %> 12 | <% else %> 13 | None 14 | <% end %> 15 | 16 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/payments_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Admin::PaymentsController.class_eval do 2 | 3 | private 4 | 5 | def load_data 6 | @amount = params[:amount] || load_order.total 7 | @payment_methods = available_payment_methods 8 | @payment_method = @payment.try(:payment_method) || @payment_methods.first 9 | end 10 | 11 | def available_payment_methods 12 | @order.subscriptions.any? ? Spree::Gateway.available_on_back_end : Spree::PaymentMethod.available_on_back_end 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/controllers/spree/api/v1/line_items_controller_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::Api::V1::LineItemsController, type: :controller do 4 | 5 | describe "line_item_options" do 6 | it { expect(Spree::Api::V1::LineItemsController.line_item_options).to include :subscription_frequency_id } 7 | it { expect(Spree::Api::V1::LineItemsController.line_item_options).to include :delivery_number } 8 | it { expect(Spree::Api::V1::LineItemsController.line_item_options).to include :subscribe } 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'spree-3-3' do 2 | gem 'spree', '~> 3.3.0' 3 | gem 'rails-controller-testing', '~> 1.0.1' 4 | end 5 | 6 | appraise 'spree-3-4' do 7 | gem 'spree', '~> 3.4.0' 8 | gem 'rails-controller-testing', '~> 1.0.1' 9 | end 10 | 11 | appraise 'spree-3-5' do 12 | gem 'spree', github: 'spree/spree', branch: '3-5-stable' 13 | gem 'rails-controller-testing', '~> 1.0.1' 14 | end 15 | 16 | appraise 'spree-master' do 17 | gem 'spree', github: 'spree/spree', branch: 'master' 18 | gem 'rails-controller-testing', '~> 1.0.1' 19 | end 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | require 'spree/testing_support/extension_rake' 6 | 7 | RSpec::Core::RakeTask.new 8 | 9 | task :default do 10 | if Dir["spec/dummy"].empty? 11 | Rake::Task[:test_app].invoke 12 | Dir.chdir("../../") 13 | end 14 | Rake::Task[:spec].invoke 15 | end 16 | 17 | desc 'Generates a dummy app for testing' 18 | task :test_app do 19 | ENV['LIB_NAME'] = 'spree_product_subscriptions' 20 | Rake::Task['extension:test_app'].invoke 21 | end 22 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/frontend/spree_product_subscriptions.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Placeholder manifest file. 3 | the installer will append this file to the app vendored assets here: 'vendor/assets/stylesheets/spree/frontend/all.css' 4 | *= require jquery-ui/datepicker 5 | *= require spree/frontend/datepicker 6 | */ 7 | 8 | .form-group { 9 | &.withError { 10 | label, .formError, .required { 11 | color: red; 12 | } 13 | 14 | input { 15 | border-color: red; 16 | } 17 | } 18 | } 19 | 20 | .m-t-25 { 21 | margin-top: 10px; 22 | } 23 | -------------------------------------------------------------------------------- /app/models/subscription_ability.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionAbility 2 | include CanCan::Ability 3 | 4 | def initialize(user) 5 | if user.respond_to?(:has_spree_role?) && user.has_spree_role?('admin') 6 | can :manage, :all 7 | else 8 | can :create, ::Spree::Subscription 9 | can :read, ::Spree::Subscription do |subscription| 10 | subscription.parent_order.user == user 11 | end 12 | can :update, ::Spree::Subscription do |subscription| 13 | subscription.parent_order.user == user 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Spree::Core::Engine.routes.draw do 2 | 3 | namespace :admin do 4 | resources :subscription_frequencies 5 | resources :subscriptions, except: [:new, :destroy, :show] do 6 | member do 7 | patch :pause 8 | patch :unpause 9 | get :cancellation 10 | patch :cancel 11 | end 12 | end 13 | end 14 | 15 | resources :subscriptions, except: [:new, :destroy, :index, :show] do 16 | member do 17 | patch :pause 18 | patch :unpause 19 | patch :cancel 20 | end 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /app/views/spree/subscriptions/_pause_links.html.erb: -------------------------------------------------------------------------------- 1 | <% if @subscription.paused? %> 2 | <%= link_to Spree.t(:activate), "javascript:void(0)", class: "btn ajax_handler #{ btn_class } btn-success", data: { url: unpause_subscription_path(@subscription), method: "PATCH", confirmation: Spree.t("subscriptions.confirm.activate") } %> 3 | <% else %> 4 | <%= link_to Spree.t(:pause), "javascript:void(0)", class: "btn #{ btn_class } ajax_handler btn-warning", data: { url: pause_subscription_path(@subscription), method: "PATCH", confirmation: Spree.t("subscriptions.confirm.pause") } %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /spec/models/spree/product_subscription_frequency_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::ProductSubscriptionFrequency, type: :model do 4 | 5 | describe "associations" do 6 | it { is_expected.to belong_to(:product).class_name("Spree::Product") } 7 | it { is_expected.to belong_to(:subscription_frequency).class_name("Spree::SubscriptionFrequency") } 8 | end 9 | 10 | describe "validations" do 11 | it { is_expected.to validate_presence_of(:product) } 12 | it { is_expected.to validate_presence_of(:subscription_frequency) } 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/models/spree/order_subscription_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::OrderSubscription, type: :model do 4 | 5 | describe "associations" do 6 | it { is_expected.to belong_to(:order).class_name("Spree::Order") } 7 | it { is_expected.to belong_to(:subscription).class_name("Spree::Subscription") } 8 | end 9 | 10 | describe "validations" do 11 | it { is_expected.to validate_presence_of(:order) } 12 | it { is_expected.to validate_presence_of(:subscription) } 13 | it { is_expected.to validate_uniqueness_of(:order_id).scoped_to(:subscription_id) } 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /app/views/spree/products/_cart_checkboxes.html.erb: -------------------------------------------------------------------------------- 1 | <% if @product.subscribable? %> 2 |

3 |
4 | <%= radio_button_tag :cart, ".add-to-cart", true, class: "cart_radio_button" %> 5 | <%= label_tag "cart_.add-to-cart".to_s, Spree.t(:one_time_order), id: "cart_.add-to-cart" %> 6 |
7 |
8 | <%= radio_button_tag :cart, ".subscription_options", false, class: "cart_radio_button" %> 9 | <%= label_tag "cart_.subscription_options".to_s, Spree.t(:subscription_order) %> 10 |
11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/spree_product_subscriptions/engine.rb: -------------------------------------------------------------------------------- 1 | module SpreeProductSubscriptions 2 | class Engine < Rails::Engine 3 | require 'spree/core' 4 | isolate_namespace Spree 5 | engine_name 'spree_product_subscriptions' 6 | 7 | # use rspec for tests 8 | config.generators do |g| 9 | g.test_framework :rspec 10 | end 11 | 12 | def self.activate 13 | Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c| 14 | Rails.configuration.cache_classes ? require(c) : load(c) 15 | end 16 | end 17 | 18 | config.to_prepare &method(:activate).to_proc 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/models/spree/variant_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::Variant, type: :model do 4 | 5 | describe "associations" do 6 | it { is_expected.to have_many(:subscriptions).class_name("Spree::Subscription").dependent(:restrict_with_error) } 7 | end 8 | 9 | describe 'delegation' do 10 | it { is_expected.to delegate_method(:variants_including_master).to(:product).with_prefix(true) } 11 | end 12 | 13 | describe 'alias method' do 14 | it { expect(Spree::Variant.instance_method(:product_variants_including_master)).to eq Spree::Variant.instance_method(:product_variants) } 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /app/models/spree/subscription_frequency.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class SubscriptionFrequency < Spree::Base 3 | 4 | has_many :product_subscription_frequencies, class_name: "Spree::ProductSubscriptionFrequency", 5 | dependent: :destroy 6 | has_many :subscriptions, class_name: "Spree::Subscription", dependent: :restrict_with_error 7 | 8 | validates :title, :months_count, presence: true 9 | with_options allow_blank: true do 10 | validates :months_count, numericality: { greater_than: 0, only_integer: true } 11 | validates :title, uniqueness: { case_sensitive: false } 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20160229104012_create_spree_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class CreateSpreeSubscriptions < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :spree_subscriptions do |t| 4 | t.references :variant, index: true 5 | t.integer :quantity 6 | t.string :frequency 7 | t.references :parent_order, index: true 8 | t.references :ship_address, index: true 9 | t.references :bill_address, index: true 10 | t.date :last_recurrence_at, index: true 11 | t.date :end_date, index: true 12 | t.date :cancelled_at, index: true 13 | t.decimal :price, precision: 8, scale: 2 14 | 15 | t.timestamps null: false 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/_pause_links.html.erb: -------------------------------------------------------------------------------- 1 | <% if !@subscription.cancelled? && !@subscription.paused? %> 2 | <%= button_link_to Spree.t(:pause), "javascript:void(0)", class: 'ajax_handler btn-warning', icon: 'pause', data: { method: "PATCH", url: pause_admin_subscription_path(@subscription), confirmation: Spree.t("subscriptions.confirm.pause") } %> 3 | <% elsif @subscription.paused? && !@subscription.cancelled? %> 4 | <%= button_link_to Spree.t("subscriptions.activate"), "javascript:void(0)", class: 'btn-success ajax_handler', icon: 'play', data: { method: "PATCH", url: unpause_admin_subscription_path(@subscription), confirmation: Spree.t("subscriptions.confirm.activate") } %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /app/overrides/add_subscribable_field_to_admin_product_listing.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: "spree/admin/products/index", 3 | name: "add_subscribable_header_to_products_listing", 4 | insert_before: "[data-hook='admin_products_index_headers'] [data-hook='admin_products_index_header_actions']", 5 | partial: "spree/admin/products/subscribable_listing_header" 6 | ) 7 | 8 | Deface::Override.new( 9 | virtual_path: "spree/admin/products/index", 10 | name: "add_subscribable_content_to_products_listing", 11 | insert_before: "[data-hook='admin_products_index_rows'] [data-hook='admin_products_index_row_actions']", 12 | partial: "spree/admin/products/subscribable_listing_content" 13 | ) 14 | -------------------------------------------------------------------------------- /app/models/spree/product_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Product.class_eval do 2 | 3 | has_many :subscriptions, through: :variants_including_master, source: :subscriptions, dependent: :restrict_with_error 4 | has_many :product_subscription_frequencies, class_name: "Spree::ProductSubscriptionFrequency", dependent: :destroy 5 | has_many :subscription_frequencies, through: :product_subscription_frequencies, dependent: :destroy 6 | 7 | alias_attribute :subscribable, :is_subscribable 8 | 9 | self.whitelisted_ransackable_attributes += %w( is_subscribable ) 10 | 11 | scope :subscribable, -> { where(subscribable: true) } 12 | 13 | validates :subscription_frequencies, presence: true, if: :subscribable? 14 | 15 | end 16 | -------------------------------------------------------------------------------- /app/overrides/add_subscribed_field_in_cart_listing.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: "spree/orders/_line_item", 3 | name: "add_subscribed_field_to_cart_listing", 4 | insert_bottom: ".line-item", 5 | partial: "spree/orders/subscription_field" 6 | ) 7 | 8 | Deface::Override.new( 9 | virtual_path: "spree/orders/_form", 10 | name: "edit_header_in_cart_listing", 11 | insert_bottom: "[data-hook='cart_items_headers']", 12 | partial: "spree/orders/cart_subscription_header" 13 | ) 14 | 15 | Deface::Override.new( 16 | virtual_path: "spree/orders/_form", 17 | name: "edit_footer_in_cart_listing", 18 | insert_bottom: ".cart-total", 19 | partial: "spree/orders/cart_subscription_footer" 20 | ) 21 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/frontend/update_quantity_in_cart.js: -------------------------------------------------------------------------------- 1 | function ProductQuantity(data) { 2 | this.oneTimeOrderQuantity = data.oneTimeOrderQuantity; 3 | this.subscriptionQuantity = data.subscriptionQuantity; 4 | } 5 | 6 | ProductQuantity.prototype.init = function() { 7 | var _this = this; 8 | this.oneTimeOrderQuantity.change(function() { 9 | _this.subscriptionQuantity.val(this.value); 10 | }) 11 | }; 12 | 13 | $(function() { 14 | var cartInputFields = $('input#quantity'); 15 | var data = { 16 | oneTimeOrderQuantity: cartInputFields.eq(0), 17 | subscriptionQuantity: cartInputFields.eq(1) 18 | } 19 | var productQuantity = new ProductQuantity(data); 20 | productQuantity.init(); 21 | }) 22 | -------------------------------------------------------------------------------- /app/views/spree/orders/_subscription_field.html.erb: -------------------------------------------------------------------------------- 1 | <% subscription = @order.subscriptions.find_by(variant: variant) %> 2 | 3 | <% if subscription %> 4 | <%= item_form.collection_select :subscription_frequency_id, variant.product.subscription_frequencies, :id, :title, { selected: subscription.subscription_frequency_id }, { class: "form-control" } %> 5 | <% else %> 6 | Not Subscribed 7 | <% end %> 8 | 9 | 10 | <% if subscription %> 11 | <%= item_form.number_field :delivery_number, class: "form-control", min: 1, value: subscription.delivery_number %> 12 | <% else %> 13 | None 14 | <% end %> 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalization: 25 | /.bundle/ 26 | /vendor/bundle 27 | /lib/bundler/man/ 28 | 29 | 30 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 31 | .rvmrc 32 | \#* 33 | *~ 34 | .#* 35 | .DS_Store 36 | .idea 37 | .project 38 | .sass-cache 39 | nbproject 40 | Gemfile.lock 41 | .byebug_history 42 | pkg 43 | *.swp 44 | spec/dummy 45 | gemfiles/*.gemfile.lock 46 | -------------------------------------------------------------------------------- /app/views/spree/subscriptions/_payment_info.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= Spree.t(:credit_card) %> 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
<%= Spree.t(:name_on_card) %>:<%= @subscription.source.name %>
<%= Spree.t(:card_type) %>:<%= @subscription.source.cc_type %>
<%= Spree.t(:card_number) %>:<%= @subscription.source.display_number %>
<%= Spree.t(:expiration) %>:<%= @subscription.source.month %>/<%= @subscription.source.year %>
21 |
22 | -------------------------------------------------------------------------------- /app/overrides/add_subscription_details_to_order_details.rb: -------------------------------------------------------------------------------- 1 | Deface::Override.new( 2 | virtual_path: 'spree/shared/_order_details', 3 | name: "add_subscription_header_to_order_details", 4 | insert_bottom: '[data-hook="order_details_line_items_headers"]', 5 | partial: "spree/orders/cart_subscription_header" 6 | ) 7 | 8 | Deface::Override.new( 9 | virtual_path: 'spree/shared/_order_details', 10 | name: "add_subscription_body_to_order_details", 11 | insert_bottom: "[data-hook='order_details_line_item_row']", 12 | partial: "spree/shared/subscription_field" 13 | ) 14 | 15 | Deface::Override.new( 16 | virtual_path: 'spree/shared/_order_details', 17 | name: "add_subscription_footer_to_order_details", 18 | insert_bottom: ".total", 19 | partial: "spree/orders/cart_subscription_footer" 20 | ) 21 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/_payment_info.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= Spree.t(:credit_card) %> 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
<%= Spree.t(:name_on_card) %>:<%= @subscription.source.name %>
<%= Spree.t(:card_type) %>:<%= @subscription.source.cc_type %>
<%= Spree.t(:card_number) %>:<%= @subscription.source.display_number %>
<%= Spree.t(:expiration) %>:<%= @subscription.source.month %>/<%= @subscription.source.year %>
21 |
22 | -------------------------------------------------------------------------------- /lib/spree_product_subscriptions/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :monthly_subscription_frequency, class: Spree::SubscriptionFrequency do 3 | title "monthly" 4 | months_count 1 5 | end 6 | 7 | factory :nil_attributes_subscription, class: Spree::Subscription do 8 | end 9 | 10 | factory :valid_subscription, class: Spree::Subscription do 11 | price 20.00 12 | quantity 2 13 | delivery_number 4 14 | association :variant, factory: :base_variant 15 | association :frequency, factory: :monthly_subscription_frequency 16 | association :parent_order, factory: :completed_order_with_totals 17 | association :ship_address, factory: :address 18 | association :bill_address, factory: :address 19 | association :source, factory: :credit_card 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/spree/api/variants/small.v1.rabl: -------------------------------------------------------------------------------- 1 | cache [I18n.locale, @current_user_roles.include?('admin'), 'small_variant', root_object] 2 | 3 | attributes *variant_attributes 4 | 5 | node(:display_price) { |p| p.display_price.to_s } 6 | node(:options_text) { |v| v.options_text } 7 | node(:track_inventory) { |v| v.should_track_inventory? } 8 | node(:in_stock) { |v| v.in_stock? } 9 | node(:is_backorderable) { |v| v.is_backorderable? } 10 | node(:total_on_hand) { |v| v.total_on_hand } 11 | node(:is_destroyed) { |v| v.destroyed? } 12 | node(:subscribable) { |v| v.product.subscribable? } 13 | node(:frequencies) { |v| v.product.subscription_frequencies } 14 | 15 | child :option_values => :option_values do 16 | attributes *option_value_attributes 17 | end 18 | 19 | child(:images => :images) { extends "spree/api/images/show" } 20 | -------------------------------------------------------------------------------- /app/views/spree/admin/products/_subscribable.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= f.field_container :subscribable, class: ['form-group'] do %> 3 | <%= f.check_box :subscribable, class: 'form-control', class: "subscribable-option subscribable_checkbox" %> 4 | <%= f.label :subscribable, Spree.t(:subscribable), class: "subscribable_label" %> 5 | <%= f.error_message_on :subscribable %> 6 | <% end %> 7 |
8 |
> 9 | <%= f.collection_check_boxes :subscription_frequency_ids, Spree::SubscriptionFrequency.all, :id, :title, { include_hidden: false } do |builder| %> 10 | <%= builder.label { builder.check_box(class: "subscribable-option") + builder.text } %> 11 |
12 | <% end %> 13 |
14 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscription_frequencies/_form.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_actions do %> 2 | <%= button_link_to Spree.t(:back_to_subscription_frequencies_list), spree.admin_subscription_frequencies_path, class: 'btn-success', icon: 'chevron-left' %> 3 | <% end %> 4 | 5 | <%= render partial: 'spree/admin/shared/error_messages', locals: { target: @subscription_frequency } %> 6 | 7 | <%= f.field_container :title, class: ['form-group'] do %> 8 | <%= f.label :title %> 9 | <%= f.text_field :title, class: 'form-control' %> 10 | <%= f.error_message_on :title %> 11 | <% end %> 12 | <%= f.field_container :months_count, class: ['form-group'] do %> 13 | <%= f.label :months_count %> 14 | <%= number_field :subscription_frequency, :months_count, min: 1, value: @subscription_frequency.months_count || 1, class: 'form-control' %> 15 | <%= f.error_message_on :months_count %> 16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/controllers/spree/orders_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::OrdersController.class_eval do 2 | 3 | before_action :add_subscription_fields, only: :populate, if: -> { params[:subscribe].present? } 4 | before_action :restrict_guest_subscription, only: :update, unless: :spree_current_user 5 | 6 | private 7 | 8 | def restrict_guest_subscription 9 | redirect_to login_path, error: Spree.t(:required_authentication) if @order.subscriptions.present? 10 | end 11 | 12 | def add_subscription_fields 13 | is_subscribed = params.fetch(:subscribe, "").present? 14 | 15 | existing_options = {options: params.fetch(:options, {}).permit!} 16 | updated_subscription_params = params.fetch(:subscription, {}).merge({subscribe: is_subscribed}).permit! 17 | existing_options[:options].merge!(updated_subscription_params) 18 | updated_params = params.merge!(existing_options) 19 | updated_params 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /app/views/spree/subscription_notifier/notify_for_next_delivery.html.erb: -------------------------------------------------------------------------------- 1 |

Hi <%= @subscription.parent_order.email %>

2 |
3 |

4 | <%= Spree.t('subscription_notifier.notify_for_next_delivery.order_details') %> 5 |

6 | <% variant = @subscription.variant %> 7 | PRODUCT CODE: <%= variant.sku %>
8 | SUBSCRIBED PRODUCT: <%= raw( variant.product.name) %>
9 | SELECTION: <%= raw( variant.options_text) %>
10 | QUANTITY: <%= @subscription.quantity %>
11 | <%= Spree.t(:item_price) %>: <%= @subscription.price %>
12 |
13 | 14 |

15 | Your next order is going to be delivered in <%= @subscription.prior_notification_days_gap %> days. 16 | Your <%= @subscription.frequency.title.capitalize %>(every <%= @subscription.frequency.months_count.to_s %>) subscription has <%= @subscription.number_of_deliveries_left %> deliveries left. 17 |

18 |

Regards

19 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/frontend/cart_radio_button.js: -------------------------------------------------------------------------------- 1 | function CartRadioButton($radioButtons) { 2 | this.$radioButtons = $radioButtons; 3 | } 4 | 5 | CartRadioButton.prototype.init = function() { 6 | this.bindEvents(); 7 | }; 8 | 9 | CartRadioButton.prototype.bindEvents = function() { 10 | var _this = this; 11 | this.$radioButtons.on("change", function() { 12 | _this.toggleDiv($(this)); 13 | }); 14 | }; 15 | 16 | CartRadioButton.prototype.toggleDiv = function($checkBox) { 17 | $($checkBox.val()).show(); 18 | this.hideOtherDivs(); 19 | }; 20 | 21 | CartRadioButton.prototype.hideOtherDivs = function() { 22 | $.each(this.$radioButtons, function(index, value) { 23 | $checkBox = $(value); 24 | if (!$checkBox.prop("checked")) { 25 | $($checkBox.val()).hide(); 26 | } 27 | }); 28 | }; 29 | 30 | $(function() { 31 | var cartRadioButton = new CartRadioButton($(".cart_radio_button")); 32 | cartRadioButton.init(); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/models/spree/subscription_frequency_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Spree::SubscriptionFrequency, type: :model do 4 | 5 | describe "associations" do 6 | it { is_expected.to have_many(:product_subscription_frequencies).class_name("Spree::ProductSubscriptionFrequency").dependent(:destroy) } 7 | end 8 | 9 | describe "validations" do 10 | it { is_expected.to validate_presence_of(:title) } 11 | it { is_expected.to validate_presence_of(:months_count) } 12 | it { is_expected.to validate_numericality_of(:months_count).is_greater_than(0).only_integer } 13 | context "uniqueness of title" do 14 | let!(:subscription_frequency_1) { create(:monthly_subscription_frequency) } 15 | let(:subscription_frequency_2) { build(:monthly_subscription_frequency) } 16 | before { subscription_frequency_2.save } 17 | it { expect(subscription_frequency_2.errors[:title]).to include I18n.t "errors.messages.taken" } 18 | end 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 VinSol 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /app/mailers/spree/subscription_notifier.rb: -------------------------------------------------------------------------------- 1 | class Spree::SubscriptionNotifier < ApplicationMailer 2 | 3 | default from: "spree-commerce@example.com" 4 | 5 | def notify_confirmation(subscription) 6 | @subscription = subscription 7 | 8 | mail to: subscription.parent_order.email, 9 | subject: t('.subject', number: subscription.number, frequency: subscription.frequency.title.capitalize) 10 | end 11 | 12 | def notify_cancellation(subscription) 13 | @subscription = subscription 14 | 15 | mail to: subscription.parent_order.email, 16 | subject: t('.subject', number: subscription.number, frequency: subscription.frequency.title.capitalize) 17 | end 18 | 19 | def notify_reoccurrence(subscription) 20 | @subscription = subscription 21 | 22 | mail to: subscription.parent_order.email, 23 | subject: t('.subject', number: subscription.number, frequency: subscription.frequency.title.capitalize) 24 | end 25 | 26 | def notify_for_next_delivery(subscription) 27 | @subscription = subscription 28 | 29 | mail to: subscription.parent_order.email, 30 | subject: t('.subject', number: subscription.number, frequency: subscription.frequency.title.capitalize) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/backend/subscribable.js: -------------------------------------------------------------------------------- 1 | function Subscribable($parentCheckbox, $childCheckboxesDiv) { 2 | this.$parentCheckbox = $parentCheckbox; 3 | this.$childCheckboxesDiv = $childCheckboxesDiv; 4 | } 5 | 6 | Subscribable.prototype.init = function() { 7 | this.bindEvents(); 8 | }; 9 | 10 | Subscribable.prototype.bindEvents = function() { 11 | this.bindParentCheckboxEvent(); 12 | }; 13 | 14 | Subscribable.prototype.bindParentCheckboxEvent = function() { 15 | var _this = this; 16 | this.$parentCheckbox.on("change", function() { 17 | if ($(this).prop("checked")) { 18 | _this.enableChildCheckboxes(); 19 | } else { 20 | _this.disableChildCheckboxes(); 21 | } 22 | }); 23 | }; 24 | 25 | Subscribable.prototype.enableChildCheckboxes = function() { 26 | this.$childCheckboxesDiv.removeClass("hidden"); 27 | this.$childCheckboxesDiv.find("input").removeAttr("disabled"); 28 | }; 29 | 30 | Subscribable.prototype.disableChildCheckboxes = function() { 31 | this.$childCheckboxesDiv.addClass("hidden"); 32 | this.$childCheckboxesDiv.find("input").attr("disabled", "disabled"); 33 | } 34 | 35 | $(function() { 36 | var subscribable = new Subscribable($("#product_subscribable"), $("#subscribable_options")); 37 | subscribable.init(); 38 | }); 39 | -------------------------------------------------------------------------------- /app/views/spree/products/_subscription_fields.html.erb: -------------------------------------------------------------------------------- 1 | <% if @product.subscribable? %> 2 | 28 | <% end %> 29 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title do %> 2 | Spree Administration: <%= Spree::Subscription.model_name.human %> <%= @subscription.number %> 3 | <% end %> 4 | 5 | <% content_for :page_title do %> 6 | <%= Spree::Subscription.model_name.human %> <%= @subscription.number %> 7 | <% end %> 8 | 9 | <% content_for :page_actions do %> 10 | 11 | <% if !@subscription.not_changeable? %> 12 | 13 | <%= render partial: 'pause_links' %> 14 | 15 | <% end %> 16 | <%= button_link_to Spree.t(:back_to_subscriptions_list), spree.admin_subscriptions_path, class: 'btn-success', icon: 'chevron-left' %> 17 | 18 | <% end %> 19 | 20 | <%= render partial: 'spree/admin/shared/flash_messages' %> 21 | <%= render partial: 'spree/admin/shared/error_messages', locals: { target: @subscription } %> 22 | 23 | <%= form_for [:admin, @subscription] do |f| %> 24 | <%= render partial: 'summary', locals: { f: f } %> 25 | <%= render partial: 'item', locals: { f: f } %> 26 | <%= render partial: 'payment_info', locals: { f: f } %> 27 | <%= render partial: 'addresses', locals: { f: f } %> 28 | <%= render partial: 'orders' %> 29 | <% unless @subscription.not_changeable? %> 30 | <%= render partial: 'spree/admin/shared/edit_resource_links' %> 31 | <% end %> 32 | <% end %> 33 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscription_frequencies/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title do %> 2 | <%= plural_resource_name(Spree::SubscriptionFrequency) %> 3 | <% end %> 4 | 5 | <% content_for :page_actions do %> 6 | <%= button_link_to Spree.t(:new_subscription_frequency), new_object_url, icon: 'add', class: 'btn-success' %> 7 | <% end %> 8 | 9 | <% if @subscription_frequencies.present? %> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @subscription_frequencies.each do |subscription_frequency| %> 18 | 19 | 20 | 21 | 25 | 26 | <% end %> 27 | 28 |
<%= Spree::SubscriptionFrequency.human_attribute_name :title %><%= Spree::SubscriptionFrequency.human_attribute_name :months_count %>
<%= subscription_frequency.title.titleize %><%= subscription_frequency.months_count %> months 22 | <%= link_to_edit subscription_frequency, no_text: true %> 23 | <%= link_to_delete subscription_frequency, no_text: true %> 24 |
29 | <% else %> 30 |
31 | <%= Spree.t(:no_resource_found, resource: plural_resource_name(Spree::SubscriptionFrequency)) %>, 32 | <%= link_to Spree.t(:add_one), new_object_url %>! 33 |
34 | <% end %> 35 | -------------------------------------------------------------------------------- /spec/models/subscription_ability_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe SubscriptionAbility do 4 | 5 | describe "User" do 6 | describe "abilities" do 7 | let(:user) { create(:user) } 8 | let(:order) { create(:completed_order_with_totals) } 9 | let(:subscription) { create(:valid_subscription, enabled: true, parent_order: order, next_occurrence_at: Time.current) } 10 | 11 | subject(:ability) { SubscriptionAbility.new(user) } 12 | 13 | context 'when user is admin' do 14 | let(:user) { create(:admin_user) } 15 | 16 | it { is_expected.to be_able_to(:manage, subscription) } 17 | end 18 | 19 | context 'when subscription belongs to user' do 20 | let(:order) { create(:completed_order_with_totals, user_id: user.id) } 21 | 22 | it { is_expected.to be_able_to(:create, subscription) } 23 | it { is_expected.to be_able_to(:read, subscription) } 24 | it { is_expected.to be_able_to(:update, subscription) } 25 | end 26 | 27 | context 'when subscription does not belongs to user' do 28 | let(:second_user) { create(:user) } 29 | let(:order) { create(:completed_order_with_totals, user_id: second_user.id) } 30 | 31 | it { is_expected.to be_able_to(:create, subscription) } 32 | it { is_expected.not_to be_able_to(:read, subscription) } 33 | it { is_expected.not_to be_able_to(:update, subscription) } 34 | end 35 | 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/views/spree/subscriptions/edit.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
Subscription <%= @subscription.number %>
3 |
4 | <% if !@subscription.not_changeable? %> 5 | 6 | <%= render partial: 'pause_links', locals: { btn_class: "btn" } %> 7 | 8 | 9 | <%= link_to Spree.t("subscriptions.admin.cancel"), cancel_subscription_path(@subscription), method: :patch, class: 'btn-danger btn', data: { confirm: Spree.t("subscriptions.confirm.cancel") } %> 10 | 11 | <% end %> 12 |
13 |
14 | 15 | 16 |
17 | <%= render partial: 'spree/admin/shared/flash_messages' %> 18 | <%= render partial: 'spree/admin/shared/error_messages', locals: { target: @subscription } %> 19 | 20 | <%= form_for @subscription do |f| %> 21 | <%= render partial: 'summary', locals: { f: f } %> 22 | <%= render partial: 'item', locals: { f: f } %> 23 | <%= render partial: 'payment_info', locals: { f: f } %> 24 | <%= render partial: 'addresses', locals: { f: f } %> 25 | <%= render partial: 'orders' %> 26 | <% unless @subscription.not_changeable? %> 27 |
28 | <%= f.submit Spree.t(:update), class: "btn btn-success btn-lg" %> 29 | <%= link_to Spree.t(:cancel), account_path, class: "btn btn-default btn-lg" %> 30 |
31 | <% end %> 32 | <% end %> 33 |
34 | -------------------------------------------------------------------------------- /app/views/spree/users/_subscriptions.html.erb: -------------------------------------------------------------------------------- 1 | <%= render partial: 'spree/admin/shared/flash_messages' %> 2 | 3 |
4 | 5 |

<%= Spree.t(:my_subscriptions) %>

6 | <% if @subscriptions.present? %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <%= render @subscriptions %> 23 | 24 |
<%= Spree::Subscription.human_attribute_name(:number) %><%= Spree.t(:product_name) %><%= Spree.t(:price) %><%= Spree.t(:started_at) %><%= Spree.t(:recurring_delivery_interval) %><%= Spree.t(:next_occurrence_possible) %><%= Spree.t(:total_deliveries) %><%= Spree.t(:deliveries_left) %><%= Spree.t(:action_links) %>
25 | <% else %> 26 |
<%= Spree.t(:you_have_no_subscriptions_yet) %>
27 | <% end %> 28 |
29 | 30 |
31 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/cancellation.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title do %> 2 | Spree Administration: Cancel <%= Spree::Subscription.model_name.human %> <%= @subscription.number %> 3 | <% end %> 4 | 5 | <% content_for :page_title do %> 6 | Cancel <%= Spree::Subscription.model_name.human %> <%= @subscription.number %> 7 | <% end %> 8 | 9 | <% content_for :page_actions do %> 10 | <%= button_link_to Spree.t(:back_to_subscriptions_list), spree.admin_subscriptions_path, class: 'btn-success', icon: 'chevron-left' %> 11 | <% end %> 12 | 13 | <%= render :partial => 'spree/admin/shared/error_messages', :locals => { :target => @subscription } %> 14 | 15 | <%= form_for @subscription, url: cancel_admin_subscription_path(@subscription) do |f| %> 16 |
17 | <%= f.field_container :cancellation_reasons, class: ["form-group"] do %> 18 | <%= f.label :cancellation_reasons, Spree.t(:cancellation_reasons) %>* 19 | <%= f.text_area :cancellation_reasons, class: "form-control" %> 20 | <%= f.error_message_on :cancellation_reasons %> 21 | <% end %> 22 |
23 |
24 |
25 | <%= button Spree.t(:submit), 'ok', 'submit', { class: 'btn-success', data: { confirm: t(".confirm") } } %> 26 | <%= Spree.t(:or) %> 27 | <%= button_link_to Spree.t('actions.cancel'), collection_url, :icon => 'remove' %> 28 |
29 | <% end %> 30 | -------------------------------------------------------------------------------- /lib/generators/spree_product_subscriptions/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | module SpreeProductSubscriptions 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | 5 | class_option :auto_run_migrations, :type => :boolean, :default => false 6 | 7 | def add_javascripts 8 | append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/spree_product_subscriptions\n" 9 | append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/spree_product_subscriptions\n" 10 | end 11 | 12 | def add_stylesheets 13 | inject_into_file 'vendor/assets/stylesheets/spree/frontend/all.css', " *= require spree/frontend/spree_product_subscriptions\n", :before => /\*\//, :verbose => true 14 | inject_into_file 'vendor/assets/stylesheets/spree/backend/all.css', " *= require spree/backend/spree_product_subscriptions\n", :before => /\*\//, :verbose => true 15 | end 16 | 17 | def add_migrations 18 | run 'bundle exec rake railties:install:migrations FROM=spree_product_subscriptions' 19 | end 20 | 21 | def run_migrations 22 | run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]') 23 | if run_migrations 24 | run 'bundle exec rake db:migrate' 25 | else 26 | puts 'Skipping rake db:migrate, don\'t forget to run it!' 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/_addresses.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

<%= Spree.t(:billing_address) %>

7 |
8 | 9 |
10 | <% if @subscription.not_changeable? %> 11 | <%= render partial: 'spree/admin/shared/address', locals: { address: @subscription.bill_address } %> 12 | <% else %> 13 | <%= f.fields_for :bill_address do |ba_form| %> 14 | <%= render partial: 'spree/admin/shared/address_form', locals: { f: ba_form, type: "billing" } %> 15 | <% end %> 16 | <% end %> 17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 |

<%= Spree.t(:shipping_address) %>

25 |
26 |
27 | <% if @subscription.not_changeable? %> 28 | <%= render partial: 'spree/admin/shared/address', locals: { address: @subscription.ship_address } %> 29 | <% else %> 30 | <%= f.fields_for :ship_address do |sa_form| %> 31 | <%= render partial: 'spree/admin/shared/address_form', locals: { f: sa_form, type: 'shipping' } %> 32 | <% end %> 33 | <% end %> 34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /spec/models/spree/product_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::Product, type: :model do 4 | 5 | describe "associations" do 6 | it { is_expected.to have_many(:subscriptions).through(:variants_including_master).source(:subscriptions).dependent(:restrict_with_error) } 7 | it { is_expected.to have_many(:product_subscription_frequencies).class_name("Spree::ProductSubscriptionFrequency").dependent(:destroy) } 8 | it { is_expected.to have_many(:subscription_frequencies).through(:product_subscription_frequencies).dependent(:destroy) } 9 | end 10 | 11 | describe "validations" do 12 | context "if subscribable" do 13 | before { subject.subscribable = true } 14 | it { is_expected.to validate_presence_of(:subscription_frequencies) } 15 | end 16 | end 17 | 18 | describe "scopes" do 19 | context ".subscribable" do 20 | let(:subscription_frequencies) { [create(:monthly_subscription_frequency)] } 21 | let(:subscribable_product) { create(:product, subscribable: true, subscription_frequencies: subscription_frequencies) } 22 | let(:unsubscribable_product) { create(:product) } 23 | it { expect(Spree::Product.subscribable).to include subscribable_product } 24 | it { expect(Spree::Product.subscribable).to_not include unsubscribable_product } 25 | end 26 | end 27 | 28 | describe "ransackable" do 29 | context "whitelisted_ransackable_attributes" do 30 | it { expect(Spree::Product.whitelisted_ransackable_attributes).to include "is_subscribable" } 31 | end 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /db/migrate/20160301200702_change_names_of_tables_and_columns.rb: -------------------------------------------------------------------------------- 1 | class ChangeNamesOfTablesAndColumns < ActiveRecord::Migration[4.2] 2 | def up 3 | change_column_default :spree_subscriptions, :quantity, 0 4 | change_column_null :spree_subscriptions, :quantity, false 5 | change_column_null :spree_subscription_frequencies, :title, false 6 | change_column :spree_subscriptions, :last_recurrence_at, :datetime 7 | change_column :spree_subscriptions, :end_date, :datetime 8 | change_column :spree_subscriptions, :cancelled_at, :datetime 9 | change_column :spree_order_subscriptions, :failed_at, :datetime 10 | rename_table :spree_order_subscriptions, :spree_orders_subscriptions 11 | rename_column :spree_products, :subscribable, :is_subscribable 12 | rename_column :spree_subscriptions, :last_recurrence_at, :last_occurrence_at 13 | end 14 | 15 | def down 16 | change_column_default :spree_subscriptions, :quantity, nil 17 | change_column_null :spree_subscriptions, :quantity, true 18 | change_column_null :spree_subscription_frequencies, :string, true 19 | change_column :spree_subscriptions, :last_occurrence_at, :date 20 | change_column :spree_subscriptions, :end_date, :date 21 | change_column :spree_subscriptions, :cancelled_at, :date 22 | change_column :spree_orders_subscriptions, :failed_at, :date 23 | rename_table :spree_orders_subscriptions, :spree_order_subscriptions 24 | rename_column :spree_subscriptions, :last_occurrence_at, :last_recurrence_at 25 | rename_column :spree_products, :is_subscribable, :subscribable 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/views/spree/subscriptions/_addresses.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

<%= Spree.t(:billing_address) %>

7 |
8 | 9 |
10 | <% if @subscription.not_changeable? %> 11 | <%= render partial: 'spree/admin/shared/address', locals: { address: @subscription.bill_address } %> 12 | <% else %> 13 | <%= f.fields_for :bill_address do |ba_form| %> 14 | <%= render partial: 'spree/address/form', locals: { form: ba_form, address_type: 'billing', address: @subscription.bill_address } %> 15 | <% end %> 16 | <% end %> 17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 |

<%= Spree.t(:shipping_address) %>

25 |
26 |
27 | <% if @subscription.not_changeable? %> 28 | <%= render partial: 'spree/admin/shared/address', locals: { address: @subscription.ship_address } %> 29 | <% else %> 30 | <%= f.fields_for :ship_address do |sa_form| %> 31 | <%= render partial: 'spree/address/form', locals: { form: sa_form, address_type: 'shipping', address: @subscription.ship_address } %> 32 | <% end %> 33 | <% end %> 34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /spree_product_subscriptions.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | Gem::Specification.new do |s| 3 | s.platform = Gem::Platform::RUBY 4 | s.name = 'spree_product_subscriptions' 5 | s.version = '3.2.0' 6 | s.summary = 'Add gem summary here' 7 | s.description = 'Add (optional) gem description here' 8 | s.required_ruby_version = '>= 2.1.0' 9 | 10 | s.author = 'Vinay' 11 | s.email = 'vinay@vinsol.com' 12 | # s.homepage = 'http://www.spreecommerce.com' 13 | 14 | #s.files = `git ls-files`.split("\n") 15 | #s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 16 | s.require_path = 'lib' 17 | s.requirements << 'none' 18 | 19 | s.add_dependency 'spree_core', '>= 3.2.0' , '< 4.0' 20 | 21 | s.add_development_dependency 'capybara', '~> 2.4' 22 | s.add_development_dependency 'coffee-rails', '~> 4.2' 23 | s.add_development_dependency 'database_cleaner' 24 | s.add_development_dependency 'factory_bot' 25 | s.add_development_dependency 'ffaker' 26 | s.add_development_dependency 'pry-rails', '~> 0.3.4' 27 | s.add_development_dependency 'rspec-rails', '~> 3.4' 28 | s.add_development_dependency 'rspec-activemodel-mocks', '~> 1.0.3' 29 | s.add_development_dependency 'sass-rails', '~> 5.0.1' 30 | s.add_development_dependency 'selenium-webdriver', '~> 2.52.0' 31 | s.add_development_dependency 'shoulda-matchers', '~> 3.1.1' 32 | s.add_development_dependency 'shoulda-callback-matchers', '~> 1.1.3' 33 | s.add_development_dependency 'simplecov', '~> 0.11.2' 34 | s.add_development_dependency 'sqlite3', '~> 1.3.11' 35 | s.add_development_dependency 'appraisal' 36 | end 37 | -------------------------------------------------------------------------------- /app/models/spree/order_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Order.class_eval do 2 | 3 | has_one :order_subscription, class_name: "Spree::OrderSubscription", dependent: :destroy 4 | has_one :parent_subscription, through: :order_subscription, source: :subscription 5 | has_many :subscriptions, class_name: "Spree::Subscription", 6 | foreign_key: :parent_order_id, 7 | dependent: :restrict_with_error 8 | 9 | self.state_machine.after_transition to: :complete, do: :enable_subscriptions, if: :any_disabled_subscription? 10 | 11 | after_update :update_subscriptions 12 | 13 | def available_payment_methods 14 | if subscriptions.exists? 15 | @available_payment_methods = Spree::Gateway.active.available_on_front_end 16 | else 17 | @available_payment_methods ||= Spree::PaymentMethod.active.available_on_front_end 18 | end 19 | end 20 | 21 | private 22 | 23 | def enable_subscriptions 24 | subscriptions.each do |subscription| 25 | subscription.update( 26 | source: payments.from_credit_card.first.source, 27 | enabled: true, 28 | ship_address: ship_address.clone, 29 | bill_address: bill_address.clone 30 | ) 31 | end 32 | end 33 | 34 | def any_disabled_subscription? 35 | subscriptions.disabled.any? 36 | end 37 | 38 | def update_subscriptions 39 | line_items.each do |line_item| 40 | if line_item.subscription_attributes_present? 41 | subscriptions.find_by(variant: line_item.variant).update(line_item.updatable_subscription_attributes) 42 | end 43 | end 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /app/views/spree/subscriptions/_orders.html.erb: -------------------------------------------------------------------------------- 1 |

Orders

2 |
3 |
4 | <% if @subscription.orders.present? %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @subscription.orders.each do |order| %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | <% end %> 27 | 28 |
<%= Spree::Order.human_attribute_name(:number) %><%= Spree.t(:date) %><%= Spree.t(:status) %><%= Spree.t(:payment_state) %><%= Spree.t(:shipment_state) %><%= Spree.t(:total) %>
<%= link_to order.number, order_url(order) %><%= order.completed_at ? l(order.completed_at.to_date) : '-' %><%= Spree.t("order_state.#{order.state}").titleize %><%= Spree.t("payment_states.#{order.payment_state}").titleize if order.payment_state %><%= Spree.t("shipment_states.#{order.shipment_state}").titleize if order.shipment_state %><%= order.display_total %>
29 | <% else %> 30 |
31 | <%= Spree.t(:no_sub_orders_found) %> 32 |
33 | <% end %> 34 |
35 |
36 | -------------------------------------------------------------------------------- /app/views/spree/subscriptions/_subscription.html.erb: -------------------------------------------------------------------------------- 1 | > 2 | <%= link_to subscription.number, edit_subscription_path(subscription) %> 3 | <%= subscription.variant.product.name %> 4 | <%= subscription.price * subscription.quantity %> 5 | <%= subscription.parent_order.completed_at.to_date.to_formatted_s(:rfc822) %> 6 | <%= subscription.frequency.title.titleize %> 7 | <%= subscription.next_occurrence_possible %> 8 | <%= subscription.delivery_number %> 9 | <%= subscription.number_of_deliveries_left %> 10 | 11 | <% @subscription = subscription %> 12 | <% if subscription.cancelled? %> 13 | Subscription Cancelled 14 | <% elsif !subscription.deliveries_remaining? %> 15 | Completed 16 | <% else %> 17 | <%= render partial: 'spree/subscriptions/pause_links', locals: { btn_class: "btn-xs" } %> 18 | <%= link_to Spree.t(:edit), edit_subscription_path(subscription), class: "btn btn-xs btn-default" %> 19 | <%= link_to Spree.t("subsriptions.admin.cancel"), "javascript:void(0)", class: "btn ajax_handler btn-xs btn-danger", data: { method: "PATCH", confirmation: Spree.t("subscriptions.confirm.cancel"), url: cancel_subscription_path(subscription) } %> 20 | <% end %> 21 | 22 | 23 | -------------------------------------------------------------------------------- /spec/mailers/spree/subscription_notifier_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Spree::SubscriptionNotifier do 4 | 5 | let(:active_subscription) { mock_model(Spree::Subscription, id: 1, enabled: true, next_occurrence_at: Time.current) } 6 | let(:order) { create(:completed_order_with_totals) } 7 | let(:subscription_frequency) { mock_model(Spree::SubscriptionFrequency, id: 1, title: 'monthly', months_count: 1) } 8 | let(:variant) { mock_model(Spree::Variant) } 9 | let(:product) { mock_model(Spree::Product) } 10 | 11 | describe 'notify_for_next_delivery' do 12 | 13 | let(:mail) { described_class.notify_for_next_delivery(active_subscription) } 14 | before do 15 | allow(active_subscription).to receive(:parent_order).and_return(order) 16 | allow(active_subscription).to receive(:frequency).and_return(subscription_frequency) 17 | allow(active_subscription).to receive(:variant).and_return(variant) 18 | allow(active_subscription).to receive(:number_of_deliveries_left).and_return(1) 19 | allow(variant).to receive(:product).and_return(product) 20 | allow(variant).to receive(:options_text).and_return('') 21 | end 22 | 23 | it 'renders the subject' do 24 | expect(mail.subject).to eq(I18n.t(:subject, scope: [:spree, :subscription_notifier, :notify_for_next_delivery], 25 | number: active_subscription.number, frequency: active_subscription.frequency.title.capitalize)) 26 | end 27 | 28 | it 'renders the receiver email' do 29 | expect(mail.to).to eq([active_subscription.parent_order.email]) 30 | end 31 | 32 | it 'renders the sender email' do 33 | expect(mail.from).to eq([Spree::SubscriptionNotifier.default[:from]]) 34 | end 35 | 36 | it 'assigns @subscription' do 37 | expect(mail.body.encoded).to match(active_subscription.parent_order.email) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/_subscription.html.erb: -------------------------------------------------------------------------------- 1 | 2 | <%= link_to subscription.number, edit_admin_subscription_path(subscription) %> 3 | <%= subscription.variant.name %> 4 | <%= subscription.price * subscription.quantity %> 5 | <%= subscription.frequency.title.titleize %> 6 | <%= subscription.delivery_number %> 7 | <%= subscription.number_of_deliveries_left %> 8 | <%= subscription.next_occurrence_possible %> 9 | 10 | <% @subscription = subscription %> 11 | <% if !subscription.deliveries_remaining? %> 12 | Completed 13 | <% elsif subscription.cancelled? %> 14 | Cancelled 15 | <% else %> 16 | 17 | <% if !@subscription.paused? %> 18 | <%= button_link_to "", "javascript:void(0)", class: 'ajax_handler btn-warning btn-sm', icon: 'pause', data: { method: "PATCH", url: pause_admin_subscription_path(@subscription), confirmation: Spree.t("subscriptions.confirm.pause") }, title: "Pause" %> 19 | <% else %> 20 | <%= button_link_to "", "javascript:void(0)", class: 'btn-success ajax_handler btn-sm', icon: 'play', data: { method: "PATCH", url: unpause_admin_subscription_path(@subscription), confirmation: Spree.t("subscriptions.confirm.activate") }, title: "Activate" %> 21 | <% end %> 22 | 23 | <%= link_to_edit(subscription, no_text: true) if can? :edit, subscription %> 24 | 25 | <%= button_link_to "", cancellation_admin_subscription_path(@subscription), class: 'btn-danger btn-sm', icon: 'cancel', title: "Cancel" %> 26 | 27 | <% end %> 28 | 29 | 30 | -------------------------------------------------------------------------------- /spec/controllers/spree/users_controller_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::UsersController, type: :controller do 4 | 5 | stub_authorization! 6 | 7 | describe "Callbacks" do 8 | describe "#load_subscriptions" do 9 | def do_show 10 | spree_get :show 11 | end 12 | 13 | let(:orders) { double(ActiveRecord::Relation) } 14 | let(:subscriptions) { double(ActiveRecord::Relation) } 15 | let(:user) { mock_model(Spree.user_class) } 16 | 17 | before do 18 | allow(controller).to receive(:spree_current_user).and_return(user) 19 | allow(user).to receive(:orders).and_return(orders) 20 | allow(orders).to receive(:complete).and_return(orders) 21 | allow(orders).to receive(:order).and_return(orders) 22 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 23 | allow(subscriptions).to receive(:order).and_return(subscriptions) 24 | allow(subscriptions).to receive(:with_parent_orders).and_return(subscriptions) 25 | end 26 | 27 | context "expects to receive" do 28 | after { do_show } 29 | it { expect(controller).to receive(:spree_current_user).and_return(user) } 30 | it { expect(user).to receive(:orders).and_return(orders) } 31 | it { expect(orders).to receive(:complete).and_return(orders) } 32 | it { expect(orders).to receive(:order).with(completed_at: :desc).and_return(orders) } 33 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 34 | it { expect(subscriptions).to receive(:order).with(created_at: :desc).and_return(subscriptions) } 35 | it { expect(subscriptions).to receive(:with_parent_orders).with(orders).and_return(subscriptions) } 36 | end 37 | 38 | context "assigns" do 39 | before { do_show } 40 | it { expect(assigns(:orders)).to eq orders } 41 | it { expect(assigns(:subscriptions)).to eq subscriptions } 42 | end 43 | end 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /app/models/spree/line_item_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::LineItem.class_eval do 2 | 3 | attr_accessor :subscription_frequency_id, :delivery_number, :subscribe 4 | 5 | after_create :create_subscription!, if: :subscribable? 6 | after_update :update_subscription_quantity, if: :can_update_subscription_quantity? 7 | after_update :update_subscription_attributes, if: :can_update_subscription_attributes? 8 | after_destroy :destroy_associated_subscription!, if: :subscription? 9 | 10 | def subscription_attributes_present? 11 | subscription_frequency_id.present? || delivery_number.present? 12 | end 13 | 14 | def updatable_subscription_attributes 15 | { 16 | subscription_frequency_id: subscription_frequency_id || subscription.subscription_frequency_id, 17 | delivery_number: delivery_number || subscription.delivery_number 18 | } 19 | end 20 | 21 | def subscribable? 22 | subscribe.present? && subscribe != "0" 23 | end 24 | 25 | def subscription? 26 | !!subscription 27 | end 28 | 29 | def subscription 30 | order.subscriptions.find_by(variant: variant) 31 | end 32 | 33 | private 34 | 35 | def create_subscription! 36 | order.subscriptions.create! subscription_attributes 37 | end 38 | 39 | def subscription_attributes 40 | { 41 | subscription_frequency_id: subscription_frequency_id, 42 | price: variant.price, 43 | delivery_number: delivery_number, 44 | variant: variant, 45 | quantity: quantity 46 | } 47 | end 48 | 49 | def update_subscription_quantity 50 | subscription.update(quantity: quantity) 51 | end 52 | 53 | def update_subscription_attributes 54 | subscription.update(updatable_subscription_attributes) 55 | end 56 | 57 | def destroy_associated_subscription! 58 | subscription.destroy! 59 | end 60 | 61 | def can_update_subscription_attributes? 62 | subscription? && subscription_attributes_present? 63 | end 64 | 65 | def can_update_subscription_quantity? 66 | subscription? && quantity_changed? 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/controllers/spree/orders_controller_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::OrdersController, type: :controller do 4 | 5 | stub_authorization! 6 | 7 | describe "Callbacks" do 8 | describe "#add_subscription_fields" do 9 | def do_populate params 10 | spree_post :populate, params 11 | end 12 | 13 | let(:variant) { create(:variant) } 14 | let (:with_subscribe_params) { { subscribe: true, variant_id: variant.id, subscription: { subscription_frequency_id: 1, delivery_number: 6 } }.with_indifferent_access } 15 | 16 | context "send populate request with params[:subscribe] present" do 17 | before { do_populate with_subscribe_params } 18 | it { expect(controller.send :add_subscription_fields).to_not be_nil } 19 | end 20 | end 21 | end 22 | 23 | describe "Callbacks" do 24 | describe "#restrict_guest_subscription" do 25 | def do_update params 26 | spree_put :update, params 27 | end 28 | 29 | let (:order) { mock_model(Spree::Order) } 30 | let (:order_subscriptions) { double(Spree::OrderSubscription) } 31 | let (:with_order_params) { { "order" => { "line_items_attributes" => { "0" => { "quantity"=>"1", "subscription_frequency_id"=>"3", "id"=>"9" } } }.with_indifferent_access } } 32 | 33 | before do 34 | allow(controller).to receive(:current_order).and_return(order) 35 | allow(order).to receive(:subscriptions).and_return(order_subscriptions) 36 | end 37 | 38 | context "send update request with params[:order] present" do 39 | it { expect(controller).to receive(:current_order).and_return(order_subscriptions) } 40 | it { expect(order).to receive(:subscriptions).and_return(order) } 41 | it { expect(order_subscriptions).to receive(:present?).and_return(true) } 42 | 43 | after { do_update with_order_params } 44 | end 45 | 46 | context 'response' do 47 | before { do_update with_order_params } 48 | 49 | it { is_expected.to respond_with 302 } 50 | it { expect(response).to redirect_to(login_path) } 51 | end 52 | end 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title do %> 2 | Spree Administration: Subscriptions 3 | <% end %> 4 | 5 | <% content_for :page_title do %> 6 | Subscriptions 7 | <% end %> 8 | 9 | <%= render partial: 'spree/admin/shared/flash_messages' %> 10 | 11 | <% content_for :table_filter do %> 12 |
13 | <%= search_form_for [:admin, @search] do |f| %> 14 |
15 |
16 |
17 | <%= f.label :parent_order_email_cont, Spree.t(:customer_email) %> 18 | <%= f.text_field :parent_order_email_cont, tabindex: 1, class: "form-control js-quick-search-target" %> 19 |
20 |
21 |
22 |
23 | <%= button Spree.t(:filter_results), 'search' %> 24 |
25 | <% end %> 26 |
27 | <% end %> 28 | 29 | <% if @subscriptions.present? %> 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | <%= render partial: 'spree/admin/subscriptions/subscription', collection: @subscriptions %> 45 | 46 |
<%= Spree.t(:number) %><%= Spree.t(:product_name) %><%= Spree.t(:price) %><%= Spree.t(:recurring_delivery_interval) %><%= Spree.t(:total_deliveries) %><%= Spree.t(:deliveries_left) %><%= Spree.t(:next_occurrence_possible) %><%= Spree.t(:action_links)%>
47 | <%= paginate @subscriptions %> 48 | <% else %> 49 |
50 | <%= Spree.t(:no_resource_found, resource: plural_resource_name(Spree::Subscription)) %> 51 |
52 | <% end %> 53 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | module Admin 3 | class SubscriptionsController < ResourceController 4 | 5 | before_action :ensure_not_cancelled, only: [:update, :cancel, :cancellation, :pause, :unpause] 6 | 7 | def cancellation 8 | end 9 | 10 | def cancel 11 | if @subscription.cancel_with_reason(cancel_subscription_attributes) 12 | redirect_to collection_url, success: t('.success') 13 | else 14 | render :cancellation 15 | end 16 | end 17 | 18 | def pause 19 | if @subscription.pause 20 | render json: { 21 | flash: t('.success'), 22 | url: unpause_subscription_path(@subscription), 23 | button_text: Spree::Subscription::ACTION_REPRESENTATIONS[:unpause], 24 | confirmation: Spree.t("subscriptions.confirm.activate") 25 | }, status: 200 26 | else 27 | render json: { 28 | flash: t('.error') 29 | }, status: 422 30 | end 31 | end 32 | 33 | def unpause 34 | if @subscription.unpause 35 | render json: { 36 | flash: t('.success', next_occurrence_at: @subscription.next_occurrence_at.to_date.to_formatted_s(:rfc822)), 37 | url: pause_subscription_path(@subscription), 38 | button_text: Spree::Subscription::ACTION_REPRESENTATIONS[:pause], 39 | next_occurrence_at: @subscription.next_occurrence_at.to_date, 40 | confirmation: Spree.t("subscriptions.confirm.pause") 41 | }, status: 200 42 | else 43 | render json: { 44 | flash: t('.error') 45 | }, status: 422 46 | end 47 | end 48 | 49 | private 50 | 51 | def cancel_subscription_attributes 52 | params.require(:subscription).permit(:cancellation_reasons) 53 | end 54 | 55 | def collection 56 | @search = super.active.ransack(params[:q]) 57 | @collection = @search.result.includes(:frequency, :complete_orders, variant: :product) 58 | .references(:complete_orders) 59 | .order(created_at: :desc) 60 | .page(params[:page]) 61 | end 62 | 63 | def ensure_not_cancelled 64 | if @subscription.cancelled? 65 | redirect_to collection_url, error: Spree.t("admin.subscriptions.error_on_already_cancelled") 66 | end 67 | end 68 | 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /app/views/spree/subscriptions/_item.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | <%= Spree.t(:subscription_item) %> 6 |

7 |
8 | 9 | 10 | 11 | 14 | 17 | 20 | 21 | 22 | 23 | 26 | 29 | 34 | 42 | 50 | 51 | 52 |
<%= Spree.t(:name) %> 12 | <%= f.label :price, Spree.t(:price) %>* 13 | 15 | <%= f.label :quantity, Spree.t(:quantity) %>* 16 | 18 | <%= f.label :frequency, Spree.t(:recurring_delivery_interval) %>* 19 |
24 | <%= mini_image(@subscription.variant) %> 25 | 27 | <%= collection_select(:subscription, :variant_id, @subscription.variant.product_variants, :id, :name, {}, data: { subscription_id: @subscription.id }) %> 28 | 30 |
31 | <%= @subscription.price %> 32 |
33 |
35 |
36 | <%= f.field_container :quantity, class: ["form-group"] do %> 37 | <%= number_field :subscription, :quantity, { min: 1, disabled: @subscription.not_changeable? } %> 38 | <%= f.error_message_on :quantity %> 39 | <% end %> 40 |
41 |
43 |
44 | <%= f.field_container :frequency, class: ["form-group"] do %> 45 | <%= f.collection_select :subscription_frequency_id, @subscription.variant.product.subscription_frequencies, :id, :title, {}, { class: "form-control", disabled: @subscription.not_changeable? } %> 46 | <%= f.error_message_on :frequency %> 47 | <% end %> 48 |
49 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start do 3 | add_filter 'spec' 4 | add_group 'Controllers', 'app/controllers' 5 | add_group 'Models', 'app/models' 6 | add_group 'Overrides', 'app/overrides' 7 | add_group 'Libraries', 'lib' 8 | end 9 | 10 | ENV['RAILS_ENV'] ||= 'test' 11 | 12 | begin 13 | require File.expand_path('../dummy/config/environment', __FILE__) 14 | rescue LoadError 15 | puts 'Could not load dummy application. Please ensure you have run `bundle exec rake test_app`' 16 | exit 17 | end 18 | 19 | require 'rspec/rails' 20 | require 'database_cleaner' 21 | require 'factory_bot' 22 | require 'ffaker' 23 | require 'shoulda-matchers' 24 | require 'shoulda-callback-matchers' 25 | require 'pry' 26 | require "spree/testing_support/factories" 27 | require 'spree/testing_support/url_helpers' 28 | require "spree/testing_support/authorization_helpers" 29 | require "spree/testing_support/controller_requests" 30 | require 'spree/testing_support/preferences' 31 | require 'rspec/active_model/mocks' 32 | require 'spree_product_subscriptions/factories' 33 | require "cancan/matchers" 34 | 35 | Shoulda::Matchers.configure do |config| 36 | config.integrate do |with| 37 | with.test_framework :rspec 38 | with.library :rails 39 | end 40 | end 41 | 42 | RSpec.configure do |config| 43 | config.mock_with :rspec 44 | config.use_transactional_fixtures = false 45 | config.fail_fast = false 46 | config.filter_run focus: true 47 | config.run_all_when_everything_filtered = true 48 | config.include FactoryBot::Syntax::Methods 49 | config.infer_spec_type_from_file_location! 50 | config.raise_errors_for_deprecations! 51 | config.expect_with :rspec do |expectations| 52 | expectations.syntax = :expect 53 | end 54 | 55 | config.before :suite do 56 | DatabaseCleaner.strategy = :transaction 57 | DatabaseCleaner.clean_with :truncation 58 | end 59 | 60 | # Before each spec check if it is a Javascript test and switch between using database transactions or not where necessary. 61 | config.before :each do 62 | DatabaseCleaner.strategy = RSpec.current_example.metadata[:js] ? :truncation : :transaction 63 | DatabaseCleaner.start 64 | end 65 | 66 | # After each spec clean the database. 67 | config.after :each do 68 | DatabaseCleaner.clean 69 | end 70 | config.include Spree::TestingSupport::ControllerRequests, type: :controller 71 | config.include Spree::TestingSupport::UrlHelpers 72 | config.include Devise::Test::ControllerHelpers, type: :controller 73 | config.include(Shoulda::Callback::Matchers::ActiveModel) 74 | 75 | end 76 | 77 | 78 | ActiveJob::Base.queue_adapter = :test 79 | 80 | 81 | 82 | Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |file| require file } 83 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/_summary.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<%= Spree.t(:subscription_summary) %>

4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 21 | 22 | 23 | 26 | 32 | 33 | 34 | 37 | 45 | 46 | 47 | 48 | 49 | 50 | <% if @subscription.cancelled? %> 51 | 52 | 55 | 58 | 59 | 60 | 63 | 66 | 67 | <% end %> 68 | 69 |
<%= Spree.t(:customer_email) %><%= @subscription.parent_order.email %>
14 | <%= Spree.t(:original_order) %>: 15 | 17 | 18 | <%= link_to @subscription.parent_order.number, edit_admin_order_path(@subscription.parent_order) %> 19 | 20 |
24 | <%= Spree.t(:next_occurrence_at) %>: 25 | 27 | <%= f.field_container :next_occurrence_at, class: ["form-group"] do %> 28 | <%= f.text_field :next_occurrence_at, class: "form-control datepicker subscription_next_occurrence_at", disabled: @subscription.not_changeable? || @subscription.paused? %> 29 | <%= f.error_message_on :next_occurrence_at %> 30 | <% end %> 31 |
35 | <%= Spree.t(:total_deliveries) %> 36 | 38 |
39 | <%= f.field_container :delivery_number, class: ["form-group"] do %> 40 | <%= number_field :subscription, :delivery_number, class: "form-control", disabled: @subscription.not_changeable? %> 41 | <%= f.error_message_on :delivery_number %> 42 | <% end %> 43 |
44 |
<%= Spree.t(:deliveries_left) %><%= @subscription.number_of_deliveries_left %>
53 | <%= Spree.t(:cancelled_at) %> 54 | 56 | <%= @subscription.cancelled_at.to_formatted_s(:long) %> 57 |
61 | <%= Spree.t(:cancellation_reasons) %> 62 | 64 | <%= @subscription.cancellation_reasons.html_safe %> 65 |
70 |
71 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/_item.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | <%= Spree.t(:subscription_item) %> 6 |

7 |
8 | 9 | 10 | 11 | 14 | 17 | 20 | 21 | 22 | 23 | 26 | 29 | 37 | 45 | 53 | 54 | 55 |
<%= Spree.t(:name) %> 12 | <%= f.label :price, Spree.t(:price) %>* 13 | 15 | <%= f.label :quantity, Spree.t(:quantity) %>* 16 | 18 | <%= f.label :frequency, Spree.t(:recurring_delivery_interval) %>* 19 |
24 | <%= mini_image(@subscription.variant) %> 25 | 27 | <%= collection_select(:subscription, :variant_id, @subscription.variant.product_variants, :id, :name, {}, data: { subscription_id: @subscription.id }) %> 28 | 30 |
31 | <%= f.field_container :price, class: ["form-group"] do %> 32 | <%= f.text_field :price, class: "form-control", disabled: @subscription.not_changeable? %> 33 | <%= f.error_message_on :price %> 34 | <% end %> 35 |
36 |
38 |
39 | <%= f.field_container :quantity, class: ["form-group"] do %> 40 | <%= number_field :subscription, :quantity, { min: 1, disabled: @subscription.not_changeable? } %> 41 | <%= f.error_message_on :quantity %> 42 | <% end %> 43 |
44 |
46 |
47 | <%= f.field_container :frequency, class: ["form-group"] do %> 48 | <%= f.collection_select :subscription_frequency_id, @subscription.variant.product.subscription_frequencies, :id, :title, {}, { class: "form-control", disabled: @subscription.not_changeable? } %> 49 | <%= f.error_message_on :frequency %> 50 | <% end %> 51 |
52 |
56 |
57 |
58 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | 7 | spree: 8 | subscription_notifier: 9 | notify_confirmation: 10 | subject: "%{frequency} %{number} Subscription Confirmation" 11 | notify_cancellation: 12 | subject: "%{frequency} %{number} Subscription Cancelled" 13 | notify_reoccurrence: 14 | subject: "%{frequency} %{number} Subscription Reoccurrence" 15 | notify_for_next_delivery: 16 | subject: "%{frequency} %{number} Subscription Notice" 17 | order_details: 'Order Details' 18 | 19 | subscriptions: 20 | pause: 21 | success: "Subscription is paused" 22 | error: "There was some failure in pausing the subscription" 23 | unpause: 24 | success: "Subscription is active. Your next occurrence will be on %{next_occurrence_at}" 25 | error: "There was some faiure in activating the subscription" 26 | cancel: 27 | success: "Subscription was successfully cancelled" 28 | error: "There was some problem in cancelling the subscription" 29 | update: 30 | success: "Subscription was successfully updated" 31 | alert: 32 | missing: "Subscription does not exist." 33 | error: 34 | out_of_range: "Next Occurrence cannot be before today's date" 35 | not_changeable: "Subscription is either cancelled or all the deliveries are done." 36 | should_be_earlier_than_next_delivery: 'should be earlier than next delivery' 37 | confirm: 38 | pause: "Are you sure you want to pause this subscription" 39 | activate: "Are you sure you want to activate this subscription" 40 | cancel: "Are you sure to cancel this subscription. One cancelled it cannot be started again." 41 | 42 | admin: 43 | subscriptions: 44 | pause: 45 | success: "Subscription is currently paused" 46 | error: "There was some failure in pausing subscription" 47 | unpause: 48 | success: "Subscription is active. Your next occurrence will be on %{next_occurrence_at}" 49 | error: "There was some problem in activating subscription" 50 | cancel: 51 | success: "Subscription is cancelled" 52 | cancellation: 53 | confirm: "Once cancelled, subscription can not be enabled again" 54 | error_on_already_cancelled: "Cancelled Subscription can not be processed." 55 | 56 | required_authentication: 'Please Login Before Proceeding.' 57 | 58 | activerecord: 59 | errors: 60 | models: 61 | spree/subscription: 62 | attributes: 63 | variant_id: 64 | does_not_belong_to_product: 'does not belong to current product' 65 | 66 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/backend/line_items_on_order_edit.js: -------------------------------------------------------------------------------- 1 | // This file contains the code for interacting with line items in the manual cart 2 | $(document).ready(function () { 3 | 'use strict'; 4 | 5 | // handle variant selection, show stock level. 6 | $('#add_line_item_variant_id').change(function(){ 7 | var variant_id = $(this).val(); 8 | 9 | var variant = _.find(window.variants, function(variant){ 10 | return variant.id == variant_id 11 | }) 12 | 13 | $('#stock_details').html(variantLineItemTemplate({variant: variant})); 14 | $('#stock_details').show(); 15 | 16 | $('button.add_variant').click(addVariant); 17 | 18 | //Function added for susbcription orders 19 | disableSubscriptionFieldsOnOneTimeOrder(variant_id); 20 | }); 21 | }); 22 | 23 | addVariant = function() { 24 | $('#stock_details').hide(); 25 | 26 | var variant_id = $('input.variant_autocomplete').val(); 27 | var quantity = $("input.quantity[data-variant-id='" + variant_id + "']").val(); 28 | // fields added for making subscription order. 29 | var subscribe = $("input.subscribe[data-variant-id='" + variant_id + "']:checked").val(); 30 | var delivery_number = $("input.delivery_number[data-variant-id='" + variant_id + "']").val(); 31 | var frequency = $("select#frequency[data-variant-id='" + variant_id + "']").val(); 32 | 33 | adjustLineItems(order_number, variant_id, quantity, subscribe, delivery_number, frequency); 34 | return 1 35 | } 36 | 37 | // function modified for subscription order fields 38 | adjustLineItems = function(order_number, variant_id, quantity, subscribe, delivery_number, frequency){ 39 | var url = Spree.routes.orders_api + "/" + order_number + '/line_items'; 40 | 41 | $.ajax({ 42 | type: "POST", 43 | url: Spree.url(url), 44 | data: { 45 | line_item: { 46 | variant_id: variant_id, 47 | quantity: quantity, 48 | options: { subscribe: subscribe, 49 | delivery_number: delivery_number, 50 | subscription_frequency_id: frequency 51 | } 52 | }, 53 | token: Spree.api_key 54 | } 55 | }).done(function( msg ) { 56 | window.Spree.advanceOrder(); 57 | window.location.reload(); 58 | }).fail(function(msg) { 59 | alert(msg.responseJSON.message) 60 | }); 61 | 62 | } 63 | 64 | // Function added for subscription fields 65 | disableSubscriptionFieldsOnOneTimeOrder = function(variant_id) { 66 | var delivery_number = $("input.delivery_number[data-variant-id='" + variant_id + "']"); 67 | var frequency = $("select#frequency[data-variant-id='" + variant_id + "']"); 68 | $("input.subscribe[data-variant-id='" + variant_id + "']").on("change", function() { 69 | if (!parseInt($(this).val())) { 70 | delivery_number.attr("disabled", "disabled"); 71 | frequency.attr("disabled", "disabled"); 72 | } else { 73 | delivery_number.removeAttr("disabled"); 74 | frequency.removeAttr("disabled"); 75 | } 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /app/views/spree/admin/subscriptions/_orders.html.erb: -------------------------------------------------------------------------------- 1 |

Orders

2 |
3 |
4 | <% if @subscription.orders.present? %> 5 | 6 | 7 | 8 | <% if @show_only_completed %> 9 | 10 | <% else %> 11 | 12 | <% end %> 13 | 14 | 15 | 16 | 17 | <% if Spree::Order.checkout_step_names.include?(:delivery) %> 18 | 19 | <% end %> 20 | 21 | 22 | 23 | 24 | 25 | <% @subscription.orders.each do |order| %> 26 | 27 | 28 | 29 | 34 | 37 | 42 | <% if Spree::Order.checkout_step_names.include?(:delivery) %> 43 | 48 | <% end %> 49 | 50 | 53 | 54 | <% end %> 55 | 56 |
<%= Spree.t :completed_at %><%= Spree.t :created_at %><%= Spree.t :number %><%= Spree.t :considered_risky %><%= Spree.t :state %><%= Spree.t :payment_state %><%= Spree.t :shipment_state %><%= Spree.t :total %>
<%= l (@show_only_completed ? order.completed_at : order.created_at).try!(:to_date) %><%= link_to order.number, edit_admin_order_path(order) %> 30 | 31 | <%= order.considered_risky ? Spree.t("risky") : Spree.t("safe") %> 32 | 33 | 35 | <%= Spree.t("order_state.#{order.state.downcase}") %> 36 | 38 | <% if order.payment_state %> 39 | <%= link_to Spree.t("payment_states.#{order.payment_state}"), admin_order_payments_path(order) %> 40 | <% end %> 41 | 44 | <% if order.shipment_state %> 45 | <%= Spree.t("shipment_states.#{order.shipment_state}") %> 46 | <% end %> 47 | <%= order.display_total.to_html %> 51 | <%= link_to_edit_url edit_admin_order_path(order), title: "admin_edit_#{dom_id(order)}", no_text: true %> 52 |
57 | <% else %> 58 |
59 | <%= Spree.t(:no_sub_orders_found) %> 60 |
61 | <% end %> 62 |
63 |
64 | -------------------------------------------------------------------------------- /app/views/spree/subscription_notifier/notify_cancellation.html.erb: -------------------------------------------------------------------------------- 1 |

Hi <%= @subscription.parent_order.email %>

2 |

Your <%= @subscription.frequency.title %> subscription is cancelled

3 |
4 |

5 | <%= Spree.t('order_mailer.confirm_email.order_summary', number: @subscription.parent_order.number) %> 6 |

7 | 8 | <% @subscription.parent_order.line_items.where(variant: @subscription.variant).each do |item| %> 9 | 10 | 11 | 15 | 16 | 17 | <% end %> 18 | 19 | 20 | 23 | 26 | 27 | <% if @subscription.parent_order.line_item_adjustments.exists? %> 28 | <% if @subscription.parent_order.all_adjustments.promotion.eligible.exists? %> 29 | <% @subscription.parent_order.all_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %> 30 | 31 | 32 | 33 | 34 | 35 | <% end %> 36 | <% end %> 37 | <% end %> 38 | <% @subscription.parent_order.shipments.group_by { |s| s.selected_shipping_rate.try(:name) }.each do |name, shipments| %> 39 | 40 | 41 | 42 | 43 | 44 | <% end %> 45 | <% if @subscription.parent_order.all_adjustments.eligible.tax.exists? %> 46 | <% @subscription.parent_order.all_adjustments.eligible.tax.group_by(&:label).each do |label, adjustments| %> 47 | 48 | 49 | 50 | 51 | 52 | <% end %> 53 | <% end %> 54 | <% @subscription.parent_order.adjustments.eligible.each do |adjustment| %> 55 | <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> 56 | 57 | 58 | 59 | 60 | 61 | <% end %> 62 | 63 | 64 | 67 | 70 | 71 |
<%= item.variant.sku %> 12 | <%= raw(item.variant.product.name) %> 13 | <%= raw(item.variant.options_text) -%> 14 | (<%=item.quantity%>) <%= Spree.t('at_symbol') %> <%= item.single_money %> = <%= item.display_amount %>
21 | <%= Spree.t('order_mailer.confirm_email.subtotal') %> 22 | 24 | <%= @subscription.parent_order.display_item_total %> 25 |
<%= Spree.t(:promotion) %> <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: @subscription.parent_order.currency) %>
<%= Spree.t(:shipping) %> <%= name %>:<%= Spree::Money.new(shipments.sum(&:discounted_cost), currency: @subscription.parent_order.currency) %>
<%= Spree.t(:tax) %> <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: @subscription.parent_order.currency) %>
<%= adjustment.label %>:<%= adjustment.display_amount %>
65 | <%= Spree.t('order_mailer.confirm_email.total') %> 66 | 68 | <%= @subscription.parent_order.display_total %> 69 |
72 |
73 | 74 |

Regards

75 | -------------------------------------------------------------------------------- /app/views/spree/subscription_notifier/notify_reoccurrence.html.erb: -------------------------------------------------------------------------------- 1 |

Hi <%= @subscription.parent_order.email %>

2 |
3 |

4 | <%= Spree.t('order_mailer.confirm_email.order_summary', number: @subscription.parent_order.number) %> 5 |

6 | 7 | <% @subscription.parent_order.line_items.where(variant: @subscription.variant).each do |item| %> 8 | 9 | 10 | 14 | 15 | 16 | <% end %> 17 | 18 | 19 | 22 | 25 | 26 | <% if @subscription.parent_order.line_item_adjustments.exists? %> 27 | <% if @subscription.parent_order.all_adjustments.promotion.eligible.exists? %> 28 | <% @subscription.parent_order.all_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %> 29 | 30 | 31 | 32 | 33 | 34 | <% end %> 35 | <% end %> 36 | <% end %> 37 | <% @subscription.parent_order.shipments.group_by { |s| s.selected_shipping_rate.try(:name) }.each do |name, shipments| %> 38 | 39 | 40 | 41 | 42 | 43 | <% end %> 44 | <% if @subscription.parent_order.all_adjustments.eligible.tax.exists? %> 45 | <% @subscription.parent_order.all_adjustments.eligible.tax.group_by(&:label).each do |label, adjustments| %> 46 | 47 | 48 | 49 | 50 | 51 | <% end %> 52 | <% end %> 53 | <% @subscription.parent_order.adjustments.eligible.each do |adjustment| %> 54 | <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> 55 | 56 | 57 | 58 | 59 | 60 | <% end %> 61 | 62 | 63 | 66 | 69 | 70 |
<%= item.variant.sku %> 11 | <%= raw(item.variant.product.name) %> 12 | <%= raw(item.variant.options_text) -%> 13 | (<%=item.quantity%>) <%= Spree.t('at_symbol') %> <%= item.single_money %> = <%= item.display_amount %>
20 | <%= Spree.t('order_mailer.confirm_email.subtotal') %> 21 | 23 | <%= @subscription.parent_order.display_item_total %> 24 |
<%= Spree.t(:promotion) %> <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: @subscription.parent_order.currency) %>
<%= Spree.t(:shipping) %> <%= name %>:<%= Spree::Money.new(shipments.sum(&:discounted_cost), currency: @subscription.parent_order.currency) %>
<%= Spree.t(:tax) %> <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: @subscription.parent_order.currency) %>
<%= adjustment.label %>:<%= adjustment.display_amount %>
64 | <%= Spree.t('order_mailer.confirm_email.total') %> 65 | 67 | <%= @subscription.parent_order.display_total %> 68 |
71 |
72 | 73 |

Your <%= @subscription.frequency.title.capitalize %> subscription has <%= @subscription.number_of_deliveries_left %> deliveries left

74 |

Regards

75 | -------------------------------------------------------------------------------- /app/views/spree/subscription_notifier/notify_confirmation.html.erb: -------------------------------------------------------------------------------- 1 |

Hi <%= @subscription.parent_order.email %>

2 |

Your <%= @subscription.frequency.title %> subscription is confirmed

3 |
4 |

5 | <%= Spree.t('order_mailer.confirm_email.order_summary', number: @subscription.parent_order.number) %> 6 |

7 | 8 | <% @subscription.parent_order.line_items.where(variant: @subscription.variant).each do |item| %> 9 | 10 | 11 | 15 | 16 | 17 | <% end %> 18 | 19 | 20 | 23 | 26 | 27 | <% if @subscription.parent_order.line_item_adjustments.exists? %> 28 | <% if @subscription.parent_order.all_adjustments.promotion.eligible.exists? %> 29 | <% @subscription.parent_order.all_adjustments.promotion.eligible.group_by(&:label).each do |label, adjustments| %> 30 | 31 | 32 | 33 | 34 | 35 | <% end %> 36 | <% end %> 37 | <% end %> 38 | <% @subscription.parent_order.shipments.group_by { |s| s.selected_shipping_rate.try(:name) }.each do |name, shipments| %> 39 | 40 | 41 | 42 | 43 | 44 | <% end %> 45 | <% if @subscription.parent_order.all_adjustments.eligible.tax.exists? %> 46 | <% @subscription.parent_order.all_adjustments.eligible.tax.group_by(&:label).each do |label, adjustments| %> 47 | 48 | 49 | 50 | 51 | 52 | <% end %> 53 | <% end %> 54 | <% @subscription.parent_order.adjustments.eligible.each do |adjustment| %> 55 | <% next if (adjustment.source_type == 'Spree::TaxRate') and (adjustment.amount == 0) %> 56 | 57 | 58 | 59 | 60 | 61 | <% end %> 62 | 63 | 64 | 67 | 70 | 71 |
<%= item.variant.sku %> 12 | <%= raw(item.variant.product.name) %> 13 | <%= raw(item.variant.options_text) -%> 14 | (<%=item.quantity%>) <%= Spree.t('at_symbol') %> <%= item.single_money %> = <%= item.display_amount %>
21 | <%= Spree.t('order_mailer.confirm_email.subtotal') %> 22 | 24 | <%= @subscription.parent_order.display_item_total %> 25 |
<%= Spree.t(:promotion) %> <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: @subscription.parent_order.currency) %>
<%= Spree.t(:shipping) %> <%= name %>:<%= Spree::Money.new(shipments.sum(&:discounted_cost), currency: @subscription.parent_order.currency) %>
<%= Spree.t(:tax) %> <%= label %>:<%= Spree::Money.new(adjustments.sum(&:amount), currency: @subscription.parent_order.currency) %>
<%= adjustment.label %>:<%= adjustment.display_amount %>
65 | <%= Spree.t('order_mailer.confirm_email.total') %> 66 | 68 | <%= @subscription.parent_order.display_total %> 69 |
72 |
73 | 74 |

Your <%= @subscription.frequency.title.capitalize %> subscription has <%= @subscription.number_of_deliveries_left %> deliveries left

75 |

Regards

76 | -------------------------------------------------------------------------------- /spec/models/spree/order_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Order, type: :model do 4 | 5 | let(:disabled_subscription) { create(:valid_subscription, enabled: false) } 6 | let(:subscriptions) { [disabled_subscription] } 7 | let(:order_with_subscriptions) { create(:completed_order_with_pending_payment, subscriptions: subscriptions) } 8 | let(:incompleted_order) { create(:order_with_line_items, subscriptions: subscriptions, state: "confirm", payments: order_with_subscriptions.payments) } 9 | 10 | describe "associations" do 11 | it { is_expected.to have_one(:order_subscription).class_name("Spree::OrderSubscription").dependent(:destroy) } 12 | it { is_expected.to have_one(:parent_subscription).through(:order_subscription).source(:subscription) } 13 | it { is_expected.to have_many(:subscriptions).class_name("Spree::Subscription").with_foreign_key(:parent_order_id).dependent(:restrict_with_error) } 14 | end 15 | 16 | describe "callbacks" do 17 | it { is_expected.to callback(:update_subscriptions).after(:update) } 18 | end 19 | 20 | describe "methods" do 21 | context "#available_payment_methods" do 22 | let(:order_without_subscriptions) { create(:order_with_line_items) } 23 | let(:credit_card_payment_method) { create(:credit_card_payment_method) } 24 | let(:check_payment_method) { create(:check_payment_method) } 25 | context "order with subscriptions" do 26 | it { expect(order_with_subscriptions.available_payment_methods).to include credit_card_payment_method } 27 | it { expect(order_with_subscriptions.available_payment_methods).to_not include check_payment_method } 28 | end 29 | 30 | context "order without subscriptions" do 31 | it { expect(order_without_subscriptions.available_payment_methods).to include check_payment_method } 32 | it { expect(order_without_subscriptions.available_payment_methods).to include credit_card_payment_method } 33 | end 34 | end 35 | 36 | context "#any_disabled_subscription?" do 37 | it { expect(order_with_subscriptions.send :any_disabled_subscription?).to eq true } 38 | end 39 | 40 | context "#enable_subscriptions" do 41 | it { expect { order_with_subscriptions.send :enable_subscriptions }.to change { order_with_subscriptions.subscriptions.disabled.count }.by -1 } 42 | end 43 | 44 | context "#update_subscriptions" do 45 | context "when subscription attributes present" do 46 | let(:line_item) { create(:line_item, variant: disabled_subscription.variant) } 47 | def add_new_line_item 48 | line_item.delivery_number = 6 49 | order_with_subscriptions.line_items << line_item 50 | order_with_subscriptions.send :update_subscriptions 51 | end 52 | it { expect { add_new_line_item }.to change { disabled_subscription.reload.delivery_number }.from(4).to(6) } 53 | end 54 | 55 | context "when subscription attributes not present" do 56 | it { expect { order_with_subscriptions.send :update_subscriptions }.to change { disabled_subscription.delivery_number }.by 0 } 57 | end 58 | end 59 | 60 | context "state machine" do 61 | it { expect { incompleted_order.next }.to change { incompleted_order.subscriptions.disabled.count }.by -1 } 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/views/spree/subscriptions/_summary.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |

<%= Spree.t(:subscription_summary) %>

4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 21 | 22 | 23 | 26 | 32 | 33 | 34 | 37 | 45 | 46 | 47 | 50 | 58 | 59 | 60 | 61 | 62 | 63 | <% if @subscription.cancelled? %> 64 | 65 | 68 | 71 | 72 | 73 | 76 | 79 | 80 | <% end %> 81 | 82 |
<%= Spree.t(:customer_email) %><%= @subscription.parent_order.email %>
14 | <%= Spree.t(:original_order) %>: 15 | 17 | 18 | <%= link_to @subscription.parent_order.number, order_url(@subscription.parent_order) %> 19 | 20 |
24 | <%= Spree.t(:next_occurrence_at) %>: 25 | 27 | <%= f.field_container :next_occurrence_at, class: ["form-group"] do %> 28 | <%= f.text_field :next_occurrence_at, class: "form-control datepicker subscription_next_occurrence_at", disabled: @subscription.not_changeable? || @subscription.paused?, value: @subscription.next_occurrence_at.to_date %> 29 | <%= f.error_message_on :next_occurrence_at %> 30 | <% end %> 31 |
35 | <%= Spree.t(:total_deliveries) %> 36 | 38 |
39 | <%= f.field_container :delivery_number, class: ["form-group"] do %> 40 | <%= number_field :subscription, :delivery_number, class: "form-control", disabled: @subscription.not_changeable? %> 41 | <%= f.error_message_on :delivery_number %> 42 | <% end %> 43 |
44 |
48 | <%= Spree.t(:prior_notification_days_gap) %> 49 | 51 |
52 | <%= f.field_container :prior_notification_days_gap, class: ["form-group"] do %> 53 | <%= number_field :subscription, :prior_notification_days_gap, class: "form-control", disabled: @subscription.not_changeable?, max: days_left_for_next_occurrence, min: 0 %> 54 | <%= f.error_message_on :prior_notification_days_gap %> 55 | <% end %> 56 |
57 |
<%= Spree.t(:deliveries_left) %><%= @subscription.number_of_deliveries_left %>
66 | <%= Spree.t(:cancelled_at) %> 67 | 69 | <%= @subscription.cancelled_at.to_formatted_s(:long) %> 70 |
74 | <%= Spree.t(:cancellation_reasons) %> 75 | 77 | <%= @subscription.cancellation_reasons.html_safe %> 78 |
83 |
84 | -------------------------------------------------------------------------------- /app/views/spree/admin/variants/_autocomplete_line_items_stock.js.erb: -------------------------------------------------------------------------------- 1 | 77 | -------------------------------------------------------------------------------- /app/controllers/spree/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class SubscriptionsController < Spree::StoreController 3 | 4 | before_action :ensure_subscription 5 | before_action :ensure_subscription_belongs_to_user, only: :edit 6 | before_action :ensure_not_cancelled, only: [:update, :cancel, :pause, :unpause] 7 | 8 | def edit 9 | end 10 | 11 | def update 12 | if @subscription.update(subscription_attributes) 13 | respond_to do |format| 14 | format.html { redirect_to edit_subscription_path(@subscription), success: t('.success') } 15 | format.json { render json: { subscription: { price: @subscription.price, id: @subscription.id } }, status: 200 } 16 | end 17 | else 18 | respond_to do |format| 19 | format.html { render :edit } 20 | format.json { render json: { errors: @subscription.errors.full_messages.to_sentence }, status: 422 } 21 | end 22 | end 23 | end 24 | 25 | def cancel 26 | respond_to do |format| 27 | if @subscription.cancel 28 | format.json { render json: { 29 | subscription_id: @subscription.id, 30 | flash: t(".success"), 31 | method: Spree::Subscription::ACTION_REPRESENTATIONS[:cancel].upcase 32 | }, status: 200 33 | } 34 | format.html { redirect_to edit_subscription_path(@subscription), success: t(".success") } 35 | else 36 | format.json { render json: { 37 | flash: t(".error") 38 | }, status: 422 39 | } 40 | format.html { redirect_to edit_subscription_path(@subscription), error: t(".error") } 41 | end 42 | end 43 | end 44 | 45 | def pause 46 | if @subscription.pause 47 | render json: { 48 | flash: t('.success'), 49 | url: unpause_subscription_path(@subscription), 50 | button_text: Spree::Subscription::ACTION_REPRESENTATIONS[:unpause], 51 | confirmation: Spree.t("subscriptions.confirm.activate") 52 | }, status: 200 53 | else 54 | render json: { 55 | flash: t('.error') 56 | }, status: 422 57 | end 58 | end 59 | 60 | def unpause 61 | if @subscription.unpause 62 | render json: { 63 | flash: t('.success', next_occurrence_at: @subscription.next_occurrence_at.to_date.to_formatted_s(:rfc822)), 64 | url: pause_subscription_path(@subscription), 65 | button_text: Spree::Subscription::ACTION_REPRESENTATIONS[:pause], 66 | next_occurrence_at: @subscription.next_occurrence_at.to_date, 67 | confirmation: Spree.t("subscriptions.confirm.pause") 68 | }, status: 200 69 | else 70 | render json: { 71 | flash: t('.error') 72 | }, status: 422 73 | end 74 | end 75 | 76 | private 77 | 78 | def subscription_attributes 79 | params.require(:subscription).permit(:quantity, :next_occurrence_at, :delivery_number, 80 | :subscription_frequency_id, :variant_id, :prior_notification_days_gap, 81 | ship_address_attributes: [:firstname, :lastname, :address1, :address2, :city, :zipcode, :country_id, :state_id, :phone], 82 | bill_address_attributes: [:firstname, :lastname, :address1, :address2, :city, :zipcode, :country_id, :state_id, :phone]) 83 | end 84 | 85 | def ensure_subscription 86 | @subscription = Spree::Subscription.active.find_by(id: params[:id]) 87 | unless @subscription 88 | respond_to do |format| 89 | format.html { redirect_to account_path, error: Spree.t('subscriptions.alert.missing') } 90 | format.json { render json: { flash: Spree.t("subscriptions.alert.missing") }, status: 422 } 91 | end 92 | end 93 | end 94 | 95 | def ensure_not_cancelled 96 | if @subscription.not_changeable? 97 | respond_to do |format| 98 | format.html { redirect_back fallback_location: root_path, error: Spree.t("subscriptions.error.not_changeable") } 99 | format.json { render json: { flash: Spree.t("subscriptions.error.not_changeable") }, status: 422 } 100 | end 101 | end 102 | end 103 | 104 | def ensure_subscription_belongs_to_user 105 | authorize! :update, @subscription 106 | end 107 | 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Spree Product Subscriptions 2 | =========================== 3 | 4 | Spree Product Subscriptions is an extension to let users have time interval based subscription of products in a spree application. 5 | 6 | * This extension allows the admin to create a subscribable product on the Admin end. 7 | 8 | * This product can then be bought one-time or as a subscription. 9 | 10 | * Once subscribed, subscription orders will automatically be created for the user at the selected time interval. 11 | 12 | Demo 13 | ---- 14 | Try Spree Product Subscriptions for Spree master with direct deployment on Heroku: 15 | 16 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/vinsol-spree-contrib/spree-demo-heroku/tree/spree-product-subscriptions-master) 17 | 18 | Try Spree Product Subscriptions for Spree 4 with direct deployment on Heroku: 19 | 20 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/vinsol-spree-contrib/spree-demo-heroku/tree/spree-product-subscriptions-4-1) 21 | 22 | Try Spree Product Subscriptions for Spree 3-4 with direct deployment on Heroku: 23 | 24 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/vinsol-spree-contrib/spree-demo-heroku/tree/spree-product-subscriptions-3-4) 25 | 26 | Installation 27 | ------------ 28 | 29 | Add spree_product_subscriptions to your Gemfile: 30 | 31 | ```ruby 32 | gem 'spree_product_subscriptions', github: 'vinsol-spree-contrib/spree_product_subscriptions' 33 | ``` 34 | 35 | Bundle your dependencies and run the installation generator: 36 | 37 | ```shell 38 | bundle 39 | bundle exec rails g spree_product_subscriptions:install 40 | ``` 41 | 42 | You can also seed the default data with: 43 | ```shell 44 | bundle exec rails g spree_product_subscriptions:seed 45 | ``` 46 | 47 | Working 48 | ------- 49 | 50 | * Admin can mark a product as "subscribable" on the `Admin -> Products -> Edit` page. The admin will have to choose the subscription frequencies to be made available for that product. 51 | 52 | * Subscription frequencies are created by default when you seed data. You can also add subscription frequencies through `Admin -> Configurations -> Subscription Frequencies -> New` page. 53 | 54 | * When user is purchasing a subscribable product, he gets an option to make it a 'One Time Order' or a 'Subscription Order'. 55 | 56 | * When making a 'Subscription Order', the user will have to choose Delivery Interval, Total Deliveries, and Quantity. The first order will be made on checkout and remaining orders will automatically be created for the user at the selected time intervals. 57 | 58 | * The users can check their subscriptions on the 'My Account' page. They can update subscription info, pause or cancel their subscriptions via the `Subscription -> Edit` page. 59 | 60 | * A cron job needs to be run to process subscriptions 61 | ``` 62 | bundle exec rake subscription:process 63 | ``` 64 | This will run the pending subscriptions. 65 | 66 | * A cron job can also be run to notify users of upcoming subscriptions: 67 | ``` 68 | bundle exec rake subscription:prior_notify 69 | ``` 70 | This will inform users that they have a subscription that is coming up in 'x' days. The number of days can be changed on subscription edit page. 71 | 72 | **Here is a detailed article with screenshot http://vinsol.com/spreecommerce-subscription** 73 | Testing 74 | ------- 75 | 76 | Be sure to bundle your dependencies and then create a dummy test app for the specs to run against. 77 | 78 | ```shell 79 | bundle 80 | bundle exec rake test_app 81 | bundle exec rspec spec 82 | ``` 83 | 84 | When testing your applications integration with this extension you may use it's factories. 85 | Simply add this require statement to your spec_helper: 86 | 87 | ```ruby 88 | require 'spree_product_subscriptions/factories' 89 | ``` 90 | 91 | ## See It In Action 92 | 93 | Youtube Video Tutorial 96 | 97 | Credits 98 | ------- 99 | 100 | [![vinsol.com: Ruby on Rails, iOS and Android developers](http://vinsol.com/themes/vinsoldotcom-theme/images/new_img/vin_logo.png "Ruby on Rails, iOS and Android developers")](http://vinsol.com) 101 | 102 | Copyright (c) 2016 [vinsol.com](http://vinsol.com "Ruby on Rails, iOS and Android developers"), released under the New MIT License 103 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/frontend/ajax_handler.js: -------------------------------------------------------------------------------- 1 | function AjaxHandler(targets, lineItemsTable) { 2 | this.targets = targets; 3 | this.lineItemsTable = lineItemsTable; 4 | } 5 | 6 | AjaxHandler.prototype.init = function() { 7 | this.bindEvents(); 8 | }; 9 | 10 | AjaxHandler.prototype.bindEvents = function() { 11 | var _this = this; 12 | 13 | $.each(_this.targets, function(index, target) { 14 | var $target = $(target); 15 | $target.unbind('click').on('click', function() { 16 | var data = $target.data(); 17 | if(confirm(data.confirmation)) { 18 | _this.sendRequest($target, data); 19 | } 20 | }); 21 | }); 22 | 23 | _this.lineItemsTable.on('change', '#subscription_variant_id', function(){ 24 | _this.updateVariant(this); 25 | }); 26 | 27 | }; 28 | 29 | AjaxHandler.prototype.updateVariant = function(variant_select) { 30 | var _this = this; 31 | $.ajax({ 32 | url: '/subscriptions/' + $(variant_select).data('subscription-id'), 33 | dataType: "JSON", 34 | method: 'PUT', 35 | data: { 36 | id: $(variant_select).data('subscription-id'), 37 | subscription: { 38 | variant_id: $(variant_select).children(':selected').val() 39 | } 40 | }, 41 | success: function(response) { 42 | var subscription = response['subscription']; 43 | var $lineItemPrice = _this.lineItemsTable.find('td.line-item-price') 44 | .find('[data-subscription-id="'+ subscription['id'] +'"]'); 45 | $lineItemPrice.html(subscription['price']); 46 | show_flash('success', 'Variant has been updated.'); 47 | }, 48 | error: function(response) { 49 | errors = JSON.parse(response.responseText).errors; 50 | show_flash('danger', errors); 51 | } 52 | }); 53 | }; 54 | 55 | AjaxHandler.prototype.sendRequest = function($target, data) { 56 | var _this = this; 57 | $.ajax({ 58 | url: data.url, 59 | dataType: "JSON", 60 | method: data.method, 61 | success: function(response) { 62 | if (response.method == "CANCEL") { 63 | _this.handleCancelSuccess($target, response); 64 | } else { 65 | _this.handlePatchSuccess($target, response) 66 | } 67 | }, 68 | error: function(response) { 69 | _this.handleErrorResponse($target, response); 70 | } 71 | }); 72 | }; 73 | 74 | show_flash = function(type, message) { 75 | var flash_div = $('.flash.' + type); 76 | if (flash_div.length == 0) { 77 | flash_div = $('
'); 78 | $('#content').prepend(flash_div); 79 | } 80 | flash_div.html(message).show().delay(5000).slideUp(); 81 | } 82 | 83 | AjaxHandler.prototype.handlePatchSuccess = function($target, response) { 84 | this.hideFlashDivs(); 85 | $target.data("url", response.url); 86 | if (response.url.match("unpause")) { 87 | $(".subscription_next_occurrence_at").attr("disabled", "disabled"); 88 | } else { 89 | $(".subscription_next_occurrence_at").val(response.next_occurrence_at); 90 | $(".subscription_next_occurrence_at").removeAttr("disabled"); 91 | } 92 | $target.toggleClass("btn-success"); 93 | $target.toggleClass("btn-warning"); 94 | $("#success_flash_message").html(response.flash).removeClass("hidden"); 95 | var $symbol = $target.find(".icon"); 96 | $symbol.toggleClass("icon-pause").toggleClass("icon-play"); 97 | if (!$target.find(".translation_missing").length && !$symbol.length) { 98 | $target.html(response.button_text); 99 | } 100 | $target.find(".translation_missing").html(response.button_text); 101 | $target.data("confirmation", response.confirmation); 102 | }; 103 | 104 | AjaxHandler.prototype.handleCancelSuccess = function($target, response) { 105 | this.hideFlashDivs(); 106 | $("#success_flash_message").html(response.flash).removeClass("hidden"); 107 | $('[data-id="' + response.subscription_id + '"] .subscription-action-links').html("Subscription Cancelled"); 108 | }; 109 | 110 | AjaxHandler.prototype.handleErrorResponse = function($target, response) { 111 | this.hideFlashDivs(); 112 | $("#error_flash_message").html(response.flash).removeClass("hidden"); 113 | }; 114 | 115 | AjaxHandler.prototype.hideFlashDivs = function() { 116 | $("#html_error_flash_message").remove(); 117 | $("#error_flash_message").addClass("hidden"); 118 | $("#html_success_flash_message").remove(); 119 | $("#success_flash_message").addClass("hidden"); 120 | }; 121 | 122 | $(function (){ 123 | var ajaxHandler = new AjaxHandler($('.ajax_handler'), $('table.line-items')); 124 | ajaxHandler.init(); 125 | }); 126 | -------------------------------------------------------------------------------- /spec/controllers/spree/admin/payments_controller_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::Admin::PaymentsController, type: :controller do 4 | 5 | let(:order) { create(:completed_order_with_pending_payment) } 6 | let(:subscriptions) { double(ActiveRecord::Relation) } 7 | 8 | stub_authorization! 9 | 10 | def do_new 11 | spree_get :new, order_id: order.number 12 | end 13 | 14 | describe "method overrides" do 15 | 16 | let(:orders) { double(ActiveRecord::Relation) } 17 | let(:credit_card_payment_method) { create(:credit_card_payment_method) } 18 | let(:check_payment_method) { create(:check_payment_method) } 19 | let(:payment_methods) { [check_payment_method, credit_card_payment_method] } 20 | let(:payment_methods_without_check) { [credit_card_payment_method] } 21 | 22 | before do 23 | allow(Spree::Order).to receive(:find_by!).and_return(order) 24 | allow(order).to receive(:subscriptions).and_return(subscriptions) 25 | end 26 | 27 | describe "#load_data" do 28 | context "when subscriptions are present" do 29 | before do 30 | allow(subscriptions).to receive(:any?).and_return(true) 31 | allow(Spree::Gateway).to receive(:available_on_back_end).and_return(payment_methods_without_check) 32 | end 33 | 34 | context "expects to receive" do 35 | after { do_new } 36 | it { expect(Spree::Order).to receive(:find_by!).and_return(order) } 37 | it { expect(order).to receive(:subscriptions).and_return(subscriptions) } 38 | it { expect(subscriptions).to receive(:any?).and_return(true) } 39 | it { expect(Spree::Gateway).to receive(:available_on_back_end).and_return(payment_methods_without_check) } 40 | end 41 | 42 | context "assigns" do 43 | before { do_new } 44 | it { expect(assigns(:payment_method)).to eq credit_card_payment_method } 45 | end 46 | end 47 | 48 | context "when subscriptions are not present" do 49 | before do 50 | allow(subscriptions).to receive(:any?).and_return(false) 51 | allow(Spree::PaymentMethod).to receive(:available_on_back_end).and_return(payment_methods) 52 | end 53 | 54 | context "expects to receive" do 55 | after { do_new } 56 | it { expect(Spree::Order).to receive(:find_by!).and_return(order) } 57 | it { expect(order).to receive(:subscriptions).and_return(subscriptions) } 58 | it { expect(subscriptions).to receive(:any?).and_return(false) } 59 | it { expect(Spree::PaymentMethod).to receive(:available_on_back_end).and_return(payment_methods) } 60 | end 61 | 62 | context "assigns" do 63 | before { do_new } 64 | it { expect(assigns(:payment_method)).to eq check_payment_method } 65 | end 66 | end 67 | end 68 | 69 | describe "#available_payment_methods" do 70 | context "when subscriptions are present" do 71 | before do 72 | allow(subscriptions).to receive(:any?).and_return(true) 73 | allow(Spree::Gateway).to receive(:available_on_back_end).and_return(payment_methods_without_check) 74 | end 75 | 76 | context "expects to receive" do 77 | after { do_new } 78 | it { expect(Spree::Order).to receive(:find_by!).and_return(order) } 79 | it { expect(order).to receive(:subscriptions).and_return(subscriptions) } 80 | it { expect(subscriptions).to receive(:any?).and_return(true) } 81 | it { expect(Spree::Gateway).to receive(:available_on_back_end).and_return(payment_methods_without_check) } 82 | end 83 | 84 | context "returns" do 85 | before { do_new } 86 | it { expect(controller.send :available_payment_methods).to eq payment_methods_without_check } 87 | it { expect(controller.send :available_payment_methods).to_not include check_payment_method } 88 | end 89 | end 90 | 91 | context "when subscriptions are not present" do 92 | before do 93 | allow(subscriptions).to receive(:any?).and_return(false) 94 | allow(Spree::PaymentMethod).to receive(:available_on_back_end).and_return(payment_methods) 95 | end 96 | 97 | context "expects to receive" do 98 | after { do_new } 99 | it { expect(Spree::Order).to receive(:find_by!).and_return(order) } 100 | it { expect(order).to receive(:subscriptions).and_return(subscriptions) } 101 | it { expect(subscriptions).to receive(:any?).and_return(false) } 102 | it { expect(Spree::PaymentMethod).to receive(:available_on_back_end).and_return(payment_methods) } 103 | end 104 | 105 | context "returns" do 106 | before { do_new } 107 | it { expect(controller.send :available_payment_methods).to eq payment_methods } 108 | it { expect(controller.send :available_payment_methods).to include check_payment_method } 109 | end 110 | end 111 | end 112 | end 113 | 114 | end 115 | -------------------------------------------------------------------------------- /spec/models/spree/line_item_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::LineItem, type: :model do 4 | 5 | let(:order) { create(:completed_order_with_totals) } 6 | let(:frequency) { create(:monthly_subscription_frequency, title: "monthly1") } 7 | let(:variant) { create(:base_variant) } 8 | let!(:line_item_without_subscription_attributes) { create(:line_item, order: order, variant: variant) } 9 | let!(:line_item_with_subscription_attributes) { create(:line_item, order: order, subscription_frequency_id: 1, delivery_number: 6) } 10 | let!(:active_subscription) { create(:valid_subscription, enabled: true, variant: line_item_with_subscription_attributes.variant, subscription_frequency_id: frequency.id, delivery_number: 6, parent_order: order) } 11 | let!(:line_item_without_subscription) { create(:line_item) } 12 | 13 | describe "callbacks" do 14 | it { is_expected.to callback(:create_subscription!).after(:create).if(:subscribable?) } 15 | it { is_expected.to callback(:update_subscription_quantity).after(:update).if(:can_update_subscription_quantity?) } 16 | it { is_expected.to callback(:update_subscription_attributes).after(:update).if(:can_update_subscription_attributes?) } 17 | it { is_expected.to callback(:destroy_associated_subscription!).after(:destroy).if(:subscription?) } 18 | end 19 | 20 | describe "attr_accessors" do 21 | it { is_expected.to respond_to :delivery_number } 22 | it { is_expected.to respond_to :delivery_number= } 23 | it { is_expected.to respond_to :subscribe } 24 | it { is_expected.to respond_to :subscribe= } 25 | it { is_expected.to respond_to :subscription_frequency_id } 26 | it { is_expected.to respond_to :subscription_frequency_id= } 27 | end 28 | 29 | describe "methods" do 30 | context "#subscription_attributes_present?" do 31 | it { expect(line_item_with_subscription_attributes).to be_subscription_attributes_present } 32 | it { expect(line_item_without_subscription_attributes).to_not be_subscription_attributes_present } 33 | end 34 | 35 | context "#updatable_subscription_attributes" do 36 | it { expect(line_item_with_subscription_attributes.updatable_subscription_attributes).to eq({ subscription_frequency_id: 1, delivery_number: 6 }) } 37 | end 38 | 39 | context "#subscription" do 40 | it { expect(line_item_with_subscription_attributes.send :subscription).to eq active_subscription } 41 | it { expect(line_item_without_subscription.send :subscription).to be_nil } 42 | end 43 | 44 | context "#subscription?" do 45 | it { expect(line_item_without_subscription.send :subscription?).to eq false } 46 | it { expect(line_item_with_subscription_attributes.send :subscription?).to eq true } 47 | end 48 | 49 | context "#can_update_subscription_attributes?" do 50 | it { expect(line_item_without_subscription_attributes.send :can_update_subscription_attributes?).to eq false } 51 | it { expect(line_item_with_subscription_attributes.send :can_update_subscription_attributes?).to eq true } 52 | end 53 | 54 | context "#can_update_subscription_quantity?" do 55 | context "when subscription not present" do 56 | it { expect(line_item_without_subscription.send :can_update_subscription_quantity?).to eq false } 57 | end 58 | 59 | context "when subscription is present but quantity not changed" do 60 | it { expect(line_item_with_subscription_attributes.send :can_update_subscription_quantity?).to eq false } 61 | end 62 | 63 | context "when subscription is present and quantity is changed" do 64 | before { line_item_with_subscription_attributes.quantity = 5 } 65 | it { expect(line_item_with_subscription_attributes.send :can_update_subscription_quantity?).to eq true } 66 | end 67 | end 68 | 69 | context "#destroy_associated_subscription!" do 70 | it { expect(line_item_with_subscription_attributes.send :destroy_associated_subscription!).to eq active_subscription } 71 | end 72 | 73 | context "#update_subscription_quantity" do 74 | def update_line_item_subscription_quantity 75 | line_item_with_subscription_attributes.quantity = 9 76 | line_item_with_subscription_attributes.send :update_subscription_quantity 77 | end 78 | it { expect { update_line_item_subscription_quantity }.to change { active_subscription.reload.quantity }.from(2).to(9) } 79 | end 80 | 81 | context "#update_subscription_attributes" do 82 | context "when subscription attributes are changed" do 83 | def update_line_item_subscription_attributes 84 | line_item_with_subscription_attributes.delivery_number = 8 85 | line_item_with_subscription_attributes.subscription_frequency_id = 2 86 | line_item_with_subscription_attributes.send :update_subscription_attributes 87 | end 88 | it { expect { update_line_item_subscription_attributes }.to change { active_subscription.reload.delivery_number }.from(6).to(8) } 89 | it { expect { update_line_item_subscription_attributes }.to change{ active_subscription.reload.subscription_frequency_id }.from(1).to(2) } 90 | end 91 | end 92 | 93 | context "#subscribable?" do 94 | context "when subscribe is present" do 95 | before { line_item_with_subscription_attributes.subscribe = true } 96 | it { expect(line_item_with_subscription_attributes.send :subscribable?).to eq true } 97 | end 98 | context "when subscribe is not present" do 99 | it { expect(line_item_with_subscription_attributes.send :subscribable?).to eq false } 100 | end 101 | end 102 | 103 | context "#subscription_attributes" do 104 | it { expect(line_item_with_subscription_attributes.send :subscription_attributes).to eq({ 105 | subscription_frequency_id: 1, 106 | delivery_number: 6, 107 | variant: active_subscription.variant, 108 | quantity: 1, 109 | price: active_subscription.variant.price 110 | }) } 111 | end 112 | 113 | context "#create_subscription!" do 114 | def create_subscription 115 | line_item_without_subscription_attributes.subscription_frequency_id = frequency.id 116 | line_item_without_subscription_attributes.delivery_number = 5 117 | line_item_without_subscription_attributes.send :create_subscription! 118 | end 119 | it { expect { create_subscription }.to change { order.subscriptions.count }.by 1 } 120 | end 121 | end 122 | 123 | end 124 | -------------------------------------------------------------------------------- /app/models/spree/subscription.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class Subscription < Spree::Base 3 | 4 | attr_accessor :cancelled 5 | 6 | include Spree::Core::NumberGenerator.new(prefix: 'S') 7 | 8 | ACTION_REPRESENTATIONS = { 9 | pause: "Pause", 10 | unpause: "Activate", 11 | cancel: "Cancel" 12 | } 13 | 14 | USER_DEFAULT_CANCELLATION_REASON = "Cancelled By User" 15 | 16 | belongs_to :ship_address, class_name: "Spree::Address" 17 | belongs_to :bill_address, class_name: "Spree::Address" 18 | belongs_to :parent_order, class_name: "Spree::Order" 19 | belongs_to :variant, inverse_of: :subscriptions 20 | belongs_to :frequency, foreign_key: :subscription_frequency_id, class_name: "Spree::SubscriptionFrequency" 21 | belongs_to :source, polymorphic: true 22 | 23 | accepts_nested_attributes_for :ship_address, :bill_address 24 | 25 | has_many :orders_subscriptions, class_name: "Spree::OrderSubscription", dependent: :destroy 26 | has_many :orders, through: :orders_subscriptions 27 | has_many :complete_orders, -> { complete }, through: :orders_subscriptions, source: :order 28 | 29 | self.whitelisted_ransackable_associations = %w( parent_order ) 30 | 31 | scope :paused, -> { where(paused: true) } 32 | scope :unpaused, -> { where(paused: false) } 33 | scope :disabled, -> { where(enabled: false) } 34 | scope :active, -> { where(enabled: true) } 35 | scope :not_cancelled, -> { where(cancelled_at: nil) } 36 | scope :with_appropriate_delivery_time, -> { where("next_occurrence_at <= :current_date", current_date: Time.current) } 37 | scope :processable, -> { unpaused.active.not_cancelled } 38 | scope :eligible_for_subscription, -> { processable.with_appropriate_delivery_time } 39 | scope :with_parent_orders, -> (orders) { where(parent_order: orders) } 40 | 41 | with_options allow_blank: true do 42 | validates :price, numericality: { greater_than_or_equal_to: 0 } 43 | validates :quantity, numericality: { greater_than: 0, only_integer: true } 44 | validates :delivery_number, numericality: { greater_than_or_equal_to: :recurring_orders_size, only_integer: true } 45 | validates :parent_order, uniqueness: { scope: :variant } 46 | end 47 | with_options presence: true do 48 | validates :quantity, :delivery_number, :price, :number, :variant, :parent_order, :frequency, :prior_notification_days_gap 49 | validates :cancellation_reasons, :cancelled_at, if: :cancelled 50 | validates :ship_address, :bill_address, :next_occurrence_at, :source, if: :enabled? 51 | end 52 | validate :next_occurrence_at_range, if: :next_occurrence_at 53 | validate :prior_notification_days_gap_value, if: :prior_notification_days_gap 54 | 55 | define_model_callbacks :pause, only: [:before] 56 | before_pause :can_pause? 57 | define_model_callbacks :unpause, only: [:before] 58 | before_unpause :can_unpause?, :set_next_occurrence_at_after_unpause 59 | define_model_callbacks :process, only: [:after] 60 | after_process :notify_reoccurrence, if: :reoccurrence_notifiable? 61 | define_model_callbacks :cancel, only: [:before] 62 | before_cancel :set_cancellation_reason, if: :can_set_cancellation_reason? 63 | 64 | before_validation :set_next_occurrence_at, if: :can_set_next_occurrence_at? 65 | before_validation :set_cancelled_at, if: :can_set_cancelled_at? 66 | before_update :not_cancelled? 67 | before_validation :update_price, on: :update, if: :variant_id_changed? 68 | before_update :next_occurrence_at_not_changed?, if: :paused? 69 | after_update :notify_user, if: :user_notifiable? 70 | after_update :notify_cancellation, if: :cancellation_notifiable? 71 | after_update :update_next_occurrence_at 72 | 73 | def process 74 | if (variant.stock_items.sum(:count_on_hand) >= quantity || variant.stock_items.any? { |stock| stock.backorderable? }) && (!variant.product.discontinued?) 75 | update_column(:next_occurrence_possible, true) 76 | else 77 | update_column(:next_occurrence_possible, false) 78 | end 79 | new_order = recreate_order if (deliveries_remaining? && next_occurrence_possible) 80 | update(next_occurrence_at: next_occurrence_at_value) if new_order.try :completed? 81 | end 82 | 83 | def cancel_with_reason(attributes) 84 | self.cancelled = true 85 | update(attributes) 86 | end 87 | 88 | def cancelled? 89 | !!cancelled_at_was 90 | end 91 | 92 | def number_of_deliveries_left 93 | delivery_number.to_i - complete_orders.size - 1 94 | end 95 | 96 | def pause 97 | run_callbacks :pause do 98 | update_attributes(paused: true) 99 | end 100 | end 101 | 102 | def unpause 103 | run_callbacks :unpause do 104 | update_attributes(paused: false) 105 | end 106 | end 107 | 108 | def cancel 109 | self.cancelled = true 110 | run_callbacks :cancel do 111 | update_attributes(cancelled_at: Time.current) 112 | end 113 | end 114 | 115 | def deliveries_remaining? 116 | number_of_deliveries_left > 0 117 | end 118 | 119 | def not_changeable? 120 | cancelled? || !deliveries_remaining? 121 | end 122 | 123 | def send_prior_notification 124 | if eligible_for_prior_notification? 125 | SubscriptionNotifier.notify_for_next_delivery(self).deliver_later 126 | end 127 | end 128 | 129 | private 130 | 131 | def eligible_for_prior_notification? 132 | (next_occurrence_at.to_date - Time.current.to_date).round == prior_notification_days_gap 133 | end 134 | 135 | def update_price 136 | if valid_variant? 137 | self.price = variant.price 138 | else 139 | self.errors.add(:variant_id, :does_not_belong_to_product) 140 | end 141 | end 142 | 143 | def valid_variant? 144 | variant_was = Spree::Variant.find_by(id: variant_id_was) 145 | variant.present? && variant_was.try(:product_id) == variant.product_id 146 | end 147 | 148 | def set_cancelled_at 149 | self.cancelled_at = Time.current 150 | end 151 | 152 | def set_next_occurrence_at 153 | self.next_occurrence_at = next_occurrence_at_value 154 | end 155 | 156 | def next_occurrence_at_value 157 | deliveries_remaining? ? Time.current + frequency.months_count.month : next_occurrence_at 158 | end 159 | 160 | def can_set_next_occurrence_at? 161 | enabled? && next_occurrence_at.nil? && deliveries_remaining? 162 | end 163 | 164 | def set_next_occurrence_at_after_unpause 165 | self.next_occurrence_at = (Time.current > next_occurrence_at) ? next_occurrence_at + frequency.months_count.month : next_occurrence_at 166 | end 167 | 168 | def can_pause? 169 | enabled? && !cancelled? && deliveries_remaining? && !paused? 170 | end 171 | 172 | def can_unpause? 173 | enabled? && !cancelled? && deliveries_remaining? && paused? 174 | end 175 | 176 | def recreate_order 177 | order = make_new_order 178 | add_variant_to_order(order) 179 | add_shipping_address(order) 180 | add_delivery_method_to_order(order) 181 | add_shipping_costs_to_order(order) 182 | add_payment_method_to_order(order) 183 | confirm_order(order) 184 | order 185 | end 186 | 187 | def make_new_order 188 | orders.create(order_attributes) 189 | end 190 | 191 | def add_variant_to_order(order) 192 | order.contents.add(variant, quantity) 193 | order.next 194 | end 195 | 196 | def add_shipping_address(order) 197 | order.ship_address = ship_address.clone 198 | order.bill_address = bill_address.clone 199 | order.next 200 | end 201 | 202 | # select shipping method which was selected in original order. 203 | def add_delivery_method_to_order(order) 204 | selected_shipping_method_id = parent_order.inventory_units.where(variant_id: variant.id).first.shipment.shipping_method.id 205 | 206 | order.shipments.each do |shipment| 207 | current_shipping_rate = shipment.shipping_rates.find_by(selected: true) 208 | proposed_shipping_rate = shipment.shipping_rates.find_by(shipping_method_id: selected_shipping_method_id) 209 | 210 | if proposed_shipping_rate.present? && current_shipping_rate != proposed_shipping_rate 211 | current_shipping_rate.update(selected: false) 212 | proposed_shipping_rate.update(selected: true) 213 | end 214 | end 215 | 216 | order.next 217 | end 218 | 219 | def add_shipping_costs_to_order(order) 220 | order.set_shipments_cost 221 | end 222 | 223 | def add_payment_method_to_order(order) 224 | if order.payments.exists? 225 | order.payments.first.update(source: source, payment_method: source.payment_method) 226 | else 227 | order.payments.create(source: source, payment_method: source.payment_method, amount: order.total) 228 | end 229 | order.next 230 | end 231 | 232 | def confirm_order(order) 233 | order.next 234 | end 235 | 236 | def order_attributes 237 | { 238 | currency: parent_order.currency, 239 | guest_token: parent_order.guest_token, 240 | store: parent_order.store, 241 | user: parent_order.user, 242 | created_by: parent_order.user, 243 | last_ip_address: parent_order.last_ip_address 244 | } 245 | end 246 | 247 | def notify_user 248 | SubscriptionNotifier.notify_confirmation(self).deliver_later 249 | end 250 | 251 | def not_cancelled? 252 | !cancelled? 253 | end 254 | 255 | def can_set_cancelled_at? 256 | cancelled.present? && deliveries_remaining? 257 | end 258 | 259 | def set_cancellation_reason 260 | self.cancellation_reasons = USER_DEFAULT_CANCELLATION_REASON 261 | end 262 | 263 | def can_set_cancellation_reason? 264 | cancelled.present? && deliveries_remaining? && cancellation_reasons.nil? 265 | end 266 | 267 | def notify_cancellation 268 | SubscriptionNotifier.notify_cancellation(self).deliver_later 269 | end 270 | 271 | def cancellation_notifiable? 272 | cancelled_at.present? && cancelled_at_changed? 273 | end 274 | 275 | def reoccurrence_notifiable? 276 | next_occurrence_at_changed? && !!next_occurrence_at_was 277 | end 278 | 279 | def notify_reoccurrence 280 | SubscriptionNotifier.notify_reoccurrence(self).deliver_later 281 | end 282 | 283 | def recurring_orders_size 284 | complete_orders.size + 1 285 | end 286 | 287 | def user_notifiable? 288 | enabled? && enabled_changed? 289 | end 290 | 291 | def next_occurrence_at_not_changed? 292 | !next_occurrence_at_changed? 293 | end 294 | 295 | def next_occurrence_at_range 296 | unless next_occurrence_at >= Time.current.to_date 297 | errors.add(:next_occurrence_at, Spree.t('subscriptions.error.out_of_range')) 298 | end 299 | end 300 | 301 | def update_next_occurrence_at 302 | update_column(:next_occurrence_at, next_occurrence_at_value) 303 | end 304 | 305 | def prior_notification_days_gap_value 306 | return if next_occurrence_at_value.nil? 307 | 308 | if Time.current + prior_notification_days_gap.days >= next_occurrence_at_value 309 | errors.add(:prior_notification_days_gap, Spree.t('subscriptions.error.should_be_earlier_than_next_delivery')) 310 | end 311 | end 312 | end 313 | end 314 | -------------------------------------------------------------------------------- /spec/controllers/spree/admin/subscriptions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Admin::SubscriptionsController, type: :controller do 4 | 5 | stub_authorization! 6 | 7 | let(:active_subscription) { mock_model(Spree::Subscription, id: 1, enabled: true, next_occurrence_at: Time.current) } 8 | let(:cancelled_subscription) { mock_model(Spree::Subscription, id: 2, cancelled_at: Time.current, cancellation_reasons: "Test") } 9 | 10 | describe "#cancellation" do 11 | def do_cancellation params 12 | spree_get :cancellation, params 13 | end 14 | 15 | let(:params) { { id: active_subscription.id } } 16 | 17 | before do 18 | allow(Spree::Subscription).to receive(:find).and_return(active_subscription) 19 | allow(active_subscription).to receive(:cancelled?).and_return(false) 20 | end 21 | 22 | context "expects to receive" do 23 | after { do_cancellation params } 24 | it { expect(Spree::Subscription).to receive(:find).with(params[:id].to_s).and_return(active_subscription) } 25 | it { expect(active_subscription).to receive(:cancelled?).and_return(false) } 26 | end 27 | 28 | context "response" do 29 | before { do_cancellation params } 30 | it { expect(response).to have_http_status 200 } 31 | it { expect(response).to render_template :cancellation } 32 | end 33 | end 34 | 35 | describe "pause" do 36 | def do_pause 37 | spree_post :pause, format: :json, id: active_subscription.id 38 | end 39 | 40 | before do 41 | allow(Spree::Subscription).to receive(:find).and_return(active_subscription) 42 | allow(active_subscription).to receive(:cancelled?).and_return(false) 43 | end 44 | 45 | describe "when pause returns success" do 46 | before do 47 | allow(active_subscription).to receive(:pause).and_return(true) 48 | end 49 | 50 | describe "expects to receive" do 51 | after { do_pause } 52 | it { expect(Spree::Subscription).to receive(:find).and_return(active_subscription) } 53 | it { expect(active_subscription).to receive(:cancelled?).and_return(false) } 54 | it { expect(active_subscription).to receive(:pause).and_return(true) } 55 | end 56 | 57 | describe "response" do 58 | before { do_pause } 59 | it { expect(response).to have_http_status 200 } 60 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("admin.subscriptions.pause.success") } 61 | end 62 | end 63 | 64 | describe "when pause is not successful" do 65 | before do 66 | allow(active_subscription).to receive(:pause).and_return(false) 67 | end 68 | 69 | describe "expects to receive" do 70 | after { do_pause } 71 | it { expect(Spree::Subscription).to receive(:find).and_return(active_subscription) } 72 | it { expect(active_subscription).to receive(:cancelled?).and_return(false) } 73 | it { expect(active_subscription).to receive(:pause).and_return(false) } 74 | end 75 | 76 | describe "response" do 77 | before { do_pause } 78 | it { expect(response).to have_http_status 422 } 79 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("admin.subscriptions.pause.error") } 80 | end 81 | end 82 | end 83 | 84 | describe "unpause" do 85 | def do_unpause 86 | spree_post :unpause, format: :json, id: active_subscription.id 87 | end 88 | 89 | before do 90 | allow(Spree::Subscription).to receive(:find).and_return(active_subscription) 91 | allow(active_subscription).to receive(:cancelled?).and_return(false) 92 | end 93 | 94 | describe "when unpause returns success" do 95 | before do 96 | allow(active_subscription).to receive(:unpause).and_return(true) 97 | end 98 | 99 | describe "expects to receive" do 100 | after { do_unpause } 101 | it { expect(Spree::Subscription).to receive(:find).and_return(active_subscription) } 102 | it { expect(active_subscription).to receive(:cancelled?).and_return(false) } 103 | it { expect(active_subscription).to receive(:unpause).and_return(true) } 104 | end 105 | 106 | describe "response" do 107 | before { do_unpause } 108 | it { expect(response).to have_http_status 200 } 109 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("admin.subscriptions.unpause.success", next_occurrence_at: active_subscription.next_occurrence_at.to_date.to_formatted_s(:rfc822)) } 110 | end 111 | end 112 | 113 | describe "when unpause is not successful" do 114 | before do 115 | allow(active_subscription).to receive(:unpause).and_return(false) 116 | end 117 | 118 | describe "expects to receive" do 119 | after { do_unpause } 120 | it { expect(Spree::Subscription).to receive(:find).and_return(active_subscription) } 121 | it { expect(active_subscription).to receive(:cancelled?).and_return(false) } 122 | it { expect(active_subscription).to receive(:unpause).and_return(false) } 123 | end 124 | 125 | describe "response" do 126 | before { do_unpause } 127 | it { expect(response).to have_http_status 422 } 128 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("admin.subscriptions.unpause.error") } 129 | end 130 | end 131 | end 132 | 133 | def do_cancel params 134 | spree_post :cancel, params 135 | end 136 | 137 | describe "#Cancel" do 138 | context "when cancel_with_reason returns true" do 139 | 140 | let(:params) { { id: active_subscription.id, subscription: { cancellation_reasons: "Test" } } } 141 | 142 | before do 143 | allow(Spree::Subscription).to receive(:find).and_return(active_subscription) 144 | allow(controller).to receive(:cancel_subscription_attributes).and_return(params[:subscription]) 145 | allow(active_subscription).to receive(:cancel_with_reason).and_return(true) 146 | allow(active_subscription).to receive(:cancelled?).and_return(false) 147 | end 148 | 149 | context "expects to receive" do 150 | after { do_cancel params } 151 | it { expect(Spree::Subscription).to receive(:find).with(params[:id].to_s).and_return(active_subscription) } 152 | it { expect(controller).to receive(:cancel_subscription_attributes).and_call_original } 153 | it { expect(active_subscription).to receive(:cancel_with_reason).with(controller.send :cancel_subscription_attributes).and_return(true) } 154 | it { expect(active_subscription).to receive(:cancelled?).and_return(false) } 155 | end 156 | 157 | context "response" do 158 | before { do_cancel params } 159 | it { expect(response).to have_http_status 302 } 160 | it { expect(response).to redirect_to controller.send :collection_url } 161 | it { expect(flash[:success]).to eq I18n.t("spree.admin.subscriptions.cancel.success") } 162 | end 163 | end 164 | 165 | context "when cancel_with_reason returns false" do 166 | 167 | let(:params) { { id: active_subscription.id, subscription: { cancellation_reasons: nil } } } 168 | 169 | before do 170 | allow(Spree::Subscription).to receive(:find).and_return(active_subscription) 171 | allow(active_subscription).to receive(:cancelled?).and_return(false) 172 | allow(controller).to receive(:cancel_subscription_attributes).and_return(params[:subscription]) 173 | allow(active_subscription).to receive(:cancel_with_reason).and_return(false) 174 | end 175 | 176 | context "expects to receive" do 177 | after { do_cancel params } 178 | it { expect(Spree::Subscription).to receive(:find).with(params[:id].to_s).and_return(active_subscription) } 179 | it { expect(active_subscription).to receive(:cancelled?).and_return(false) } 180 | it { expect(controller).to receive(:cancel_subscription_attributes).and_call_original } 181 | it { expect(active_subscription).to receive(:cancel_with_reason).with(controller.send :cancel_subscription_attributes).and_return(false) } 182 | end 183 | 184 | context "response" do 185 | before { do_cancel params } 186 | it { expect(response).to have_http_status 200 } 187 | it { expect(response).to render_template :cancellation } 188 | end 189 | end 190 | end 191 | 192 | describe "callbacks" do 193 | describe "#ensure_not_cancelled" do 194 | def do_cancellation params 195 | spree_get :cancellation, params 196 | end 197 | 198 | context "when subscription is cancelled" do 199 | 200 | let(:params) { { id: cancelled_subscription.id } } 201 | 202 | before do 203 | allow(Spree::Subscription).to receive(:find).and_return(cancelled_subscription) 204 | allow(cancelled_subscription).to receive(:cancelled?).and_return(true) 205 | end 206 | 207 | context "expects to receive" do 208 | after { do_cancellation params } 209 | it { expect(Spree::Subscription).to receive(:find).with(params[:id].to_s).and_return(cancelled_subscription) } 210 | it { expect(cancelled_subscription).to receive(:cancelled?).and_return(true) } 211 | end 212 | 213 | context "response" do 214 | before { do_cancellation params } 215 | it { expect(response).to have_http_status 302 } 216 | it { expect(response).to redirect_to controller.send :collection_url } 217 | it { expect(flash[:error]).to eq I18n.t("spree.admin.subscriptions.error_on_already_cancelled") } 218 | end 219 | end 220 | 221 | context "when subscription is not cancelled" do 222 | let(:params) { { id: active_subscription.id } } 223 | 224 | before do 225 | allow(Spree::Subscription).to receive(:find).and_return(active_subscription) 226 | allow(active_subscription).to receive(:cancelled?).and_return(false) 227 | end 228 | 229 | context "expects to receive" do 230 | after { do_cancellation params } 231 | it { expect(Spree::Subscription).to receive(:find).with(params[:id].to_s).and_return(active_subscription) } 232 | it { expect(active_subscription).to receive(:cancelled?).and_return(false) } 233 | end 234 | 235 | context "response" do 236 | before { do_cancellation params } 237 | it { expect(response).to have_http_status 200 } 238 | it { expect(response).to render_template :cancellation } 239 | end 240 | end 241 | end 242 | 243 | describe "#collection" do 244 | def do_index 245 | spree_get :index 246 | end 247 | 248 | let(:subscriptions) { double(ActiveRecord::Relation) } 249 | let(:search_subscriptions) { double(Ransack::Search) } 250 | let(:result_subscriptions) { double(ActiveRecord::Relation) } 251 | 252 | before do 253 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 254 | allow(subscriptions).to receive(:ransack).and_return(search_subscriptions) 255 | allow(search_subscriptions).to receive(:result).and_return(result_subscriptions) 256 | allow(result_subscriptions).to receive(:includes).and_return(result_subscriptions) 257 | allow(result_subscriptions).to receive(:references).and_return(result_subscriptions) 258 | allow(result_subscriptions).to receive(:order).and_return(result_subscriptions) 259 | allow(result_subscriptions).to receive(:page).and_return(result_subscriptions) 260 | end 261 | 262 | context "expects to receive" do 263 | after { do_index } 264 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 265 | it { expect(subscriptions).to receive(:ransack).with(controller.params[:q]).and_return(search_subscriptions) } 266 | it { expect(search_subscriptions).to receive(:result).and_return(result_subscriptions) } 267 | it { expect(result_subscriptions).to receive(:includes).with(:frequency, :complete_orders, variant: :product).and_return(result_subscriptions) } 268 | it { expect(result_subscriptions).to receive(:references).with(:complete_orders).and_return(result_subscriptions) } 269 | it { expect(result_subscriptions).to receive(:order).with(created_at: :desc).and_return(result_subscriptions) } 270 | it { expect(result_subscriptions).to receive(:page).with(controller.params[:page]).and_return(result_subscriptions) } 271 | end 272 | 273 | context "assigns" do 274 | before { do_index } 275 | it { expect(assigns(:search)).to eq search_subscriptions } 276 | it { expect(assigns(:collection)).to eq result_subscriptions } 277 | end 278 | end 279 | end 280 | 281 | end 282 | -------------------------------------------------------------------------------- /spec/controllers/spree/subscriptions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spree::SubscriptionsController, type: :controller do 4 | 5 | stub_authorization! 6 | 7 | let(:active_subscription) { mock_model(Spree::Subscription, id: 1, enabled: true, next_occurrence_at: Time.current) } 8 | let(:cancelled_subscription) { mock_model(Spree::Subscription, id: 2, cancelled_at: Time.current, cancellation_reasons: "Test", enabled: true) } 9 | let(:subscriptions) { double(ActiveRecord::Relation) } 10 | 11 | describe "Callbacks" do 12 | def do_cancel params 13 | spree_post :cancel, params 14 | end 15 | 16 | describe "#ensure_subscription" do 17 | context "html request" do 18 | context "when subscription is present" do 19 | let(:params) { { id: active_subscription.id } } 20 | 21 | before do 22 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 23 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 24 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 25 | allow(active_subscription).to receive(:cancel).and_return(true) 26 | end 27 | 28 | context "expects to receive" do 29 | after { do_cancel params } 30 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 31 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 32 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 33 | it { expect(active_subscription).to receive(:cancel).and_return(true) } 34 | end 35 | 36 | context "assigns" do 37 | before { do_cancel params } 38 | it { expect(response).to have_http_status 302 } 39 | it { expect(flash[:error]).to be_nil } 40 | end 41 | end 42 | 43 | context "when subscription is not present" do 44 | let(:params) { { id: "" } } 45 | 46 | before do 47 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 48 | allow(subscriptions).to receive(:find_by).and_return(nil) 49 | end 50 | 51 | context "expects to receive" do 52 | after { do_cancel params } 53 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 54 | it { expect(subscriptions).to receive(:find_by).and_return(nil) } 55 | end 56 | 57 | context "assigns" do 58 | before { do_cancel params } 59 | it { expect(response).to have_http_status 302 } 60 | it { expect(response).to redirect_to account_path } 61 | it { expect(flash[:error]).to eq Spree.t("subscriptions.alert.missing") } 62 | end 63 | end 64 | end 65 | 66 | context "json request" do 67 | context "when subscription is present" do 68 | let(:params) { { id: active_subscription.id, format: :json } } 69 | 70 | before do 71 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 72 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 73 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 74 | allow(active_subscription).to receive(:cancel).and_return(true) 75 | end 76 | 77 | context "expects to receive" do 78 | after { do_cancel params } 79 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 80 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 81 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 82 | it { expect(active_subscription).to receive(:cancel).and_return(true) } 83 | end 84 | 85 | context "assigns" do 86 | before { do_cancel params } 87 | it { expect(response).to have_http_status 200 } 88 | it { expect(JSON.parse(response.body)["flash"]).to_not eq Spree.t("subscriptions.alert.missing") } 89 | end 90 | end 91 | 92 | context "when subscription is not present" do 93 | let(:params) { { id: "", format: :json } } 94 | 95 | before do 96 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 97 | allow(subscriptions).to receive(:find_by).and_return(nil) 98 | end 99 | 100 | context "expects to receive" do 101 | after { do_cancel params } 102 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 103 | it { expect(subscriptions).to receive(:find_by).and_return(nil) } 104 | end 105 | 106 | context "assigns" do 107 | before { do_cancel params } 108 | it { expect(response).to have_http_status 422 } 109 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("subscriptions.alert.missing") } 110 | end 111 | end 112 | end 113 | end 114 | 115 | describe "#ensure_not_cancelled" do 116 | context "html request" do 117 | context "when subscription is cancelled" do 118 | let(:params) { { id: cancelled_subscription.id } } 119 | 120 | before do 121 | request.env["HTTP_REFERER"] = account_path 122 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 123 | allow(subscriptions).to receive(:find_by).and_return(cancelled_subscription) 124 | allow(cancelled_subscription).to receive(:not_changeable?).and_return(true) 125 | end 126 | 127 | context "expects to receive" do 128 | after { do_cancel params } 129 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 130 | it { expect(subscriptions).to receive(:find_by).and_return(cancelled_subscription) } 131 | it { expect(cancelled_subscription).to receive(:not_changeable?).and_return(true) } 132 | end 133 | 134 | context "response" do 135 | before { do_cancel params } 136 | it { expect(response).to have_http_status 302 } 137 | it { expect(response).to redirect_to account_path } 138 | it { expect(flash[:error]).to eq Spree.t("subscriptions.error.not_changeable") } 139 | end 140 | end 141 | 142 | context "when subscription is not cancelled" do 143 | let(:params) { { id: active_subscription.id } } 144 | 145 | before do 146 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 147 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 148 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 149 | allow(active_subscription).to receive(:cancel).and_return(true) 150 | end 151 | 152 | context "expects to receive" do 153 | after { do_cancel params } 154 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 155 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 156 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 157 | it { expect(active_subscription).to receive(:cancel).and_return(true) } 158 | end 159 | 160 | context "response" do 161 | before { do_cancel params } 162 | it { expect(response).to have_http_status 302 } 163 | it { expect(flash[:error]).to be_nil } 164 | end 165 | end 166 | end 167 | 168 | context "json request" do 169 | context "when subscription is cancelled" do 170 | let(:params) { { id: cancelled_subscription.id, format: :json } } 171 | 172 | before do 173 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 174 | allow(subscriptions).to receive(:find_by).and_return(cancelled_subscription) 175 | allow(cancelled_subscription).to receive(:not_changeable?).and_return(true) 176 | end 177 | 178 | context "expects to receive" do 179 | after { do_cancel params } 180 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 181 | it { expect(subscriptions).to receive(:find_by).and_return(cancelled_subscription) } 182 | it { expect(cancelled_subscription).to receive(:not_changeable?).and_return(true) } 183 | end 184 | 185 | context "response" do 186 | before { do_cancel params } 187 | it { expect(response).to have_http_status 422 } 188 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("subscriptions.error.not_changeable") } 189 | end 190 | end 191 | 192 | context "when subscription is not cancelled" do 193 | let(:params) { { id: active_subscription.id, format: :json } } 194 | 195 | before do 196 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 197 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 198 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 199 | allow(active_subscription).to receive(:cancel).and_return(true) 200 | end 201 | 202 | context "expects to receive" do 203 | after { do_cancel params } 204 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 205 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 206 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 207 | it { expect(active_subscription).to receive(:cancel).and_return(true) } 208 | end 209 | 210 | context "response" do 211 | before { do_cancel params } 212 | it { expect(response).to have_http_status 200 } 213 | it { expect(JSON.parse(response.body)["flash"]).to_not eq Spree.t("subscriptions.error.not_changeable") } 214 | end 215 | end 216 | end 217 | end 218 | end 219 | 220 | describe "edit" do 221 | def do_edit params 222 | spree_get :edit, params 223 | end 224 | 225 | it { is_expected.to use_before_action(:ensure_subscription_belongs_to_user) } 226 | 227 | describe "when subscription is found" do 228 | before do 229 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 230 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 231 | end 232 | 233 | describe "expects to receive" do 234 | after { do_edit({ id: active_subscription.id }) } 235 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 236 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 237 | end 238 | 239 | describe "response" do 240 | before { do_edit({ id: active_subscription.id }) } 241 | it { expect(response).to have_http_status 200 } 242 | it { expect(response).to render_template :edit } 243 | end 244 | end 245 | 246 | describe "when subscription is not found" do 247 | before do 248 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 249 | allow(subscriptions).to receive(:find_by).and_return(nil) 250 | end 251 | 252 | describe "expects to receive" do 253 | after { do_edit({ id: "" }) } 254 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 255 | it { expect(subscriptions).to receive(:find_by).and_return(nil) } 256 | end 257 | 258 | describe "response" do 259 | before { do_edit({ id: "" }) } 260 | it { expect(response).to have_http_status 302 } 261 | it { expect(response).to redirect_to account_path } 262 | it { expect(flash[:error]).to eq Spree.t("subscriptions.alert.missing") } 263 | end 264 | end 265 | 266 | describe '#ensure_subscription_belongs_to_user' do 267 | let(:user) { create(:user) } 268 | let(:order) { create(:completed_order_with_totals) } 269 | let(:subscription) { create(:valid_subscription, enabled: true, parent_order: order, next_occurrence_at: Time.current) } 270 | 271 | it "is expected to authorize user's ability to update subscription" do 272 | expect(controller).to receive(:authorize!).with(:update, subscription) 273 | end 274 | 275 | after { do_edit({ id: subscription.id }) } 276 | end 277 | end 278 | 279 | describe "update" do 280 | def do_update params 281 | spree_put :update, params 282 | end 283 | 284 | describe "when subscription is found" do 285 | 286 | let(:params) { { id: active_subscription.id, subscription: { quantity: 2 } } } 287 | 288 | describe "when subscription is successfully updated" do 289 | before do 290 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 291 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 292 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 293 | allow(controller).to receive(:subscription_attributes).and_return(params[:subscription]) 294 | allow(active_subscription).to receive(:update).and_return(true) 295 | end 296 | 297 | describe "expects to receive" do 298 | after { do_update(params) } 299 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 300 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 301 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 302 | it { expect(controller).to receive(:subscription_attributes).and_call_original } 303 | it { expect(active_subscription).to receive(:update).with(controller.send :subscription_attributes).and_return(true) } 304 | end 305 | 306 | describe "response" do 307 | context 'when request.json?' do 308 | before { do_update(params.merge(format: :json)) } 309 | it { expect(response).to have_http_status 200 } 310 | it { expect(response.body['subscription']).not_to be_nil } 311 | end 312 | context 'when request.html?' do 313 | before { do_update(params) } 314 | it { expect(response).to have_http_status 302 } 315 | it { expect(response).to redirect_to edit_subscription_path(active_subscription) } 316 | it { expect(flash[:success]).to eq Spree.t("subscriptions.update.success") } 317 | end 318 | end 319 | end 320 | 321 | describe "when subscription is not successfully updated" do 322 | before do 323 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 324 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 325 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 326 | allow(controller).to receive(:subscription_attributes).and_return(params[:subscription]) 327 | allow(active_subscription).to receive(:update).and_return(false) 328 | end 329 | 330 | describe "expects to receive" do 331 | after { do_update(params) } 332 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 333 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 334 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 335 | it { expect(controller).to receive(:subscription_attributes).and_call_original } 336 | it { expect(active_subscription).to receive(:update).with(controller.send :subscription_attributes).and_return(false) } 337 | end 338 | 339 | describe "response" do 340 | context 'when request.json?' do 341 | before { do_update(params.merge(format: :json)) } 342 | it { expect(response).to have_http_status 422 } 343 | it { expect(response.body['errors']).not_to be_nil } 344 | end 345 | context 'when request.html?' do 346 | before { do_update(params) } 347 | it { expect(response).to have_http_status 200 } 348 | it { expect(response).to render_template :edit } 349 | end 350 | end 351 | end 352 | end 353 | 354 | describe "when subscription is not found" do 355 | before do 356 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 357 | allow(subscriptions).to receive(:find_by).and_return(nil) 358 | end 359 | 360 | describe "expects to receive" do 361 | after { do_update({ id: "" }) } 362 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 363 | it { expect(subscriptions).to receive(:find_by).and_return(nil) } 364 | end 365 | 366 | describe "response" do 367 | before { do_update({ id: "" }) } 368 | it { expect(response).to have_http_status 302 } 369 | it { expect(response).to redirect_to account_path } 370 | it { expect(flash[:error]).to eq Spree.t("subscriptions.alert.missing") } 371 | end 372 | end 373 | end 374 | 375 | describe "pause" do 376 | def do_pause 377 | spree_post :pause, format: :json, id: active_subscription.id 378 | end 379 | 380 | before do 381 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 382 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 383 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 384 | end 385 | 386 | describe "when pause returns success" do 387 | before do 388 | allow(active_subscription).to receive(:pause).and_return(true) 389 | end 390 | 391 | describe "expects to receive" do 392 | after { do_pause } 393 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 394 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 395 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 396 | it { expect(active_subscription).to receive(:pause).and_return(true) } 397 | end 398 | 399 | describe "response" do 400 | before { do_pause } 401 | it { expect(response).to have_http_status 200 } 402 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("subscriptions.pause.success") } 403 | end 404 | end 405 | 406 | describe "when pause is not successful" do 407 | before do 408 | allow(active_subscription).to receive(:pause).and_return(false) 409 | end 410 | 411 | describe "expects to receive" do 412 | after { do_pause } 413 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 414 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 415 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 416 | it { expect(active_subscription).to receive(:pause).and_return(false) } 417 | end 418 | 419 | describe "response" do 420 | before { do_pause } 421 | it { expect(response).to have_http_status 422 } 422 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("subscriptions.pause.error") } 423 | end 424 | end 425 | end 426 | 427 | describe "unpause" do 428 | def do_unpause 429 | spree_post :unpause, format: :json, id: active_subscription.id 430 | end 431 | 432 | before do 433 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 434 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 435 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 436 | end 437 | 438 | describe "when unpause returns success" do 439 | before do 440 | allow(active_subscription).to receive(:unpause).and_return(true) 441 | end 442 | 443 | describe "expects to receive" do 444 | after { do_unpause } 445 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 446 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 447 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 448 | it { expect(active_subscription).to receive(:unpause).and_return(true) } 449 | end 450 | 451 | describe "response" do 452 | before { do_unpause } 453 | it { expect(response).to have_http_status 200 } 454 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("subscriptions.unpause.success", next_occurrence_at: active_subscription.next_occurrence_at.to_date.to_formatted_s(:rfc822)) } 455 | end 456 | end 457 | 458 | describe "when unpause is not successful" do 459 | before do 460 | allow(active_subscription).to receive(:unpause).and_return(false) 461 | end 462 | 463 | describe "expects to receive" do 464 | after { do_unpause } 465 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 466 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 467 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 468 | it { expect(active_subscription).to receive(:unpause).and_return(false) } 469 | end 470 | 471 | describe "response" do 472 | before { do_unpause } 473 | it { expect(response).to have_http_status 422 } 474 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("subscriptions.unpause.error") } 475 | end 476 | end 477 | end 478 | 479 | describe "cancel" do 480 | describe "html response" do 481 | def do_cancel params 482 | spree_post :cancel, params 483 | end 484 | 485 | before do 486 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 487 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 488 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 489 | end 490 | 491 | describe "when subscription cancel is successful" do 492 | before do 493 | allow(active_subscription).to receive(:cancel).and_return(true) 494 | end 495 | 496 | describe "expects to receive" do 497 | after { do_cancel({ id: active_subscription.id }) } 498 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 499 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 500 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 501 | it { expect(active_subscription).to receive(:cancel).and_return(true) } 502 | end 503 | 504 | describe "response" do 505 | before { do_cancel({ id: active_subscription.id }) } 506 | it { expect(response).to have_http_status 302 } 507 | it { expect(response).to redirect_to edit_subscription_path(active_subscription) } 508 | it { expect(flash[:success]).to eq Spree.t("subscriptions.cancel.success") } 509 | end 510 | end 511 | 512 | describe "when subscription cancel is not successful" do 513 | before do 514 | allow(active_subscription).to receive(:cancel).and_return(false) 515 | end 516 | 517 | describe "expects to receive" do 518 | after { do_cancel({ id: active_subscription.id }) } 519 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 520 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 521 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 522 | it { expect(active_subscription).to receive(:cancel).and_return(false) } 523 | end 524 | 525 | describe "response" do 526 | before { do_cancel({ id: active_subscription.id }) } 527 | it { expect(response).to have_http_status 302 } 528 | it { expect(response).to redirect_to edit_subscription_path(active_subscription) } 529 | it { expect(flash[:error]).to eq Spree.t("subscriptions.cancel.error") } 530 | end 531 | end 532 | end 533 | 534 | describe "json response" do 535 | def do_cancel 536 | spree_post :cancel, format: :json, id: active_subscription.id 537 | end 538 | 539 | before do 540 | allow(Spree::Subscription).to receive(:active).and_return(subscriptions) 541 | allow(subscriptions).to receive(:find_by).and_return(active_subscription) 542 | allow(active_subscription).to receive(:not_changeable?).and_return(false) 543 | end 544 | 545 | describe "when cancel returns success" do 546 | before do 547 | allow(active_subscription).to receive(:cancel).and_return(true) 548 | end 549 | 550 | describe "expects to receive" do 551 | after { do_cancel } 552 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 553 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 554 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 555 | it { expect(active_subscription).to receive(:cancel).and_return(true) } 556 | end 557 | 558 | describe "response" do 559 | before { do_cancel } 560 | it { expect(response).to have_http_status 200 } 561 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("subscriptions.cancel.success") } 562 | end 563 | end 564 | 565 | describe "when cancel is not successful" do 566 | before do 567 | allow(active_subscription).to receive(:cancel).and_return(false) 568 | end 569 | 570 | describe "expects to receive" do 571 | after { do_cancel } 572 | it { expect(Spree::Subscription).to receive(:active).and_return(subscriptions) } 573 | it { expect(subscriptions).to receive(:find_by).and_return(active_subscription) } 574 | it { expect(active_subscription).to receive(:not_changeable?).and_return(false) } 575 | it { expect(active_subscription).to receive(:cancel).and_return(false) } 576 | end 577 | 578 | describe "response" do 579 | before { do_cancel } 580 | it { expect(response).to have_http_status 422 } 581 | it { expect(JSON.parse(response.body)["flash"]).to eq Spree.t("subscriptions.cancel.error") } 582 | end 583 | end 584 | end 585 | end 586 | 587 | end 588 | --------------------------------------------------------------------------------