diff --git a/.ruby-version b/.ruby-version index 5859406..2bf1c1c 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.2.3 +2.3.1 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a26d0ca --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +language: ruby + +rvm: + - 2.3.1 + +script: + - bundle exec rake + +before_install: + - "phantomjs --version" + - "export PATH=$PWD/travis_phantomjs/phantomjs-2.1.1-linux-x86_64/bin:$PATH" + - "phantomjs --version" + - "if [ $(phantomjs --version) != '2.1.1' ]; then rm -rf $PWD/travis_phantomjs; mkdir -p $PWD/travis_phantomjs; fi" + - "if [ $(phantomjs --version) != '2.1.1' ]; then wget https://assets.membergetmember.co/software/phantomjs-2.1.1-linux-x86_64.tar.bz2 -O $PWD/travis_phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2; fi" + - "if [ $(phantomjs --version) != '2.1.1' ]; then tar -xvf $PWD/travis_phantomjs/phantomjs-2.1.1-linux-x86_64.tar.bz2 -C $PWD/travis_phantomjs; fi" + - "phantomjs --version" diff --git a/Gemfile b/Gemfile index ccc6604..6429a7a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,11 +1,13 @@ source 'http://rubygems.org' -gem 'solidus', github: 'solidusio/solidus', branch: 'master' +gem 'rake', '< 11.0' +gem 'solidus', github: 'solidusio/solidus', branch: 'v1.4' # Provides basic authentication functionality for testing parts of your engine gem 'solidus_auth_devise', github: 'solidusio/solidus_auth_devise', branch: 'master' gem 'active_model_serializers', '~> 0.8.3' gem 'stripe' gem 'slim-rails' +gem 'deface' group :test do gem 'factory_girl', '4.5.0' @@ -17,6 +19,10 @@ group :test do gem 'guard-rspec', require: false gem 'simplecov', require: false gem 'selenium-webdriver' + gem 'poltergeist' + gem 'capybara-screenshot' + gem 'vcr' + gem 'webmock' end group :development do diff --git a/app/assets/javascripts/spree/backend/spree_subscriptions.js.coffee b/app/assets/javascripts/spree/backend/solidus_subscriptions.js.coffee similarity index 81% rename from app/assets/javascripts/spree/backend/spree_subscriptions.js.coffee rename to app/assets/javascripts/spree/backend/solidus_subscriptions.js.coffee index ca07d4d..afef53c 100644 --- a/app/assets/javascripts/spree/backend/spree_subscriptions.js.coffee +++ b/app/assets/javascripts/spree/backend/solidus_subscriptions.js.coffee @@ -11,7 +11,6 @@ $(document).ready -> subscription_item_id = save.data('subscription-item-id') quantity = parseInt(save.parents('tr').find('input.subscription_item_quantity').val()) - toggleItemEdit() adjustSubscriptionItem(subscription_item_id, quantity) false @@ -21,7 +20,6 @@ $(document).ready -> del = $(this); subscription_item_id = del.data('subscription-item-id'); - toggleItemEdit() deleteSubscriptionItem(subscription_item_id) # handle adding @@ -30,16 +28,16 @@ $(document).ready -> variant = _.find(window.variants, (variant) -> variant.id == variant_id ) - + variantLineItemTemplate = HandlebarsTemplates["variants/line_items_autocomplete_stock"]; $('#stock_details').html variantLineItemTemplate(variant: variant) $('#stock_details').show() $('button.add_variant').click addSubscriptionVariant - # Add some tips - $('.with-tip').powerTip - smartPlacement: true - fadeInTime: 50 - fadeOutTime: 50 - intentPollInterval: 300 + + # handle the tabs + $('.subscription.tabs li > a').click -> + targetTab = $(this).data('target') + $('.subscription.tab-container > div').hide() + $('div#' + targetTab).show() toggleSubscriptionItemEdit = -> @@ -66,9 +64,11 @@ adjustSubscriptionItem = (subscription_item_id, quantity) -> quantity: quantity token: Spree.api_key ).done (msg) -> - show_flash 'success', 'Successfully updated the quantity.' - $('.subscription-item-qty-show').text(quantity) - $('a.edit-subscription-item').trigger 'click' + show_flash 'success', 'Successfully updated the item quantity.' + findSubscriptionItemRow(subscription_item_id) + .find('.subscription-item-qty-show').text(quantity) + findSubscriptionItemRow(subscription_item_id) + .find('a.edit-subscription-item').trigger 'click' deleteSubscriptionItem = (subscription_item_id) -> url = Spree.pathFor('api/subscriptions/' + subscription_id + '/subscription_items/' + subscription_item_id) @@ -101,4 +101,7 @@ adjustSubscriptionItems = (subscription_id, variant_id, quantity) -> subscription_item: variant_id: variant_id quantity: quantity - token: Spree.api_key).done (msg) -> \ No newline at end of file + token: Spree.api_key).done (msg) -> + +findSubscriptionItemRow = (subscription_item_id) -> + $('tr#subscription-item-' + subscription_item_id) diff --git a/app/assets/javascripts/spree/frontend/spree_subscriptions.js.coffee b/app/assets/javascripts/spree/frontend/solidus_subscriptions.js.coffee similarity index 100% rename from app/assets/javascripts/spree/frontend/spree_subscriptions.js.coffee rename to app/assets/javascripts/spree/frontend/solidus_subscriptions.js.coffee diff --git a/app/assets/stylesheets/spree/backend/solidus_subscriptions.css b/app/assets/stylesheets/spree/backend/solidus_subscriptions.css new file mode 100644 index 0000000..4c86b5a --- /dev/null +++ b/app/assets/stylesheets/spree/backend/solidus_subscriptions.css @@ -0,0 +1,5 @@ +/* css for spree_subscription gem specific */ + +.tab-container > div:not(:first-child) { + display: none; +} diff --git a/app/assets/stylesheets/spree/backend/spree_subscriptions.css b/app/assets/stylesheets/spree/backend/spree_subscriptions.css deleted file mode 100644 index b55b2e4..0000000 --- a/app/assets/stylesheets/spree/backend/spree_subscriptions.css +++ /dev/null @@ -1 +0,0 @@ -/* css for spree_subscription gem specific */ \ No newline at end of file diff --git a/app/assets/stylesheets/spree/frontend/spree_subscriptions.css b/app/assets/stylesheets/spree/frontend/solidus_subscriptions.css similarity index 100% rename from app/assets/stylesheets/spree/frontend/spree_subscriptions.css rename to app/assets/stylesheets/spree/frontend/solidus_subscriptions.css diff --git a/app/controllers/spree/admin/subscriptions_controller.rb b/app/controllers/spree/admin/subscriptions_controller.rb index ba1cf21..473e589 100644 --- a/app/controllers/spree/admin/subscriptions_controller.rb +++ b/app/controllers/spree/admin/subscriptions_controller.rb @@ -14,8 +14,9 @@ def new # build subscription addresses user = order.user - @subscription.build_ship_address(order.ship_address.dup.attributes.merge({user_id: user.id})) - @subscription.build_bill_address(order.bill_address.dup.attributes.merge({user_id: user.id})) + non_existing_attributes = Spree::Address.attribute_names - Spree::SubscriptionAddress.dup.attribute_names + @subscription.build_ship_address(order.ship_address.dup.attributes.except(*non_existing_attributes).merge({user_id: user.id})) + @subscription.build_bill_address(order.bill_address.dup.attributes.except(*non_existing_attributes).merge({user_id: user.id})) # build items build_subscription_items(@subscription, order) @@ -38,15 +39,9 @@ def create end def renew - before_failure_count = @object.failure_count - ::GenerateSubscriptionOrder.new(@object).call + SubscriptionRenewalJob.perform_later @subscription.id + flash[:success] = flash_message_for(@object, :being_renewed) - # check if the failure count has increase, that means we have an error - if @object.failure_count > before_failure_count - flash[:error] = flash_message_for(@object, :error_renew) - else - flash[:success] = flash_message_for(@object, :successfully_renewed) - end respond_with(@object) do |format| format.html { redirect_to location_after_save } end @@ -96,7 +91,13 @@ def credit_card end def failures - @subscriptions = Spree::Subscription.active.where('failure_count > 0').order('created_at desc') + params[:q] = { + combinator: 'and', + state_in: ['active', 'renewing'], + failure_count_gt: 0, + s: 'last_renewal_at desc' + } + @subscriptions = collection end def adjust_sku @@ -142,11 +143,12 @@ def require_order_id def build_subscription_from_order(order) attrs = { user_id: order.user.id, + email: order.email, state: 'active', interval: order.subscription_interval, - credit_card_id: order.credit_card_id_if_available + credit_card_id: order.credit_card_id_if_available, } - order.build_subscription(attrs) + order.subscriptions.build(attrs) end def build_subscription_items(subscription, order) diff --git a/app/controllers/spree/api/subscriptions_controller.rb b/app/controllers/spree/api/subscriptions_controller.rb index b0b7b5c..a439a59 100644 --- a/app/controllers/spree/api/subscriptions_controller.rb +++ b/app/controllers/spree/api/subscriptions_controller.rb @@ -3,12 +3,10 @@ module Api class SubscriptionsController < Spree::Api::BaseController before_action :find_subscription, except: [:index] - def self.prepended(base) - base.prepend_after_action :deliver_cancellation_email, only: [:cancel] - base.prepend_after_action :deliver_pause_email, only: [:pause] - # need to touch user so the address list is updated - base.prepend_after_action :touch_user, only: [:update_address, :create_address, :select_address] - end + after_action :deliver_cancellation_email, only: [:cancel] + after_action :deliver_pause_email, only: [:pause] + # need to touch user so the address list is updated + after_action :touch_user, only: [:update_address, :create_address, :select_address] def index render json: current_api_user.subscriptions, @@ -32,6 +30,12 @@ def update end end + def renew + SubscriptionRenewalJob.perform_later @subscription.id + + render_subscription + end + def skip_next_order @subscription.skip_next_order @@ -100,7 +104,7 @@ def select_address # create a new credit card # then assign it to the subscription def create_credit_card - order = @subscription.last_order + order = @subscription.last_completed_order credit_card = nil begin ::Spree::CreditCard.transaction do @@ -165,7 +169,7 @@ def permitted_address_params end def permitted_subscription_attributes - [:interval, :credit_card_id, :email] + [:interval, :credit_card_id, :email, :next_renewal_at] end end end diff --git a/app/jobs/create_subscription_job.rb b/app/jobs/create_subscription_job.rb index 891786e..c3abbcd 100644 --- a/app/jobs/create_subscription_job.rb +++ b/app/jobs/create_subscription_job.rb @@ -36,8 +36,12 @@ def eligible_line_items(order) end def create_subscription_addresses(order, subscription, user) - subscription.create_ship_address!(order.ship_address.dup.attributes.merge({user_id: user.id})) - subscription.create_bill_address!(order.bill_address.dup.attributes.merge({user_id: user.id})) + non_existing_attributes = Spree::Address.attribute_names - Spree::SubscriptionAddress.dup.attribute_names + order_ship_address = order.ship_address.dup.attributes.except(*non_existing_attributes) + order_bill_address = order.bill_address.dup.attributes.except(*non_existing_attributes) + + subscription.create_ship_address!(order_ship_address.merge({user_id: user.id})) + subscription.create_bill_address!(order_bill_address.merge({user_id: user.id})) end def create_subscription_items(eligible_line_items, subscription, interval) diff --git a/app/jobs/subscription_renewal_job.rb b/app/jobs/subscription_renewal_job.rb new file mode 100644 index 0000000..18cd3c0 --- /dev/null +++ b/app/jobs/subscription_renewal_job.rb @@ -0,0 +1,19 @@ +class SubscriptionRenewalJob < ActiveJob::Base + queue_as :default + + def perform(subscription_id) + subscription = Spree::Subscription.find(subscription_id) + subscription.renew! + + before_failure_count = subscription.failure_count + ::GenerateSubscriptionOrder.new(subscription).call + + # check if the failure count has increase, that means we have an error + if subscription.failure_count > before_failure_count + failed_order = subscription.orders.reorder('created_at desc').first + log = SubscriptionLog.find_by_order_id(failed_order.id) + else + subscription.renewed! + end + end +end diff --git a/app/mailers/spree/subscription_mailer.rb b/app/mailers/spree/subscription_mailer.rb index a6cec99..e6e884b 100644 --- a/app/mailers/spree/subscription_mailer.rb +++ b/app/mailers/spree/subscription_mailer.rb @@ -28,7 +28,7 @@ def pause(subscription) def set_default_variables(subject, campaign) @subject = subject @campaign = campaign - @order = @subscription.last_order + @order = @subscription.last_completed_order @to_address = @order.email @from_addess = from_address end diff --git a/app/models/concerns/subscription_state_machine.rb b/app/models/concerns/subscription_state_machine.rb new file mode 100644 index 0000000..ed8aceb --- /dev/null +++ b/app/models/concerns/subscription_state_machine.rb @@ -0,0 +1,44 @@ +module SubscriptionStateMachine + extend ActiveSupport::Concern + included do + class << self + def active + with_states %w(active renewing) + end + end + + state_machine initial: :active do + event(:renew) { transition %i(active renewing) => :renewing } + event(:renewed) { transition renewing: :active } + + after_transition any => :renewing, do: :mark_last_renewal! + after_transition renewing: :active, do: :adjust_next_renewal! + + + event :cancel do + transition all => :cancelled + end + event(:pause) { transition active: :paused } + event(:resume) { transition paused: :active } + + after_transition on: :cancel do |subscription| + subscription.update_attributes(cancelled_at: Time.now) + end + + after_transition on: :pause do |subscription| + subscription.update_attributes(pause_at: Time.now, resume_at: nil) + end + + after_transition on: :resume do |subscription, transition| + resume_at = transition.args.first || Time.now + subscription.update_attributes(resume_at: resume_at) + # if resume is set at a future date, do not unpause + if resume_at.to_date > Date.today + subscription.update_attributes(state: 'paused') + else + subscription.update_attributes(pause_at: nil) + end + end + end + end +end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 1708c1a..a48bf59 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -17,9 +17,8 @@ def line_item_interval_match(line_item, options) line_item.interval == options[:interval] end - def subscription_interval - subscription ? subscription.interval : 4 + subscription ? subscription.interval : 1 end def subscription_products diff --git a/app/models/spree/order_subscription.rb b/app/models/spree/order_subscription.rb new file mode 100644 index 0000000..e304b9e --- /dev/null +++ b/app/models/spree/order_subscription.rb @@ -0,0 +1,7 @@ +module Spree + class OrderSubscription < Spree::Base + self.table_name = :spree_orders_subscriptions + belongs_to :order + belongs_to :subscription + end +end diff --git a/app/models/spree/subscription.rb b/app/models/spree/subscription.rb index 3833886..1d77e70 100644 --- a/app/models/spree/subscription.rb +++ b/app/models/spree/subscription.rb @@ -1,7 +1,8 @@ module Spree class Subscription < ActiveRecord::Base + include SubscriptionStateMachine + has_many :subscription_items, dependent: :destroy, inverse_of: :subscription - has_and_belongs_to_many :orders, join_table: :spree_orders_subscriptions belongs_to :user belongs_to :credit_card alias_attribute :items, :subscription_items @@ -15,6 +16,9 @@ class Subscription < ActiveRecord::Base has_many :subscription_skips, dependent: :destroy, inverse_of: :subscription alias_attribute :skips, :subscription_skips + has_many :order_subscriptions + has_many :orders, through: :order_subscriptions + accepts_nested_attributes_for :ship_address accepts_nested_attributes_for :bill_address @@ -23,12 +27,18 @@ class Subscription < ActiveRecord::Base validates_presence_of :user after_save :reset_failure_count, if: :credit_card_id_changed? + after_create :mark_last_renewal! + after_touch :adjust_next_renewal! class << self def active where(state: 'active') end + def renewing + where(state: 'renewing') + end + def paused where(state: 'paused') end @@ -47,8 +57,7 @@ def good_standing def ready_for_next_order subscriptions = active.with_interval.good_standing.select do |subscription| - last_order = subscription.last_order - next unless last_order + next unless subscription.last_completed_order next if subscription.prepaid? subscription.next_shipment_date.to_date <= Date.today end @@ -66,50 +75,35 @@ def products end def last_shipment_date - last_order.completed_at if last_order + last_completed_order.completed_at if last_completed_order end def next_shipment_date - if skip_order_at - skip_order_at.advance(calc_next_renewal_date) - elsif last_order - last_order.completed_at.advance(calc_next_renewal_date) - end + next_renewal_at end def calc_next_renewal_date - { weeks: interval } + { months: interval } end def active? - self.state == 'active' - end - - def cancelled? - state == 'cancelled' + %w(active renewing).include? state end - def paused? - state == 'paused' - end - - def cancel - update_attribute(:state, 'cancelled') - update_attribute(:cancelled_at, Time.now) + def last_order + orders.reorder('created_at desc').first end - alias_method :cancel!, :cancel - - def last_order - orders.complete.reorder("completed_at desc").first + def last_completed_order + completed_orders.reorder('completed_at desc').first end def last_order_credit_card - last_order.payments.where('amount > 0').where(state: 'completed').last.source + last_completed_order.payments.where('amount > 0').where(state: 'completed').last.source end def last_order_date - orders.first.complete? ? orders.first.completed_at : orders.first.created_at + last_completed_order ? last_completed_order.completed_at : last_order.created_at end def next_order @@ -124,20 +118,23 @@ def created_at next_order end + def last_order_currency + orders.complete.last.currency + end + def create_next_order! # just keeping safe non_existing_attributes = Spree::SubscriptionAddress.dup.attribute_names - Spree::Address.attribute_names - # use subscription's addresses for the new order and email created_order = orders.create!( - user: last_order.user, + user: last_completed_order.user, repeat_order: true, bill_address: Spree::Address.new(bill_address.dup.attributes.except(*non_existing_attributes)), ship_address: Spree::Address.new(ship_address.dup.attributes.except(*non_existing_attributes)), channel: 'subscription', - store: Spree::Store.current + store: last_completed_order.store, + currency: last_completed_order.currency ) - created_order.update_column(:email, email) if email created_order end @@ -194,22 +191,13 @@ def clear_skip_order alias_attribute :can_skip?, :can_skip - def pause - update_attributes(pause_at: Time.now, resume_at: nil, state: 'paused') - end - - def resume(resume_at_date = Time.now) - update_attributes(resume_at: resume_at_date) - update_attributes(pause_at: nil, state: 'active') if (resume_at_date.to_date <= Date.today) - end - def completed_orders orders.complete end # fetch the last completed order shipment def shipment - last_order.shipments.last + last_completed_order.shipments.last end # fetch the last completed order shipping method @@ -244,5 +232,19 @@ def as_json(options = { }) })) end + private + + def mark_last_renewal! + touch(:last_renewal_at) + end + + def adjust_next_renewal! + return if renewing? + + last_renewal_at = Date.today if last_renewal_at.nil? + + update_column(:next_renewal_at, + last_renewal_at.advance(calc_next_renewal_date)) + end end end diff --git a/app/models/spree/subscription_item.rb b/app/models/spree/subscription_item.rb index 11c133a..58aa042 100644 --- a/app/models/spree/subscription_item.rb +++ b/app/models/spree/subscription_item.rb @@ -5,7 +5,7 @@ class SubscriptionItem < ActiveRecord::Base belongs_to :tax_category, class_name: "Spree::TaxCategory" has_one :product, through: :variant - + before_validation :copy_price before_validation :copy_tax_category @@ -21,7 +21,6 @@ def copy_price if variant self.price = variant.price if price.nil? self.cost_price = variant.cost_price if cost_price.nil? - self.currency = variant.currency if currency.nil? end end diff --git a/app/overrides/admin_orders_edit.rb b/app/overrides/admin_orders_edit.rb index 42fd3e0..76385f0 100644 --- a/app/overrides/admin_orders_edit.rb +++ b/app/overrides/admin_orders_edit.rb @@ -3,10 +3,10 @@ :insert_bottom => ".additional-info", :original => '3a09af526d991bcbb51fcee781d28f7d7cbc981e', :text => ' -
+ <%= @subscription.cancel_feedback %> +
+| <%= l (order.created_at).to_date %> | @@ -56,7 +53,7 @@||||||||||||||
| <%= @subscription.subscription_log_for(order).reason %> | +<%= @subscription.subscription_log_for(order).reason %> | |||||||||||||
| id | -date | -order | -renewal date | -attempts | +ID | +Status | +Last Order | +Order | +Renew Date | +Attempts | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| <%= link_to subscription.id, edit_admin_subscription_path(subscription), target: '_blank' %> - | <%= l(subscription.last_order_date, format: :subscription_date_format) %> - | <%= link_to subscription.orders.first.number, edit_admin_order_path(subscription.orders.first), target: '_blank' %> - | <%=link_to subscription.user.email, edit_admin_user_path(subscription.user), target: '_blank' %> - | <%= l(subscription.next_shipment_date, format: :subscription_date_format) %> - | <%= subscription.failure_count %> + | <%= link_to subscription.id, edit_admin_subscription_path(subscription), target: '_blank' %> | ++ <%= subscription.state %> + | ++ <%= l(subscription.last_order_date, format: :subscription_date_format) %> + | +
+ <% if subscription.last_order %>
+ <%= link_to subscription.last_order.number, edit_admin_order_path(subscription.last_order), target: '_blank' %>
+ <% end %>
+
+ <%=link_to subscription.user.email, edit_admin_user_path(subscription.user), target: '_blank' %>
+ + <% if subscription.active? %> + <%= subscription.subscription_log_for(subscription.last_order)&.reason %> + <% end %> + |
+ <%= l(subscription.next_shipment_date, format: :subscription_date_format) if subscription.can_renew? %> | +<%= subscription.failure_count %> |
| <%= subscription.products.collect(&:name).to_sentence %> | <%= subscription.last_shipment_date %> | <%= subscription.next_shipment_date %> | -<%= t(subscription.state, scope: 'subscription_state', default: t('subscription_state.active')).titleize %> | +<%= Spree.t(subscription.state, scope: 'subscription_state', default: Spree.t('subscription_state.active')).titleize %> |
<% if subscription.active? %>
- <%= button_to t('action.pause'), pause_subscription_path(subscription), method: :put, class: "pause-subscription" %>
+ <%= button_to Spree.t('actions.pause'), pause_subscription_path(subscription), method: :put, class: "pause-subscription" %>
<% else %>
>
<%= form_for subscription, url: resume_subscription_path(subscription), method: :put do |f| %>
<%= f.label(:resume_at, t('resume_at')) %>
<%= f.date_field(:resume_at, value: subscription.resume_at ? subscription.resume_at.to_date : Date.today, min: Date.today) %>
- <%= f.submit(t('action.resume'), class: "resume-subscription") %>
+ <%= f.submit(Spree.t('actions.resume'), class: "resume-subscription") %>
<% end %>
<% if !subscription.resume_at.nil? && !subscription.cancelled? %>
Will be resumed on <%= subscription.resume_at.to_date %>
- <%= tag :input, { type: :button, value: t('action.change'), class: 'change-resume-subscription-on' } %>
+ <%= tag :input, { type: :button, value: Spree.t('actions.change'), class: 'change-resume-subscription-on' } %>
<% end %>
<% end %>
|
<% if !subscription.cancelled? %> - <%= button_to t('action.cancel'), cancel_subscription_path(subscription), method: :put, class: "cancel-subscription" %> + <%= button_to Spree.t('actions.cancel'), cancel_subscription_path(subscription), method: :put, class: "cancel-subscription" %> <% end %> | diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index bbe5bd7..2749ba3 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -13,3 +13,12 @@ Spree.user_class = "Spree::User" Spree::PermittedAttributes.line_item_attributes << :interval + +Spree::Backend::Config.configure do |config| + config.menu_items << config.class::MenuItem.new( + [:subscriptions], + 'clock-o', + partial: 'spree/admin/shared/subscriptions_sub_menu', + url: :admin_subscriptions_path + ) +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 213e441..bd719cc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,11 +2,6 @@ # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. en: - subscribable: 'subscribable' - subscription: "subscription" - subscriptions: "subscriptions" - my_subscriptions: "My subscriptions" - listing_subscriptions: Subscriptions products: "Products" last_shipped_on: "Last Shipment" next_shipment: "Next Shipment" @@ -19,13 +14,15 @@ en: id: "ID" successfully_cancelled: successfully cancelled subscription successfully_renewed: successfully renewed subscription + being_renewed: subscription being renewed error_renew: failed to renew subscription order_number: Order Number requires_order_id: Requires order id subscription_created: subscription created successfully_skipped: subscription next order has been skipped successfully_undo_skip: subscription next order skip has been undone - subscription: Subscription + subscriptions: Subscriptions + my_subscriptions: "My subscriptions" frequency: Frequency editing_subscription: Editing Subscription addl_information: Addl Information @@ -40,12 +37,28 @@ en: back: Back admin: tab: - subscriptions: "Subscriptions" + subscriptions: Subscriptions + failures: Failed Renewals + adjust_sku: Adjust SKUs subscription: + new: New Subscription subscription: Subscription current_items: Current Items orders: Order skips_history: Skips History + cancellation: Cancellation + failures: Failed Renewals + renewing: Renewing + no_subscriptions_found: No Subscriptions Found + + subscription: + cancellation: Cancellation + cancel_reasons: + replenish_often: My products were replenished too often + replenish_seldom: My product were not replensihed often enough + no_longer_want: I no longer want to use the products + want_single_purchase: I want to make single purchases + not_say: I’d prefer not to say activerecord: attributes: diff --git a/config/routes.rb b/config/routes.rb index b1d03d5..3737829 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,6 +20,7 @@ namespace :api, defaults: { format: 'json' } do resources :subscriptions, except: [:create, :new, :destroy] do member do + put :renew put :skip_next_order put :undo_skip_next_order put :pause diff --git a/db/migrate/20150922153455_add_email_to_subscriptions.rb b/db/migrate/20150922153455_add_email_to_subscriptions.rb index 8836107..2acac62 100644 --- a/db/migrate/20150922153455_add_email_to_subscriptions.rb +++ b/db/migrate/20150922153455_add_email_to_subscriptions.rb @@ -3,7 +3,7 @@ def up add_column :spree_subscriptions, :email, :string, default: nil Spree::Subscription.active.each do |subscription| - subscription.update_column(:email, subscription.last_order.email) + subscription.update_column(:email, subscription.last_completed_order.email) end end diff --git a/db/migrate/20160707074723_add_cancel_reason_and_feedback_to_subscriptions.rb b/db/migrate/20160707074723_add_cancel_reason_and_feedback_to_subscriptions.rb new file mode 100644 index 0000000..66ab6cf --- /dev/null +++ b/db/migrate/20160707074723_add_cancel_reason_and_feedback_to_subscriptions.rb @@ -0,0 +1,6 @@ +class AddCancelReasonAndFeedbackToSubscriptions < ActiveRecord::Migration + def change + add_column :spree_subscriptions, :cancel_reasons, :text, array: true, default: [] + add_column :spree_subscriptions, :cancel_feedback, :text + end +end diff --git a/db/migrate/20160711000407_add_renewal_dates_to_subscriptions.rb b/db/migrate/20160711000407_add_renewal_dates_to_subscriptions.rb new file mode 100644 index 0000000..6ad5a48 --- /dev/null +++ b/db/migrate/20160711000407_add_renewal_dates_to_subscriptions.rb @@ -0,0 +1,12 @@ +class AddRenewalDatesToSubscriptions < ActiveRecord::Migration + def change + add_column :spree_subscriptions, :next_renewal_at, :datetime + add_column :spree_subscriptions, :last_renewal_at, :datetime + + Spree::Subscription.active.each do |subscription| + subscription.update_column(:last_renewal_at, + subscription.last_completed_order.completed_at) + subscription.touch + end + end +end diff --git a/db/migrate/20171011152133_add_address3_to_subscription_addresses.rb b/db/migrate/20171011152133_add_address3_to_subscription_addresses.rb new file mode 100644 index 0000000..8ceda45 --- /dev/null +++ b/db/migrate/20171011152133_add_address3_to_subscription_addresses.rb @@ -0,0 +1,5 @@ +class AddAddress3ToSubscriptionAddresses < ActiveRecord::Migration + def change + add_column :spree_subscription_addresses, :address3, :string, limit: 255 + end +end diff --git a/solidus_subscriptions.gemspec b/solidus_subscriptions.gemspec index 5110f55..85f45e3 100644 --- a/solidus_subscriptions.gemspec +++ b/solidus_subscriptions.gemspec @@ -2,13 +2,13 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = 'solidus_subscriptions' - s.version = '1.3.3.beta' + s.version = '2' s.summary = 'A Solidus extension to manage subscribable products.' s.description = """ This Solidus extension enables an e-commerce owner manage products that can be subscribed to, via recurring payments and shipments at set intervals. """ - s.required_ruby_version = '>= 1.8.7' + s.required_ruby_version = '>= 2.3.0' s.author = 'Bryan Mahoney' s.email = 'bryan@godynamo.com' @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.requirements << 'none' - s.add_dependency "solidus", [">= 1.0.6", "< 2"] + s.add_dependency "solidus", [">= 1.4", "< 2"] s.add_development_dependency 'capybara', '~> 2.4' s.add_development_dependency 'coffee-rails' diff --git a/spec/cassettes/Editing_a_subscription_payment_info/can_update_payment_info.yml b/spec/cassettes/Editing_a_subscription_payment_info/can_update_payment_info.yml new file mode 100644 index 0000000..281c5ee --- /dev/null +++ b/spec/cassettes/Editing_a_subscription_payment_info/can_update_payment_info.yml @@ -0,0 +1,171 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.stripe.com/v1/customers + body: + encoding: UTF-8 + string: email=desirae.borer%40green.com + headers: + User-Agent: + - Stripe/v1 RubyBindings/2.1.0 + Authorization: + - Bearer sk_test_jTiNI1BxjFxBr4TUqdHefc1f + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"2.1.0","lang":"ruby","lang_version":"2.3.1 p112 (2016-04-26)","platform":"x86_64-darwin16","engine":"ruby","publisher":"stripe","uname":"Darwin + Hugos-MacBook-Pro.local 16.5.0 Darwin Kernel Version 16.5.0: Fri Mar 3 16:52:33 + PST 2017; root:xnu-3789.51.2~3/RELEASE_X86_64 x86_64","hostname":"Hugos-MacBook-Pro.local"}' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Tue, 11 Apr 2017 15:56:32 GMT + Content-Type: + - application/json + Content-Length: + - '641' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_ASM4kFe9NDHHeK + Stripe-Version: + - '2015-02-18' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "cus_ASM44Uyh4b6zok", + "object": "customer", + "account_balance": 0, + "created": 1491926192, + "currency": null, + "default_source": null, + "delinquent": false, + "description": null, + "discount": null, + "email": "desirae.borer@green.com", + "livemode": false, + "metadata": {}, + "shipping": null, + "sources": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_ASM44Uyh4b6zok/sources" + }, + "subscriptions": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/customers/cus_ASM44Uyh4b6zok/subscriptions" + } + } + http_version: + recorded_at: Tue, 11 Apr 2017 15:56:17 GMT +- request: + method: post + uri: https://api.stripe.com/v1/customers/cus_ASM44Uyh4b6zok/sources + body: + encoding: UTF-8 + string: source[object]=card&source[number]=4242424242424242&source[cvc]=123&source[exp_month]=6&source[exp_year]=2020 + headers: + User-Agent: + - Stripe/v1 RubyBindings/2.1.0 + Authorization: + - Bearer sk_test_jTiNI1BxjFxBr4TUqdHefc1f + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-User-Agent: + - '{"bindings_version":"2.1.0","lang":"ruby","lang_version":"2.3.1 p112 (2016-04-26)","platform":"x86_64-darwin16","engine":"ruby","publisher":"stripe","uname":"Darwin + Hugos-MacBook-Pro.local 16.5.0 Darwin Kernel Version 16.5.0: Fri Mar 3 16:52:33 + PST 2017; root:xnu-3789.51.2~3/RELEASE_X86_64 x86_64","hostname":"Hugos-MacBook-Pro.local"}' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Tue, 11 Apr 2017 15:56:33 GMT + Content-Type: + - application/json + Content-Length: + - '577' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Request-Id: + - req_ASM4q7db4QCCvh + Stripe-Version: + - '2015-02-18' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains + body: + encoding: UTF-8 + string: | + { + "id": "card_1A7RM5LrlCqAeZVWm3KKmS6J", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_ASM44Uyh4b6zok", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 6, + "exp_year": 2020, + "fingerprint": "Nlkrx3hFLJB0Ad6I", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + } + http_version: + recorded_at: Tue, 11 Apr 2017 15:56:17 GMT +recorded_with: VCR 3.0.3 diff --git a/spec/controllers/spree/api/subscriptions_controller_spec.rb b/spec/controllers/spree/api/subscriptions_controller_spec.rb index 3e19cd3..7311391 100644 --- a/spec/controllers/spree/api/subscriptions_controller_spec.rb +++ b/spec/controllers/spree/api/subscriptions_controller_spec.rb @@ -31,18 +31,18 @@ module Spree context "skipping" do it "should skip next order" do - expect(@subscription.next_shipment_date.to_date).to eql 4.weeks.from_now.to_date + expect(next_shipment_date).to eql original_shipment_date api_get :skip_next_order, id: @subscription.id - expect(@subscription.next_shipment_date.to_date).to eql 8.weeks.from_now.to_date + expect(next_shipment_date).to eql next_calc_shipment_date end it "should be able to undo a skip next order" do @subscription.skip_next_order - expect(@subscription.next_shipment_date.to_date).to eql 8.weeks.from_now.to_date + expect(next_shipment_date).to eql next_calc_shipment_date api_get :undo_skip_next_order, id: @subscription.id - expect(@subscription.next_shipment_date.to_date).to eql 4.weeks.from_now.to_date + expect(next_shipment_date).to eql original_shipment_date end end diff --git a/spec/factories/subscription_factory.rb b/spec/factories/subscription_factory.rb index 1978889..6ffee48 100644 --- a/spec/factories/subscription_factory.rb +++ b/spec/factories/subscription_factory.rb @@ -1,8 +1,9 @@ FactoryGirl.define do factory :subscription, :class => Spree::Subscription do - state nil + state 'active' interval 2 - # prepaid false + next_renewal_at 1.month.from_now + # prepaid false ship_address { FactoryGirl.create(:subscription_address) @@ -11,6 +12,10 @@ FactoryGirl.create(:subscription_address) } + orders { + [FactoryGirl.create(:completed_order_with_totals)] + } + association(:user) end end diff --git a/spec/features/admin/adjust_sku_spec.rb b/spec/features/admin/adjust_sku_spec.rb index 4a6b7f6..f1e786b 100644 --- a/spec/features/admin/adjust_sku_spec.rb +++ b/spec/features/admin/adjust_sku_spec.rb @@ -16,7 +16,7 @@ expect(@subscription.subscription_items.first.variant.sku).to eq("GPM100-2") expect(page).to have_content(@subscription.id) expect(page).to have_content(@subscription.email) - expect(page).to have_content(@subscription.last_order.number) + expect(page).to have_content(@subscription.last_completed_order.number) end end diff --git a/spec/features/admin/failures_spec.rb b/spec/features/admin/failures_spec.rb new file mode 100644 index 0000000..f0ee909 --- /dev/null +++ b/spec/features/admin/failures_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe "Subscription Failures", type: :feature do + stub_authorization! + + let!(:failed_subscription) { FactoryGirl.create(:subscription, state: 'renewing', failure_count: 1) } + let!(:cancelled_subscription) { FactoryGirl.create(:subscription, state: 'cancelled', failure_count: 1) } + let!(:active_subscription) { FactoryGirl.create(:subscription) } + + before do + visit spree.failures_admin_subscriptions_path + end + + context "admin subscription renewal failures", js: true do + it "shows the only subscription that failed renewing" do + expect(page).not_to have_content('active') + expect(page).not_to have_content('cancelled') + + expect(page).to have_content('renewing') + end + end +end diff --git a/spec/features/admin/subscription_index_spec.rb b/spec/features/admin/subscription_index_spec.rb index 009b53d..11f45e3 100644 --- a/spec/features/admin/subscription_index_spec.rb +++ b/spec/features/admin/subscription_index_spec.rb @@ -5,17 +5,28 @@ include_context "setup subscriptions for adjusting skus" stub_authorization! + before do + visit spree.admin_subscriptions_path + end + context "admin subscriptions index page", js: true do it "users can filter subscriptions by the SKU of their items" do - visit spree.admin_path - visit spree.admin_subscriptions_path - fill_in("q[subscription_items_variant_sku_eq]", with: "GPM100") + click_button('Filter Results') + expect(page).to have_content(@subscription.id) expect(page).to have_content(@subscription.email) expect(page).to have_content(@subscription.orders.first.number) end - end + it "users can filter by state" do + @subscription.update_column(:state, 'renewing') + + select("Renewing", from: "q[state_eq]") + click_button('Filter Results') + + expect(page).to have_content('renewing') + end + end end diff --git a/spec/jobs/subscription_renewal_job_spec.rb b/spec/jobs/subscription_renewal_job_spec.rb new file mode 100644 index 0000000..9f445a4 --- /dev/null +++ b/spec/jobs/subscription_renewal_job_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe SubscriptionRenewalJob do + include SubscriptionMacros + + let(:user) { create(:user) } + + before(:each) do + setup_subscriptions_for user + end + + it "renews a subscription with a new order" do + subscription = user.subscriptions.first + + expect(user.orders.count).to eq(1) + + SubscriptionRenewalJob.new.perform(subscription.id) + + expect(user.orders.count).to eq(2) + end +end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 943a8d3..f2e8d1c 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -79,7 +79,7 @@ it "returns 4 weeks if it doesn't have previous subscriptions" do order = create(:order, subscriptions: []) - expect(order.subscription_interval).to eq(4) + expect(order.subscription_interval).to eq(1) end end diff --git a/spec/models/spree/subscription_spec.rb b/spec/models/spree/subscription_spec.rb index cb51852..90b5902 100644 --- a/spec/models/spree/subscription_spec.rb +++ b/spec/models/spree/subscription_spec.rb @@ -3,7 +3,7 @@ describe Spree::Subscription do include OrderMacros - it { should have_and_belong_to_many(:orders) } + it { should have_many(:orders) } it { should belong_to(:user) } it { should belong_to(:credit_card)} it { should respond_to(:resume_on)} @@ -36,7 +36,7 @@ end it "should be able to calculate the date of the next shipment" do - @order.subscriptions.last.next_shipment_date.to_i.should == 4.weeks.from_now.to_i + next_shipment_date.should == original_shipment_date end after do @@ -53,12 +53,12 @@ end it "should calculate the correct next shipment date if user decides to skip" do - @order.subscriptions.last.next_shipment_date.to_i.should == 8.weeks.from_now.to_i + next_shipment_date.should == next_calc_shipment_date end it "should fall back to the original shipment date after undoing" do @order.subscriptions.last.undo_skip_next_order - @order.subscriptions.last.next_shipment_date.to_i.should == 4.weeks.from_now.to_i + next_shipment_date.should == original_shipment_date end end @@ -110,7 +110,7 @@ end context "when the subscription has not been cancelled" do - let(:subscription_state) { nil } + let(:subscription_state) { 'active' } it "returns false" do expect(subscription.cancelled?).to be false @@ -119,14 +119,14 @@ end describe "#cancel!" do - let(:subscription) { FactoryGirl.create(:subscription, state: nil) } + let(:subscription) { FactoryGirl.create(:subscription, state: :active) } it "cancels the subscription" do expect { subscription.cancel! }.to change { subscription.state - }.from(nil).to('cancelled') + }.from('active').to('cancelled') end end @@ -137,7 +137,7 @@ subscription.pause }.to change { subscription.state - }.from(nil).to('paused') + }.from('active').to('paused') end end @@ -198,6 +198,24 @@ def create_subscriptions(subscriptions_attr) end end + context '#create_next_order!' do + let(:second_store) { create(:store) } + + before do + create_completed_subscription_order + + subscription.last_order.update_column(:currency, 'CAD') + subscription.last_order.update_column(:store_id, second_store.id) + end + + it "has the last order's currency and store" do + subscription.create_next_order! + + expect(subscription.last_order.currency).to eq 'CAD' + expect(subscription.last_order.store_id).to eq second_store.id + end + end + describe "can_renew?" do let(:subscription) { FactoryGirl.create(:subscription) } diff --git a/spec/requests/store/my_account/edit_subscription_payment_info_spec.rb b/spec/requests/store/my_account/edit_subscription_payment_info_spec.rb index 6f8dcf2..15f5b6f 100644 --- a/spec/requests/store/my_account/edit_subscription_payment_info_spec.rb +++ b/spec/requests/store/my_account/edit_subscription_payment_info_spec.rb @@ -18,7 +18,7 @@ expect(@credit_card.current_payment_info).to_not be_nil end - scenario "can update payment info" do + scenario "can update payment info", :vcr do @credit_card.number = "4242424242424242" @credit_card.name = "John Doe" @credit_card.expiry = "06/20" diff --git a/spec/requests/store/my_account/pause_subscription_spec.rb b/spec/requests/store/my_account/pause_subscription_spec.rb index c42f69f..ed6a545 100644 --- a/spec/requests/store/my_account/pause_subscription_spec.rb +++ b/spec/requests/store/my_account/pause_subscription_spec.rb @@ -57,7 +57,7 @@ def next_month end def pause_all_subscriptions_for(user) - user.subscriptions.each { |s| s.pause } + user.subscriptions.each { |s| s.pause! } end end diff --git a/spec/services/adjust_sku_service_spec.rb b/spec/services/adjust_sku_service_spec.rb index 2a1a229..d9e4fea 100644 --- a/spec/services/adjust_sku_service_spec.rb +++ b/spec/services/adjust_sku_service_spec.rb @@ -28,8 +28,8 @@ AdjustSkuService.new.update_subscriptions("bdc1", "bdc2") expect(@subscription.subscription_items.count).to eq(2) - expect(@subscription.subscription_items[0].variant_id).to be(variant2.id) - expect(@subscription.subscription_items[1].variant_id).to be(@new_variant.id) + expect(@subscription.subscription_items[0].variant.sku).to eq(@new_variant.sku) + expect(@subscription.subscription_items[1].variant.sku).to eq(variant2.sku) end it "all affected subscriptions are updated" do @@ -51,24 +51,4 @@ expect(@subscription.subscription_items.first).to have_attributes(quantity: 5, interval: 2, variant_id: @new_variant.id) end end - - - it "gets all subscription items with a specific sku" do - user2 = FactoryGirl.create(:user) - subscription2 = user2.subscriptions.create!(ship_address_id: address.id, bill_address_id: address.id) - subscription2.subscription_items.create!(variant_id: @old_variant.id, quantity: 1, price: 10.00) - - subscription_items = AdjustSkuService.new.subscription_items(@old_variant) - - expect(subscription_items.count).to eq(2) - end - - it "creates a subscription item with the updated variant sku" do - item = @subscription.subscription_items.first - - AdjustSkuService.new.create_subscription_item(item, @new_variant) - - expect(@subscription.subscription_items.last.variant.sku).to eq("bdc2") - end - end diff --git a/spec/support/poltergeist.rb b/spec/support/poltergeist.rb new file mode 100644 index 0000000..a20cc66 --- /dev/null +++ b/spec/support/poltergeist.rb @@ -0,0 +1,27 @@ +require 'capybara/poltergeist' + +Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, + js_errors: false, + inspector: true, + timeout: 240, + url_blacklist: %w( + typekit.net + fontdeck.com + facebook.net + facebook.com + optimizely.com + ravenjs.com + google.com + googleapis.com + googleadservices.com + googletagmanager.com + google-analytics.com) + ) +end + +Capybara.raise_server_errors = false +Capybara.always_include_port = true +Capybara.javascript_driver = :poltergeist +Capybara.default_max_wait_time = 10 +Capybara.server_port = 61_454 diff --git a/spec/support/subscription_helpers.rb b/spec/support/subscription_helpers.rb new file mode 100644 index 0000000..c8d719d --- /dev/null +++ b/spec/support/subscription_helpers.rb @@ -0,0 +1,15 @@ +def subscription + @order.subscriptions.last +end + +def next_shipment_date + @order.subscriptions.last.next_shipment_date.to_date +end + +def next_calc_shipment_date + subscription.last_shipment_date.advance(subscription.calc_next_renewal_date).to_date +end + +def original_shipment_date + (subscription.interval).months.from_now.to_date +end diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb new file mode 100644 index 0000000..cfd44f0 --- /dev/null +++ b/spec/support/vcr.rb @@ -0,0 +1,6 @@ +VCR.configure do |c| + c.allow_http_connections_when_no_cassette = true + c.cassette_library_dir = 'spec/cassettes' + c.hook_into :webmock + c.configure_rspec_metadata! +end