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.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +26 -0
  3. data/README.md +128 -0
  4. data/Rakefile +28 -0
  5. data/app/assets/javascripts/spree/backend/solidus_subscriptions.js +2 -0
  6. data/app/assets/javascripts/spree/frontend/solidus_subscriptions.js +2 -0
  7. data/app/assets/stylesheets/spree/backend/solidus_subscriptions.css +4 -0
  8. data/app/assets/stylesheets/spree/frontend/solidus_subscriptions.css +4 -0
  9. data/app/controllers/solidus_subscriptions/api/v1/line_items_controller.rb +35 -0
  10. data/app/controllers/solidus_subscriptions/api/v1/subscriptions_controller.rb +44 -0
  11. data/app/controllers/spree/admin/subscriptions_controller.rb +66 -0
  12. data/app/decorators/spree/controllers/api/line_items/create_subscription_line_items.rb +28 -0
  13. data/app/decorators/spree/controllers/orders/create_subscription_line_items.rb +33 -0
  14. data/app/decorators/spree/line_items/subscription_line_items_association.rb +22 -0
  15. data/app/decorators/spree/orders/after_create.rb +15 -0
  16. data/app/decorators/spree/orders/finalize_creates_subscriptions.rb +19 -0
  17. data/app/decorators/spree/orders/subscription_line_items_association.rb +15 -0
  18. data/app/decorators/spree/users/have_many_subscriptions.rb +18 -0
  19. data/app/decorators/spree/variant_pretty_name.rb +13 -0
  20. data/app/jobs/solidus_subscriptions/process_installments_job.rb +22 -0
  21. data/app/models/solidus_subscriptions/checkout.rb +137 -0
  22. data/app/models/solidus_subscriptions/dispatcher.rb +32 -0
  23. data/app/models/solidus_subscriptions/failure_dispatcher.rb +19 -0
  24. data/app/models/solidus_subscriptions/installment.rb +126 -0
  25. data/app/models/solidus_subscriptions/installment_detail.rb +23 -0
  26. data/app/models/solidus_subscriptions/interval.rb +24 -0
  27. data/app/models/solidus_subscriptions/line_item.rb +98 -0
  28. data/app/models/solidus_subscriptions/line_item_builder.rb +44 -0
  29. data/app/models/solidus_subscriptions/order_builder.rb +40 -0
  30. data/app/models/solidus_subscriptions/out_of_stock_dispatcher.rb +19 -0
  31. data/app/models/solidus_subscriptions/payment_failed_dispatcher.rb +23 -0
  32. data/app/models/solidus_subscriptions/subscription.rb +217 -0
  33. data/app/models/solidus_subscriptions/subscription_generator.rb +60 -0
  34. data/app/models/solidus_subscriptions/subscription_line_item_builder.rb +21 -0
  35. data/app/models/solidus_subscriptions/subscription_order_promotion_rule.rb +25 -0
  36. data/app/models/solidus_subscriptions/subscription_promotion_rule.rb +38 -0
  37. data/app/models/solidus_subscriptions/success_dispatcher.rb +16 -0
  38. data/app/models/solidus_subscriptions/unsubscribable_error.rb +17 -0
  39. data/app/models/solidus_subscriptions/user_mismatch_error.rb +15 -0
  40. data/app/overrides/views/admin_subscribable_checkbox.rb +6 -0
  41. data/app/overrides/views/admin_subscriptions_menu_link.rb +8 -0
  42. data/app/overrides/views/subscription_line_item_fields.rb +6 -0
  43. data/app/views/spree/admin/promotions/rules/_subscription_order_promotion_rule.html.erb +0 -0
  44. data/app/views/spree/admin/promotions/rules/_subscription_promotion_rule.html.erb +0 -0
  45. data/app/views/spree/admin/shared/_no_objects_found.html.erb +4 -0
  46. data/app/views/spree/admin/shared/_subscription_tab.html.erb +3 -0
  47. data/app/views/spree/admin/solidus_subscriptions/subscriptions/_subscription.html.erb +66 -0
  48. data/app/views/spree/admin/subscriptions/_form.html.erb +81 -0
  49. data/app/views/spree/admin/subscriptions/_legacy_form.html.erb +81 -0
  50. data/app/views/spree/admin/subscriptions/_legacy_sidebar.html.erb +28 -0
  51. data/app/views/spree/admin/subscriptions/edit.html.erb +21 -0
  52. data/app/views/spree/admin/subscriptions/index.html.erb +119 -0
  53. data/app/views/spree/admin/subscriptions/new.html.erb +9 -0
  54. data/app/views/spree/admin/variants/_subscribable_checkbox.html.erb +6 -0
  55. data/app/views/spree/frontend/products/_subscription_line_item_fields.html.erb +30 -0
  56. data/config/locales/en.yml +91 -0
  57. data/config/routes.rb +25 -0
  58. data/db/migrate/20160825164850_create_solidus_subscriptions_subscriptions.rb +11 -0
  59. data/db/migrate/20160825173548_create_solidus_subscriptions_line_items.rb +17 -0
  60. data/db/migrate/20160825202248_create_solidus_subscriptions_installments.rb +23 -0
  61. data/db/migrate/20160825211202_create_solidus_subscriptions_installment_details.rb +22 -0
  62. data/db/migrate/20160825214240_add_subscribable_to_spree_variants.rb +5 -0
  63. data/db/migrate/20160829201653_change_subscription_line_items_installments_to_max_installments.rb +5 -0
  64. data/db/migrate/20160902220242_remove_state_from_solidus_susbscriptions_installment_details.rb +5 -0
  65. data/db/migrate/20160902220604_add_successful_to_solidus_subscriptions_installment_details.rb +5 -0
  66. data/db/migrate/20160902221218_add_message_to_solidus_subscriptions_installment_details.rb +5 -0
  67. data/db/migrate/20160922164101_add_interval_length_and_units_to_subscription_line_items.rb +8 -0
  68. data/db/migrate/20161006191003_add_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -0
  69. data/db/migrate/20161006191127_add_successive_skip_count_to_solidus_subscriptions_subscriptions.rb +5 -0
  70. data/db/migrate/20161014212649_allow_spree_line_item_id_to_be_null.rb +5 -0
  71. data/db/migrate/20161017155749_add_order_id_to_solidus_subscriptions_installment_details.rb +6 -0
  72. data/db/migrate/20161017175509_remove_order_id_from_solidus_subscriptions_installments.rb +6 -0
  73. data/db/migrate/20161017201944_add_subscription_order_to_spree_orders.rb +5 -0
  74. data/db/migrate/20161221155142_add_store_to_solidus_subscriptions_subscriptions.rb +6 -0
  75. data/db/migrate/20161223152905_add_address_id_to_solidus_subscriptions_subscriptions.rb +7 -0
  76. data/db/migrate/20170106224713_change_line_item_max_installments_to_end_date.rb +6 -0
  77. data/db/migrate/20170111224458_change_subscription_actionable_date_to_datetime.rb +5 -0
  78. data/db/migrate/20170111232801_change_inteval_actionable_date_to_datetime.rb +5 -0
  79. data/db/migrate/20170112012407_add_config_options_to_subscriptions.rb +7 -0
  80. data/lib/generators/solidus_subscriptions/install/install_generator.rb +30 -0
  81. data/lib/solidus_subscriptions.rb +6 -0
  82. data/lib/solidus_subscriptions/ability.rb +19 -0
  83. data/lib/solidus_subscriptions/config.rb +97 -0
  84. data/lib/solidus_subscriptions/engine.rb +56 -0
  85. data/lib/solidus_subscriptions/permitted_attributes.rb +36 -0
  86. data/lib/solidus_subscriptions/processor.rb +108 -0
  87. data/lib/solidus_subscriptions/testing_support/factories.rb +5 -0
  88. data/lib/solidus_subscriptions/testing_support/factories/installment_detail_factory.rb +7 -0
  89. data/lib/solidus_subscriptions/testing_support/factories/installment_factory.rb +21 -0
  90. data/lib/solidus_subscriptions/testing_support/factories/line_item_factory.rb +18 -0
  91. data/lib/solidus_subscriptions/testing_support/factories/spree/line_item_factory.rb +17 -0
  92. data/lib/solidus_subscriptions/testing_support/factories/spree/order_factory.rb +18 -0
  93. data/lib/solidus_subscriptions/testing_support/factories/spree_modification_factory.rb +8 -0
  94. data/lib/solidus_subscriptions/testing_support/factories/subscription_factory.rb +43 -0
  95. data/lib/solidus_subscriptions/version.rb +3 -0
  96. data/lib/tasks/process_subscriptions.rake +6 -0
  97. 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