solidus_subscriptions-alpha 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +26 -0
- data/README.md +128 -0
- data/Rakefile +28 -0
- data/app/assets/javascripts/spree/backend/solidus_subscriptions.js +2 -0
- data/app/assets/javascripts/spree/frontend/solidus_subscriptions.js +2 -0
- data/app/assets/stylesheets/spree/backend/solidus_subscriptions.css +4 -0
- data/app/assets/stylesheets/spree/frontend/solidus_subscriptions.css +4 -0
- data/app/controllers/solidus_subscriptions/api/v1/line_items_controller.rb +35 -0
- data/app/controllers/solidus_subscriptions/api/v1/subscriptions_controller.rb +44 -0
- data/app/controllers/spree/admin/subscriptions_controller.rb +66 -0
- data/app/decorators/spree/controllers/api/line_items/create_subscription_line_items.rb +28 -0
- data/app/decorators/spree/controllers/orders/create_subscription_line_items.rb +33 -0
- data/app/decorators/spree/line_items/subscription_line_items_association.rb +22 -0
- data/app/decorators/spree/orders/after_create.rb +15 -0
- data/app/decorators/spree/orders/finalize_creates_subscriptions.rb +19 -0
- data/app/decorators/spree/orders/subscription_line_items_association.rb +15 -0
- data/app/decorators/spree/users/have_many_subscriptions.rb +18 -0
- data/app/decorators/spree/variant_pretty_name.rb +13 -0
- data/app/jobs/solidus_subscriptions/process_installments_job.rb +22 -0
- data/app/models/solidus_subscriptions/checkout.rb +137 -0
- data/app/models/solidus_subscriptions/dispatcher.rb +32 -0
- data/app/models/solidus_subscriptions/failure_dispatcher.rb +19 -0
- data/app/models/solidus_subscriptions/installment.rb +126 -0
- data/app/models/solidus_subscriptions/installment_detail.rb +23 -0
- data/app/models/solidus_subscriptions/interval.rb +24 -0
- data/app/models/solidus_subscriptions/line_item.rb +98 -0
- data/app/models/solidus_subscriptions/line_item_builder.rb +44 -0
- data/app/models/solidus_subscriptions/order_builder.rb +40 -0
- data/app/models/solidus_subscriptions/out_of_stock_dispatcher.rb +19 -0
- data/app/models/solidus_subscriptions/payment_failed_dispatcher.rb +23 -0
- data/app/models/solidus_subscriptions/subscription.rb +217 -0
- data/app/models/solidus_subscriptions/subscription_generator.rb +60 -0
- data/app/models/solidus_subscriptions/subscription_line_item_builder.rb +21 -0
- data/app/models/solidus_subscriptions/subscription_order_promotion_rule.rb +25 -0
- data/app/models/solidus_subscriptions/subscription_promotion_rule.rb +38 -0
- data/app/models/solidus_subscriptions/success_dispatcher.rb +16 -0
- data/app/models/solidus_subscriptions/unsubscribable_error.rb +17 -0
- data/app/models/solidus_subscriptions/user_mismatch_error.rb +15 -0
- data/app/overrides/views/admin_subscribable_checkbox.rb +6 -0
- data/app/overrides/views/admin_subscriptions_menu_link.rb +8 -0
- data/app/overrides/views/subscription_line_item_fields.rb +6 -0
- data/app/views/spree/admin/promotions/rules/_subscription_order_promotion_rule.html.erb +0 -0
- data/app/views/spree/admin/promotions/rules/_subscription_promotion_rule.html.erb +0 -0
- data/app/views/spree/admin/shared/_no_objects_found.html.erb +4 -0
- data/app/views/spree/admin/shared/_subscription_tab.html.erb +3 -0
- data/app/views/spree/admin/solidus_subscriptions/subscriptions/_subscription.html.erb +66 -0
- data/app/views/spree/admin/subscriptions/_form.html.erb +81 -0
- data/app/views/spree/admin/subscriptions/_legacy_form.html.erb +81 -0
- data/app/views/spree/admin/subscriptions/_legacy_sidebar.html.erb +28 -0
- data/app/views/spree/admin/subscriptions/edit.html.erb +21 -0
- data/app/views/spree/admin/subscriptions/index.html.erb +119 -0
- data/app/views/spree/admin/subscriptions/new.html.erb +9 -0
- data/app/views/spree/admin/variants/_subscribable_checkbox.html.erb +6 -0
- data/app/views/spree/frontend/products/_subscription_line_item_fields.html.erb +30 -0
- data/config/locales/en.yml +91 -0
- data/config/routes.rb +25 -0
- data/db/migrate/20160825164850_create_solidus_subscriptions_subscriptions.rb +11 -0
- data/db/migrate/20160825173548_create_solidus_subscriptions_line_items.rb +17 -0
- data/db/migrate/20160825202248_create_solidus_subscriptions_installments.rb +23 -0
- data/db/migrate/20160825211202_create_solidus_subscriptions_installment_details.rb +22 -0
- data/db/migrate/20160825214240_add_subscribable_to_spree_variants.rb +5 -0
- data/db/migrate/20160829201653_change_subscription_line_items_installments_to_max_installments.rb +5 -0
- data/db/migrate/20160902220242_remove_state_from_solidus_susbscriptions_installment_details.rb +5 -0
- data/db/migrate/20160902220604_add_successful_to_solidus_subscriptions_installment_details.rb +5 -0
- data/db/migrate/20160902221218_add_message_to_solidus_subscriptions_installment_details.rb +5 -0
- data/db/migrate/20160922164101_add_interval_length_and_units_to_subscription_line_items.rb +8 -0
- data/db/migrate/20161006191003_add_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -0
- data/db/migrate/20161006191127_add_successive_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -0
- data/db/migrate/20161014212649_allow_spree_line_item_id_to_be_null.rb +5 -0
- data/db/migrate/20161017155749_add_order_id_to_solidus_subscriptions_installment_details.rb +6 -0
- data/db/migrate/20161017175509_remove_order_id_from_solidus_subscriptions_installments.rb +6 -0
- data/db/migrate/20161017201944_add_subscription_order_to_spree_orders.rb +5 -0
- data/db/migrate/20161221155142_add_store_to_solidus_subscriptions_subscriptions.rb +6 -0
- data/db/migrate/20161223152905_add_address_id_to_solidus_subscriptions_subscriptions.rb +7 -0
- data/db/migrate/20170106224713_change_line_item_max_installments_to_end_date.rb +6 -0
- data/db/migrate/20170111224458_change_subscription_actionable_date_to_datetime.rb +5 -0
- data/db/migrate/20170111232801_change_inteval_actionable_date_to_datetime.rb +5 -0
- data/db/migrate/20170112012407_add_config_options_to_subscriptions.rb +7 -0
- data/lib/generators/solidus_subscriptions/install/install_generator.rb +30 -0
- data/lib/solidus_subscriptions.rb +6 -0
- data/lib/solidus_subscriptions/ability.rb +19 -0
- data/lib/solidus_subscriptions/config.rb +97 -0
- data/lib/solidus_subscriptions/engine.rb +56 -0
- data/lib/solidus_subscriptions/permitted_attributes.rb +36 -0
- data/lib/solidus_subscriptions/processor.rb +108 -0
- data/lib/solidus_subscriptions/testing_support/factories.rb +5 -0
- data/lib/solidus_subscriptions/testing_support/factories/installment_detail_factory.rb +7 -0
- data/lib/solidus_subscriptions/testing_support/factories/installment_factory.rb +21 -0
- data/lib/solidus_subscriptions/testing_support/factories/line_item_factory.rb +18 -0
- data/lib/solidus_subscriptions/testing_support/factories/spree/line_item_factory.rb +17 -0
- data/lib/solidus_subscriptions/testing_support/factories/spree/order_factory.rb +18 -0
- data/lib/solidus_subscriptions/testing_support/factories/spree_modification_factory.rb +8 -0
- data/lib/solidus_subscriptions/testing_support/factories/subscription_factory.rb +43 -0
- data/lib/solidus_subscriptions/version.rb +3 -0
- data/lib/tasks/process_subscriptions.rake +6 -0
- metadata +460 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# This module is intended to be included into any active record
|
|
2
|
+
# modle which needs to be aware of how intervals and stored and
|
|
3
|
+
# calculated in the db.
|
|
4
|
+
#
|
|
5
|
+
# Base models must have the following fields: interval_length (integer) and interval_units (integer)
|
|
6
|
+
module SolidusSubscriptions
|
|
7
|
+
module Interval
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.enum interval_units: {
|
|
10
|
+
day: 0,
|
|
11
|
+
week: 1,
|
|
12
|
+
month: 2,
|
|
13
|
+
year: 3
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Calculates the number of seconds in the interval.
|
|
18
|
+
#
|
|
19
|
+
# @return [Integer] The number of seconds.
|
|
20
|
+
def interval
|
|
21
|
+
ActiveSupport::Duration.new(interval_length, { interval_units.pluralize.to_sym => interval_length })
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# The LineItem class is responsible for associating Line items to subscriptions. # It tracks the following values:
|
|
2
|
+
#
|
|
3
|
+
# [Spree::LineItem] :spree_line_item The spree object which created this instance
|
|
4
|
+
#
|
|
5
|
+
# [SolidusSubscription::Subscription] :subscription The object responsible for
|
|
6
|
+
# grouping all information needed to create new subscription orders together
|
|
7
|
+
#
|
|
8
|
+
# [Integer] :subscribable_id The id of the object to be added to new subscription
|
|
9
|
+
# orders when they are placed
|
|
10
|
+
#
|
|
11
|
+
# [Integer] :quantity How many units of the subscribable should be included in
|
|
12
|
+
# future orders
|
|
13
|
+
#
|
|
14
|
+
# [Integer] :interval How often subscription orders should be placed
|
|
15
|
+
#
|
|
16
|
+
# [Integer] :installments How many subscription orders should be placed
|
|
17
|
+
module SolidusSubscriptions
|
|
18
|
+
class LineItem < ActiveRecord::Base
|
|
19
|
+
include Interval
|
|
20
|
+
|
|
21
|
+
belongs_to :spree_line_item, class_name: 'Spree::LineItem', inverse_of: :subscription_line_items
|
|
22
|
+
has_one :order, through: :spree_line_item, class_name: 'Spree::Order'
|
|
23
|
+
belongs_to(
|
|
24
|
+
:subscription,
|
|
25
|
+
class_name: 'SolidusSubscriptions::Subscription',
|
|
26
|
+
inverse_of: :line_items
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
validates :subscribable_id, presence: :true
|
|
30
|
+
validates :quantity, numericality: { greater_than: 0 }
|
|
31
|
+
validates :interval_length, numericality: { greater_than: 0 }, unless: -> { subscription }
|
|
32
|
+
|
|
33
|
+
before_update :update_actionable_date_if_interval_changed
|
|
34
|
+
|
|
35
|
+
def next_actionable_date
|
|
36
|
+
dummy_subscription.next_actionable_date
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def as_json(**options)
|
|
40
|
+
options[:methods] ||= [:dummy_line_item, :next_actionable_date]
|
|
41
|
+
super(options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get a placeholder line item for calculating the values of future
|
|
45
|
+
# subscription orders. It is frozen and cannot be saved
|
|
46
|
+
def dummy_line_item
|
|
47
|
+
li = LineItemBuilder.new([self]).spree_line_items.first
|
|
48
|
+
return unless li
|
|
49
|
+
|
|
50
|
+
li.order = dummy_order
|
|
51
|
+
li.validate
|
|
52
|
+
li.freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def interval
|
|
56
|
+
subscription.try!(:interval) || super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Get a placeholder order for calculating the values of future
|
|
62
|
+
# subscription orders. It is a frozen duplicate of the current order and
|
|
63
|
+
# cannot be saved
|
|
64
|
+
def dummy_order
|
|
65
|
+
order = spree_line_item ? spree_line_item.order.dup : Spree::Order.create
|
|
66
|
+
order.ship_address = subscription.shipping_address || subscription.user.ship_address if subscription
|
|
67
|
+
|
|
68
|
+
order.freeze
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A place holder for calculating dynamic values needed to display in the cart
|
|
72
|
+
# it is frozen and cannot be saved
|
|
73
|
+
def dummy_subscription
|
|
74
|
+
Subscription.new(line_items: [dup], interval_length: interval_length, interval_units: interval_units).freeze
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def update_actionable_date_if_interval_changed
|
|
78
|
+
if persisted? && subscription && (interval_length_changed? || interval_units_changed?)
|
|
79
|
+
base_date = if subscription.installments.any?
|
|
80
|
+
subscription.installments.last.created_at
|
|
81
|
+
else
|
|
82
|
+
subscription.created_at
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
new_date = interval.since(base_date)
|
|
86
|
+
|
|
87
|
+
if new_date < Time.zone.now
|
|
88
|
+
# if the chosen base time plus the new interval is in the past, set
|
|
89
|
+
# the actionable_date to be now to avoid confusion and possible
|
|
90
|
+
# mis-processing.
|
|
91
|
+
new_date = Time.zone.now
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
subscription.actionable_date = new_date
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# This class is responsible for taking SubscriptionLineItems and building
|
|
2
|
+
# them into Spree::LineItems which can be added to an order
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
class LineItemBuilder
|
|
5
|
+
attr_reader :subscription_line_items
|
|
6
|
+
|
|
7
|
+
# Get a new instance of a LineItemBuilder
|
|
8
|
+
#
|
|
9
|
+
# @param subscription_line_items[Array<SolidusSubscriptions::LineItem>] The
|
|
10
|
+
# subscription line item to be converted into a Spree::LineItem
|
|
11
|
+
#
|
|
12
|
+
# @return [SolidusSubscriptions::LineItemBuilder]
|
|
13
|
+
def initialize(subscription_line_items)
|
|
14
|
+
@subscription_line_items = subscription_line_items
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Get a new (unpersisted) Spree::LineItem which matches the details of
|
|
18
|
+
# :subscription_line_item
|
|
19
|
+
#
|
|
20
|
+
# @return [Array<Spree::LineItem>]
|
|
21
|
+
def spree_line_items
|
|
22
|
+
line_items = subscription_line_items.map do |subscription_line_item|
|
|
23
|
+
variant = subscribables.fetch(subscription_line_item.subscribable_id)
|
|
24
|
+
|
|
25
|
+
raise UnsubscribableError.new(variant) unless variant.subscribable?
|
|
26
|
+
next unless variant.can_supply?(subscription_line_item.quantity)
|
|
27
|
+
|
|
28
|
+
Spree::LineItem.new(variant: variant, quantity: subscription_line_item.quantity)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Either all line items for an installment are fullfilled or none are
|
|
32
|
+
line_items.all? ? line_items : []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def subscribables
|
|
38
|
+
return @subscribables if @subscribables
|
|
39
|
+
|
|
40
|
+
ids = subscription_line_items.map(&:subscribable_id)
|
|
41
|
+
@subscribables ||= Spree::Variant.find(ids).index_by(&:id)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# This class is responsible for adding line items to order without going
|
|
2
|
+
# through order contents.
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
class OrderBuilder
|
|
5
|
+
attr_reader :order
|
|
6
|
+
|
|
7
|
+
# Get a new instance of a OrderBuilder
|
|
8
|
+
#
|
|
9
|
+
# @param order [Spree::Order] The order to be built
|
|
10
|
+
#
|
|
11
|
+
# @return [SolidusSubscriptions::OrderBuilder]
|
|
12
|
+
def initialize(order)
|
|
13
|
+
@order = order
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Add line items for to an order. If the order already
|
|
17
|
+
# has a line item for a given variant_id, update the quantity. Otherwise
|
|
18
|
+
# add the line item to the order.
|
|
19
|
+
#
|
|
20
|
+
# @param items [Array<Spree::LineItem>] The order to add the line item to
|
|
21
|
+
# @return [Array<Spree::LineItem] The collection that was passed in
|
|
22
|
+
def add_line_items(items)
|
|
23
|
+
items.map { |item| add_item_to_order(item) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def add_item_to_order(new_item)
|
|
29
|
+
line_item = order.line_items.detect do |li|
|
|
30
|
+
li.variant_id == new_item.variant_id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if line_item
|
|
34
|
+
line_item.increment!(:quantity, new_item.quantity)
|
|
35
|
+
else
|
|
36
|
+
order.line_items << new_item
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# This service class is intented to provide callback behaviour to handle
|
|
2
|
+
# the case where an installment cannot be processed due to lack of stock.
|
|
3
|
+
module SolidusSubscriptions
|
|
4
|
+
class OutOfStockDispatcher < Dispatcher
|
|
5
|
+
def dispatch
|
|
6
|
+
installments.each(&:out_of_stock)
|
|
7
|
+
super
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def message
|
|
13
|
+
"
|
|
14
|
+
The following installments cannot be fulfilled due to lack of stock:
|
|
15
|
+
#{installments.map(&:id).join(', ')}.
|
|
16
|
+
"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This service class is intented to provide callback behaviour to handle
|
|
2
|
+
# the case where a subscription order cannot be processed because a payment
|
|
3
|
+
# failed
|
|
4
|
+
module SolidusSubscriptions
|
|
5
|
+
class PaymentFailedDispatcher < Dispatcher
|
|
6
|
+
def dispatch
|
|
7
|
+
order.touch :completed_at
|
|
8
|
+
order.cancel!
|
|
9
|
+
|
|
10
|
+
installments.each { |i| i.payment_failed!(order) }
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def message
|
|
17
|
+
"
|
|
18
|
+
The following installments could not be processed due to payment
|
|
19
|
+
authorization failure: #{installments.map(&:id).join(', ')}
|
|
20
|
+
"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# The subscription class is responsable for grouping together the
|
|
2
|
+
# information required for the system to place a subscriptions order on
|
|
3
|
+
# behalf of a specific user.
|
|
4
|
+
module SolidusSubscriptions
|
|
5
|
+
class Subscription < ActiveRecord::Base
|
|
6
|
+
include Interval
|
|
7
|
+
|
|
8
|
+
PROCESSING_STATES = [:pending, :failed, :success]
|
|
9
|
+
|
|
10
|
+
belongs_to :user, class_name: Spree.user_class
|
|
11
|
+
has_many :line_items, class_name: 'SolidusSubscriptions::LineItem', inverse_of: :subscription
|
|
12
|
+
has_many :installments, class_name: 'SolidusSubscriptions::Installment'
|
|
13
|
+
belongs_to :store, class_name: 'Spree::Store'
|
|
14
|
+
belongs_to :shipping_address, class_name: 'Spree::Address'
|
|
15
|
+
|
|
16
|
+
validates :user, presence: :true
|
|
17
|
+
validates :skip_count, :successive_skip_count, presence: true, numericality: { greater_than_or_equal_to: 0 }
|
|
18
|
+
validates :interval_length, numericality: { greater_than: 0 }
|
|
19
|
+
|
|
20
|
+
accepts_nested_attributes_for :shipping_address
|
|
21
|
+
accepts_nested_attributes_for :line_items, allow_destroy: true
|
|
22
|
+
|
|
23
|
+
# The following methods are delegated to the associated
|
|
24
|
+
# SolidusSubscriptions::LineItem
|
|
25
|
+
#
|
|
26
|
+
# :quantity, :subscribable_id
|
|
27
|
+
delegate :quantity, :subscribable_id, to: :line_item
|
|
28
|
+
|
|
29
|
+
# Find all subscriptions that are "actionable"; that is, ones that have an
|
|
30
|
+
# actionable_date in the past and are not invalid or canceled.
|
|
31
|
+
scope :actionable, (lambda do
|
|
32
|
+
where("#{table_name}.actionable_date <= ?", Time.zone.now).
|
|
33
|
+
where.not(state: ["canceled", "inactive"])
|
|
34
|
+
end)
|
|
35
|
+
|
|
36
|
+
# Find subscriptions based on their processing state. This state is not a
|
|
37
|
+
# model attrubute.
|
|
38
|
+
#
|
|
39
|
+
# @param state [Symbol] One of :pending, :success, or failed
|
|
40
|
+
#
|
|
41
|
+
# pending: New subscriptions, never been processed
|
|
42
|
+
# failed: Subscriptions which failed to be processed on the last attempt
|
|
43
|
+
# success: Subscriptions which were successfully processed on the last attempt
|
|
44
|
+
scope :in_processing_state, (lambda do |state|
|
|
45
|
+
case state.to_sym
|
|
46
|
+
when :success
|
|
47
|
+
fulfilled.joins(:installments)
|
|
48
|
+
when :failed
|
|
49
|
+
fulfilled_ids = fulfilled.pluck(:id)
|
|
50
|
+
where.not(id: fulfilled_ids)
|
|
51
|
+
when :pending
|
|
52
|
+
includes(:installments).where(solidus_subscriptions_installments: { id: nil })
|
|
53
|
+
else
|
|
54
|
+
raise ArgumentError.new("state must be one of: :success, :failed, :pending")
|
|
55
|
+
end
|
|
56
|
+
end)
|
|
57
|
+
|
|
58
|
+
scope :fulfilled, (lambda do
|
|
59
|
+
unfulfilled_ids = unfulfilled.pluck(:id)
|
|
60
|
+
where.not(id: unfulfilled_ids)
|
|
61
|
+
end)
|
|
62
|
+
|
|
63
|
+
scope :unfulfilled, (lambda do
|
|
64
|
+
joins(:installments).merge(Installment.unfulfilled)
|
|
65
|
+
end)
|
|
66
|
+
|
|
67
|
+
def self.ransackable_scopes(_auth_object = nil)
|
|
68
|
+
[:in_processing_state]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.processing_states
|
|
72
|
+
PROCESSING_STATES
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The subscription state determines the behaviours around when it is
|
|
76
|
+
# processed. Here is a brief description of the states and how they affect
|
|
77
|
+
# the subscription.
|
|
78
|
+
#
|
|
79
|
+
# [active] Default state when created. Subscription can be processed
|
|
80
|
+
# [canceled] The user has ended their subscription. Subscription will not
|
|
81
|
+
# be processed.
|
|
82
|
+
# [pending_cancellation] The user has ended their subscription, but the
|
|
83
|
+
# conditions for canceling the subscription have not been met. Subscription
|
|
84
|
+
# will continue to be processed until the subscription is canceled and
|
|
85
|
+
# the conditions are met.
|
|
86
|
+
# [inactive] The number of installments has been fulfilled. The subscription
|
|
87
|
+
# will no longer be processed
|
|
88
|
+
state_machine :state, initial: :active do
|
|
89
|
+
event :cancel do
|
|
90
|
+
transition [:active, :pending_cancellation] => :canceled,
|
|
91
|
+
if: ->(subscription) { subscription.can_be_canceled? }
|
|
92
|
+
|
|
93
|
+
transition active: :pending_cancellation
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
after_transition to: :canceled, do: :advance_actionable_date
|
|
97
|
+
|
|
98
|
+
event :deactivate do
|
|
99
|
+
transition active: :inactive,
|
|
100
|
+
if: ->(subscription) { subscription.can_be_deactivated? }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
event :activate do
|
|
104
|
+
transition any - [:active] => :active
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
after_transition to: :active, do: :advance_actionable_date
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# This method determines if a subscription may be canceled. Canceled
|
|
111
|
+
# subcriptions will not be processed. By default subscriptions may always be
|
|
112
|
+
# canceled. If this method is overriden to return false, the subscription
|
|
113
|
+
# will be moved to the :pending_cancellation state until it is canceled
|
|
114
|
+
# again and this condition is true.
|
|
115
|
+
#
|
|
116
|
+
# USE CASE: Subscriptions can only be canceled more than 10 days before they
|
|
117
|
+
# are processed. Override this method to be:
|
|
118
|
+
#
|
|
119
|
+
# def can_be_canceled?
|
|
120
|
+
# return true if actionable_date.nil?
|
|
121
|
+
# (actionable_date - 10.days.from_now.to_date) > 0
|
|
122
|
+
# end
|
|
123
|
+
#
|
|
124
|
+
# If a user cancels this subscription less than 10 days before it will
|
|
125
|
+
# be processed the subscription will be bumped into the
|
|
126
|
+
# :pending_cancellation state instead of being canceled. Susbcriptions
|
|
127
|
+
# pending cancellation will still be processed.
|
|
128
|
+
def can_be_canceled?
|
|
129
|
+
return true if actionable_date.nil?
|
|
130
|
+
(actionable_date - Config.minimum_cancellation_notice).future?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def skip
|
|
134
|
+
check_successive_skips_exceeded
|
|
135
|
+
check_total_skips_exceeded
|
|
136
|
+
|
|
137
|
+
return if errors.any?
|
|
138
|
+
|
|
139
|
+
advance_actionable_date
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# This method determines if a subscription can be deactivated. A deactivated
|
|
143
|
+
# subscription will not be processed. By default a subscription can be
|
|
144
|
+
# deactivated if the end_date defined on
|
|
145
|
+
# subscription_line_item is less than the current date
|
|
146
|
+
# In this case the subscription has been fulfilled and
|
|
147
|
+
# should not be processed again. Subscriptions without an end_date
|
|
148
|
+
# value cannot be deactivated.
|
|
149
|
+
def can_be_deactivated?
|
|
150
|
+
active? && line_item.end_date && actionable_date > line_item.end_date
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get the date after the current actionable_date where this subscription
|
|
154
|
+
# will be actionable again
|
|
155
|
+
#
|
|
156
|
+
# @return [Date] The current actionable_date plus 1 interval. The next
|
|
157
|
+
# date after the current actionable_date this subscription will be
|
|
158
|
+
# eligible to be processed.
|
|
159
|
+
def next_actionable_date
|
|
160
|
+
return nil unless active?
|
|
161
|
+
new_date = (actionable_date || Time.zone.now)
|
|
162
|
+
(new_date + interval).beginning_of_minute
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Advance the actionable date to the next_actionable_date value. Will modify
|
|
166
|
+
# the record.
|
|
167
|
+
#
|
|
168
|
+
# @return [Date] The next date after the current actionable_date this
|
|
169
|
+
# subscription will be eligible to be processed.
|
|
170
|
+
def advance_actionable_date
|
|
171
|
+
update! actionable_date: next_actionable_date
|
|
172
|
+
actionable_date
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Get the builder for the subscription_line_item. This will be an
|
|
176
|
+
# object that can generate the appropriate line item for the subscribable
|
|
177
|
+
# object
|
|
178
|
+
#
|
|
179
|
+
# @return [SolidusSubscriptions::LineItemBuilder]
|
|
180
|
+
def line_item_builder
|
|
181
|
+
LineItemBuilder.new(line_items)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The state of the last attempt to process an installment associated to
|
|
185
|
+
# this subscrtipion
|
|
186
|
+
#
|
|
187
|
+
# @return [String] pending if the no installments have been processed,
|
|
188
|
+
# failed if the last installment has not been fulfilled and, success
|
|
189
|
+
# if the last installment was fulfilled.
|
|
190
|
+
def processing_state
|
|
191
|
+
return 'pending' if installments.empty?
|
|
192
|
+
installments.last.fulfilled? ? 'success' : 'failed'
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def check_successive_skips_exceeded
|
|
198
|
+
return unless Config.maximum_successive_skips
|
|
199
|
+
|
|
200
|
+
if successive_skip_count >= Config.maximum_successive_skips
|
|
201
|
+
errors.add(:successive_skip_count, :exceeded)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def check_total_skips_exceeded
|
|
206
|
+
return unless Config.maximum_total_skips
|
|
207
|
+
|
|
208
|
+
if skip_count >= Config.maximum_total_skips
|
|
209
|
+
errors.add(:skip_count, :exceeded)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def line_item
|
|
214
|
+
line_items.first
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|